JWT 简析与使用

常见的认证机制

在了解 jwt 是什么之前需要知道有哪些认证机制。

HTTP Basic Auth

HTTP Basic Auth简单点说明就是每次请求API时都提供用户的username和password,简言之, Basic Auth是配合RESTful API 使用的最简单的认证方式,只需提供用户名密码即可,但由于有把用户 名密码暴露给第三方客户端的风险,在生产环境下被使用的越来越少。因此,在开发对外开放的 RESTful API时,尽量避免采用HTTP Basic Auth。

Cookie认证机制就是为一次请求认证在服务端创建一个Session对象,同时在客户端的浏览器端创建了一个Cookie对象;通过客户端带上来Cookie对象来与服务器端的session对象匹配来实现状态管理 的。

默认的,当我们关闭浏览器的时候,cookie会被删除。但可以通过修改cookie 的expire time使 cookie在一定时间内有效。

OAuth(开放授权,Open Authorization)是一个开放的授权标准,允许用户让第三方应用访问该用 户在某一web服务上存储的私密的资源(如照片,视频,联系人列表),而无需将用户名和密码提供给 第三方应用。如网站通过微信、微博登录等,主要用于第三方登录。

OAuth允许用户提供一个令牌,而不是用户名和密码来访问他们存放在特定服务提供者的数据。每一 个令牌授权一个特定的第三方系统(例如,视频编辑网站)在特定的时段(例如,接下来的2小时内)内 访问特定的资源(例如仅仅是某一相册中的视频)。这样,OAuth让用户可以授权第三方网站访问他们 存储在另外服务提供者的某些特定信息,而非所有内容。

下面是OAuth2.0的流程:

这种基于OAuth的认证机制适用于个人消费者类的互联网产品,如社交类APP等应用,但是不太适合 拥有自有认证权限管理的企业应用。

使用基于 Token 的身份验证方法,在服务端不需要存储用户的登录记录。大概的流程是这样的:

2. 服务端收到请求,去验证用户名与密码 3. 验证成功后,服务端会签发一个 Token,再把这个 Token 发送给客户端 4. 客户端收到 Token 以后可以把它存储起来,比如放在 Cookie 里,或者 header 里面 5. 客户端每次向服务端请求资源的时候需要带着服务端签发的 Token 6. 服务端收到请求,然后去验证客户端请求里面带着的 Token,如果验证成功,就向客户端返回请 求的数据

比第一种方式更安全,比第二种方式更节约服务器资源,比第三种方式更加轻量。

具体,Token Auth的优点( Token机制相对于Cookie机制又有什么好处呢? ):

  1. 支持跨域访问: Cookie是不允许垮域访问的,这一点对Token机制是不存在的,前提是传输的用户 认证信息通过HTTP头传输。
  2. 无状态(也称:服务端可扩展行):Token机制在服务端不需要存储session信息,因为Token 自身包 含了所有登录用户的信息,只需要在客户端的cookie或本地介质存储状态信息。
  3. 更适用CDN: 可以通过内容分发网络请求你服务端的所有资料(如:javascript,HTML,图片 等),而你的服务端只要提供API即可。
  4. 去耦: 不需要绑定到一个特定的身份验证方案。Token可以在任何地方生成,只要在你的API被调用 的时候,你可以进行Token生成调用即可。
  5. 更适用于移动应用: 当你的客户端是一个原生平台(iOS, Android,Windows 10等)时,Cookie 是不被支持的(你需要通过Cookie容器进行处理),这时采用Token认证机制就会简单得多。
  6. CSRF:因为不再依赖于Cookie,所以你就不需要考虑对CSRF(跨站请求伪造)的防范。
  7. 性能: 一次网络往返时间(通过数据库查询session信息)总比做一次HMACSHA256计算的Token 验证和解析要费时得多。
  8. 不需要为登录页面做特殊处理: 如果你使用Protractor 做功能测试的时候,不再需要为登录页面做 特殊处理。
  9. 基于标准化:你的API可以采用标准化的 JSON Web Token (JWT). 这个标准已经存在多个后端库 (.NET, Ruby, Java,Python, PHP)和多家公司的支持(如:Firebase,Google, Microsoft)。

在本章中我们 Token Auth 中使用的保存的 token 是 jwt 。

Jwt 简介

什么是 Jwt

JSON Web Token(JWT)是一个开放的行业标准(RFC 7519),它定义了一种简介的、自包含的协 议格式,用于在通信双方传递json对象,传递的信息经过数字签名可以被验证和信任。JWT可以使用 HMAC算法或使用RSA的公钥/私钥对来签名,防止被篡改。

Jwt 在线解析网站:JWT.IO

JWT令牌的优点:

  1. jwt基于json,非常方便解析。
  2. 可以在令牌中自定义丰富的内容,易扩展。
  3. 通过非对称加密算法及数字签名技术,JWT防止篡改,安全性高。
  4. 资源服务使用JWT可不依赖认证服务即可完成授权。

缺点:

  1. JWT令牌较长,占存储空间比较大。

Jwt 的组成

一个JWT实际上就是一个字符串,它由三部分组成,头部、载荷与签名。

头部用于描述关于该 JWT 的最基本的信息,例如其类型(即 JW T)以及签名所用的算法(如 HMAC SHA256 或 RSA)等。这也可以被表示成一个 JSON 对象。

{
    "alg": "HS256",
    "typ":"JWT"
 }
  • typ :是类型。
  • alg :签名的算法,这里使用的算法是 HS256 算法

我们对头部的 json 字符串进行 BASE64 编码(网上有很多在线编码的网站),编码后的字符串如下:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9

Base64 是一种基于 64 个可打印字符来表示二进制数据的表示方法。由于2的6次方等于 64,所以每6 个比特为一个单元,对应某个可打印字符。三个字节有 24 个比特,对应于 4 个 Base64 单元,即3个字节 需要用 4 个可打印字符来表示。JDK 中提供了非常方便的 BASE64Encoder 和 BASE64Decoder ,用它们 可以非常方便的完成基于 BASE64 的编码和解码。

Payload

第二部分是负载,就是存放有效信息的地方。这个名字像是特指飞机上承载的货品,这些有效信息包含 三个部分:

  1. 标准中注册的声明(建议但不强制使用)

    • iss: jwt 签发者
    • sub: jwt 所面向的用户
    • aud: 接收j wt 的一方
    • exp: jwt 的过期时间,这个过期时间必须要大于签发时间
    • nbf: 定义在什么时间之前,该 jwt 都是不可用的.
    • iat: jwt 的签发时间
    • jti: jwt 的唯一身份标识,主要用来作为一次性 token,从而回避重放攻击。
  2. 公共的声明

    公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.但不建议添加 敏感信息,因为该部分在客户端可解密.

  3. 私有的声明

    私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为 base64 是对称解密 的,意味着该部分信息可以归类为明文信息。

    这个指的就是自定义的 claim。比如下面那个举例中的 name 都属于自定的 claim 。这些 claim 跟 JWT 标 准规定的 claim 区别在于:JWT 规定的 claim,JWT 的接收方在拿到 JWT 之后,都知道怎么对这些标准的 claim 进行验证(还不知道是否能够验证);而private claims不会验证,除非明确告诉接收方要对这些 claim 进行验证以及规则才行。

    {
      "sub": "1234567890",
      "name": "John Doe",
      "iat": 1516239022
    }
    

    其中sub 是标准的声明, name 是自定义的声明(公共的或私有的)。然后将其进行base64编码,得到Jwt的第二部分:

    eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkphbWVzIiwiYWRtaW4iOnRydWV9
    

Signature

jwt的第三部分是一个签证信息,这个签证信息由三部分组成:

  1. header (base64后的)
  2. payload (base64后的)
  3. secret(盐,一定要保密)

这个部分需要 base64 加密后的 header 和 base64 加密后的 payload 使用。连接组成的字符串,然后通过 header 中声明的加密方式进行加盐 secret 组合加密,然后就构成了 jwt 的第三部分:

8HI-Lod0ncfVDnbKIPJJqLH998duF9DSDGkx3gRPNVI

将这三部分用.连接成一个完整的字符串,构成了最终的jwt:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4 gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.8HI-Lod0ncfVDnbKIPJJqLH998duF9DSDGkx3gRPNVI

注意:secret 是保存在服务器端的, jwt 的签发生成也是在服务器端的, secret 就是用来进行 jwt 的签发和jwt 的验证,所以,它就是你服务端的私钥,在任何场景都不应该流露出去。一旦客户端得知 这个secret , 那就意味着客户端是可以自我签发 jwt 了。

Jwt 与 Spring Boot 的初步整合

JJWT是一个提供端到端的JWT创建和验证的Java库。永远免费和开源(Apache License,版本2.0),JJW 很容易使用和理解。它被设计成一个以建筑为中心的流畅界面,隐藏了它的大部分复杂性。

依赖导入

<!--pom.xml-->
<!--JWT依赖-->
<dependency>
  <groupId>io.jsonwebtoken</groupId>
  <artifactId>jjwt</artifactId>
  <version>0.9.0</version>
</dependency>

如何创建一个 Jwt 字符串
使用 jjwt 提供的 JwtBuilder。测试代码如下

/**
 * 测试 Jwt 生成
 */
@Test
public void testCreateToken(){
  //创建一个JwtBuilder对象
  JwtBuilder jwtBuilder = Jwts.builder()
      //声明的标识{"jti":"888"}
      .setId("888")
      // 主体,用户{"sub":"Rose"}
      .setSubject("Rose")
      // 创建日期{"ita":"xxxxxx"}
      .setIssuedAt(new Date())
      //签名手段,参数1:算法,参数2:盐
      .signWith(SignatureAlgorithm.HS256,"xxxx");
  String token = jwtBuilder.compact();
  System.out.println(token);
  // 三部分的 Base64 解密
  System.out.println("--------------------");
  String[] split = token.split("\\.");
    // header 包含了 加密算法, 类型
  System.out.println(Base64Codec.BASE64.decodeToString(split[0]));
    // payload 包含了 签发者,用户,接收方,过期时间,签发时间,身份标识...
  System.out.println(Base64Codec.BASE64.decodeToString(split[1]));
    //无法解密 第三部分是采用加密算法生成的,无法使用 Base64 解密
  System.out.println(Base64Codec.BASE64.decodeToString(split[2]));
}

如何将一个 jwt token 解析出来,jwt 中的内容放在哪儿?
jjwt 提供了一个 Claims 类,拓展了 Map 接口,可以用于存放 jwt 中的键值对。测试代码如下

/**
 * 测试 Token 解析
 */
@Test
public void testParseToken(){
	String token="eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODgiLCJzdWIiOiJSb3NlIiwiaWF0IjoxNjE0NTAzNjIyfQ.YcIJ5hOZxPELf9yAtWt0jCOK6_EgHiOjSWZdeUdV7gs";
	//解析token获取负载中的声明对象
	Claims claims = Jwts.parser()
			.setSigningKey("xxxx")
			.parseClaimsJws(token)
			.getBody();
	//解析token获取负载中的声明对象
	System.out.println(claims.getId());
	System.out.println(claims.getSubject());
	System.out.println(claims.getIssuedAt());
}

如何给一个 jwt 设置过期时间?
在使用 Jwts.builder() 时 setExpiration 就可以得到一个有过期时间的 jwt token 了。示例代码如下

public void testCreateTokenHasExp(){
	//当前系统时间的长整型
	long now=System.currentTimeMillis();
	//过期时间,这里是1分钟后的时间长整型
	long exp=now+60*1000;
	//当前系统时间的长整型
	JwtBuilder builder = Jwts.builder()
			//声明的标识{"jti":"888"}
			.setId("888")
			//主体,用户{"sub":"Rose"}
			.setSubject("Rose")
			//创建日期{"ita":"xxxxxx"}
			.setIssuedAt(new Date())
			//签名手段,参数1:算法,参数2:盐
			.signWith(SignatureAlgorithm.HS256, "xxxx")
			//设置过期时间
			.setExpiration(new Date(exp));
	String token= builder.compact();
	System.out.println(token);
}

如何解析出 jwt token 中的过期时间?

直接从 Claims 中获取 Expiration 就行了。示例代码如下

/**
 * 测试带有过期时间的 jwt token 解析
 */
@Test
public void testParseTokenHasExp(){
	String token="eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODgiLCJzdWIiOiJSb3NlIiwiaWF0IjoxNjE0NTA0Mjk4LCJleHAiOjE2MTQ1MDQzNTh9.vmp1nnQIC4i5JKQ6yDLBaqqTvBJa4ONcsNdWraOF9UM";
	//解析token获取负载中的声明对象
	Claims claims = Jwts.parser()
			.setSigningKey("xxxx")
			.parseClaimsJws(token)
			.getBody();

	System.out.println("id:" + claims.getId());
	System.out.println("subject:" + claims.getSubject());
	System.out.println("issuedAt:" + claims.getIssuedAt());
	DateFormat sf =new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
	System.out.println("签发时间:"+sf.format(claims.getIssuedAt()));
	System.out.println("过期时间:"+sf.format(claims.getExpiration()));
	System.out.println("当前时间:"+sf.format(new Date()));
}

在之前我们提到过,jwt 的 playload 中可以自定义添加属性。 如何生成一个有自定义属性的 jwt token?
在 Jwts.builder() 时 claim 需要的属性就行了。当然,你也可以将你需要的添加的多个属性组成一个 Map,之后 claim 也行。具体见示例代码

/**
 * 测试带自定义 claims 的 token 生成
 */
@Test
public void testCreateTokenByClaims(){
	//当前系统时间的长整型
	long now = System.currentTimeMillis();
	//过期时间,这里是1分钟后的时间长整型
	long exp = now + 60 * 1000;
	//创建一个JwtBuilder对象
	JwtBuilder jwtBuilder = Jwts.builder()
			//声明的标识{"jti":"888"}
			.setId("888")
			//主体,用户{"sub":"Rose"}
			.setSubject("Rose")
			//创建日期{"ita":"xxxxxx"}
			.setIssuedAt(new Date())
			//签名手段,参数1:算法,参数2:盐
			.signWith(SignatureAlgorithm.HS256, "xxxx")
			//设置过期时间
			.setExpiration(new Date(exp)).claim("roles","admin")
			//直接传入map
			// .addClaims(map)
			.claim("roles","admin")
			.claim("logo","shsxt.jpg");
	//获取jwt的token
	String token = jwtBuilder.compact();
	System.out.println(token);
}

如何将自定义的属性从 jwt token 中解析出来呢?
直接从 Claims 中取就行了。示例代码

/**
 * 测试带 自定义 claims 的 token 解析
 */
@Test public void testParseTokenByClaims() {
	String token ="eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODgiLCJzdWIiOiJSb3NlIiwiaWF0IjoxNjE0NTA0ODIxLCJleHAiOjE2MTQ1MDQ4ODEsInJvbGVzIjoiYWRtaW4iLCJsb2dvIjoic2hzeHQuanBnIn0.8RN8y-XR1aFQc0ZzlhoU_uXVzb1WVqNxJqHZZToIdTo";
	//解析token获取负载中的声明对象
	Claims claims = Jwts.parser()
			.setSigningKey("xxxx")
			.parseClaimsJws(token)
			.getBody();
	//打印声明的属性
	System.out.println("id:" + claims.getId());
	System.out.println("subject:" + claims.getSubject());
	System.out.println("issuedAt:" + claims.getIssuedAt());
	DateFormat sf =new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
	System.out.println("签发时间:"+sf.format(claims.getIssuedAt()));
	System.out.println("过期时间:"+sf.format(claims.getExpiration()));
	System.out.println("当前时间:"+sf.format(new Date()));
	System.out.println("roles:"+claims.get("roles"));
	System.out.println("logo:"+claims.get("logo"));
}

Jwt 工具类

/**
 * JWT生产Token工具类
 */
@Component
public class JWTTokenUtils {
  /**
   * 时间格式化
   */
  private static final DateTimeFormatter df = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

  @Autowired
  private SysUserDetailsService sysUserDetailsService;

  private static JWTTokenUtils jwtTokenUtils;

  @PostConstruct
  public void init() {
    jwtTokenUtils = this;
    jwtTokenUtils.sysUserDetailsService = this.sysUserDetailsService;
  }

  /**
   * 创建Token
   *
   * @param sysUserDetails 用户信息
   * @return
   */
  public static String createAccessToken(SysUserDetails sysUserDetails) {
    String token = Jwts.builder()// 设置JWT
        .setId(sysUserDetails.getId().toString()) // 用户Id
        .setSubject(sysUserDetails.getUsername()) // 主题
        .setIssuedAt(new Date()) // 签发时间
        .setIssuer("Mvbbb") // 签发者
        .setExpiration(new Date(System.currentTimeMillis() + JWTConfig.expiration)) // 过期时间
        .signWith(SignatureAlgorithm.HS512, JWTConfig.secret) // 签名算法、密钥
        .claim("authorities", JSON.toJSONString(sysUserDetails.getAuthorities()))// 自定义其他属性,如用户组织机构ID,用户所拥有的角色,用户权限信息等
        .claim("ip", sysUserDetails.getIp()).compact();
    return JWTConfig.tokenPrefix + token;
  }

  /**
   * 刷新Token
   *
   * @param oldToken 过期但未超过刷新时间的Token
   * @return
   */
  public static String refreshAccessToken(String oldToken) {
    String username = JWTTokenUtils.getUserNameByToken(oldToken);
    SysUserDetails sysUserDetails = (SysUserDetails) jwtTokenUtils.sysUserDetailsService
        .loadUserByUsername(username);
    sysUserDetails.setIp(JWTTokenUtils.getIpByToken(oldToken));
    return createAccessToken(sysUserDetails);
  }

  /**
   * 解析Token
   *
   * @param token Token信息
   * @return
   */
  public static SysUserDetails parseAccessToken(String token) {
    SysUserDetails sysUserDetails = null;
    if (StringUtils.isNotEmpty(token)) {
      try {
        // 去除JWT前缀
        token = token.substring(JWTConfig.tokenPrefix.length());

        // 解析Token
        Claims claims = Jwts.parser().setSigningKey(JWTConfig.secret).parseClaimsJws(token).getBody();

        // 获取用户信息
        sysUserDetails = new SysUserDetails();
        sysUserDetails.setId(Long.parseLong(claims.getId()));
        sysUserDetails.setUsername(claims.getSubject());

        // 获取角色
        Set<GrantedAuthority> authorities = new HashSet<GrantedAuthority>();
        String authority = claims.get("authorities").toString();
        if (StringUtils.isNotEmpty(authority)) {
          List<Map<String, String>> authorityList = JSON.parseObject(authority,
              new TypeReference<List<Map<String, String>>>() {
              });
          for (Map<String, String> role : authorityList) {
            if (!role.isEmpty()) {
              authorities.add(new SimpleGrantedAuthority(role.get("authority")));
            }
          }
        }

        sysUserDetails.setAuthorities(authorities);

        // 获取IP
        String ip = claims.get("ip").toString();
        sysUserDetails.setIp(ip);
      } catch (Exception e) {
        log.error("解析Token异常:" + e);
      }
    }
    return sysUserDetails;
  }

  /**
   * 保存Token信息到Redis中
   *
   * @param token    Token信息
   * @param username 用户名
   * @param ip       IP
   */
  public static void setTokenInfo(String token, String username, String ip) {
    if (StringUtils.isNotEmpty(token)) {
      // 去除JWT前缀
      token = token.substring(JWTConfig.tokenPrefix.length());

      Integer refreshTime = JWTConfig.refreshTime;
      LocalDateTime localDateTime = LocalDateTime.now();

      RedisUtils.hset(token, "username", username, refreshTime);
      RedisUtils.hset(token, "ip", ip, refreshTime);
      RedisUtils.hset(token, "refreshTime",
          df.format(localDateTime.plus(JWTConfig.refreshTime, ChronoUnit.MILLIS)), refreshTime);
      RedisUtils.hset(token, "expiration", df.format(localDateTime.plus(JWTConfig.expiration, ChronoUnit.MILLIS)),
          refreshTime);
    }
  }

  /**
   * 将Token放到黑名单中
   *
   * @param token Token信息
   */
  public static void addBlackList(String token) {
    if (StringUtils.isNotEmpty(token)) {
      // 去除JWT前缀
      token = token.substring(JWTConfig.tokenPrefix.length());
      RedisUtils.hset("blackList", token, df.format(LocalDateTime.now()));
    }
  }

  /**
   * Redis中删除Token
   *
   * @param token Token信息
   */
  public static void deleteRedisToken(String token) {
    if (StringUtils.isNotEmpty(token)) {
      // 去除JWT前缀
      token = token.substring(JWTConfig.tokenPrefix.length());
      RedisUtils.deleteKey(token);
    }
  }

  /**
   * 判断当前Token是否在黑名单中
   *
   * @param token Token信息
   */
  public static boolean isBlackList(String token) {
    if (StringUtils.isNotEmpty(token)) {
      // 去除JWT前缀
      token = token.substring(JWTConfig.tokenPrefix.length());
      return RedisUtils.hasKey("blackList", token);
    }
    return false;
  }

  /**
   * 是否过期
   *
   * @param expiration 过期时间,字符串
   * @return 过期返回True,未过期返回false
   */
  public static boolean isExpiration(String expiration) {
    LocalDateTime expirationTime = LocalDateTime.parse(expiration, df);
    LocalDateTime localDateTime = LocalDateTime.now();
    if (localDateTime.compareTo(expirationTime) > 0) {
      return true;
    }
    return false;
  }

  /**
   * 是否有效
   *
   * @param refreshTime 刷新时间,字符串
   * @return 有效返回True,无效返回false
   */
  public static boolean isValid(String refreshTime) {
    LocalDateTime validTime = LocalDateTime.parse(refreshTime, df);
    LocalDateTime localDateTime = LocalDateTime.now();
    if (localDateTime.compareTo(validTime) > 0) {
      return false;
    }
    return true;
  }

  /**
   * 检查Redis中是否存在Token
   *
   * @param token Token信息
   * @return
   */
  public static boolean hasToken(String token) {
    if (StringUtils.isNotEmpty(token)) {
      // 去除JWT前缀
      token = token.substring(JWTConfig.tokenPrefix.length());
      return RedisUtils.hasKey(token);
    }
    return false;
  }

  /**
   * 从Redis中获取过期时间
   *
   * @param token Token信息
   * @return 过期时间,字符串
   */
  public static String getExpirationByToken(String token) {
    if (StringUtils.isNotEmpty(token)) {
      // 去除JWT前缀
      token = token.substring(JWTConfig.tokenPrefix.length());
      return RedisUtils.hget(token, "expiration").toString();
    }
    return null;
  }

  /**
   * 从Redis中获取刷新时间
   *
   * @param token Token信息
   * @return 刷新时间,字符串
   */
  public static String getRefreshTimeByToken(String token) {
    if (StringUtils.isNotEmpty(token)) {
      // 去除JWT前缀
      token = token.substring(JWTConfig.tokenPrefix.length());
      return RedisUtils.hget(token, "refreshTime").toString();
    }
    return null;
  }

  /**
   * 从Redis中获取用户名
   *
   * @param token Token信息
   * @return
   */
  public static String getUserNameByToken(String token) {
    if (StringUtils.isNotEmpty(token)) {
      // 去除JWT前缀
      token = token.substring(JWTConfig.tokenPrefix.length());
      return RedisUtils.hget(token, "username").toString();
    }
    return null;
  }

  /**
   * 从Redis中获取IP
   *
   * @param token Token信息
   * @return
   */
  public static String getIpByToken(String token) {
    if (StringUtils.isNotEmpty(token)) {
      // 去除JWT前缀
      token = token.substring(JWTConfig.tokenPrefix.length());
      return RedisUtils.hget(token, "ip").toString();
    }
    return null;
  }

}

title : ‘JWT 简析与使用’ slug : ‘jwt-usage’ date : 2021-03-24 tags : - ‘JWT’ - ‘SpringBoot’ categories : - ‘Java’ description : ‘JSON Web Token(JWT)是一个开放的行业标准(RFC 7519),它定义了一种简介的、自包含的协 议格式,用于在通信双方传递json对象,传递的信息经过数字签名可以被验证和信任。JWT可以使用 HMAC算法或使用RSA的公钥/私钥对来签名,防止被篡改。’

常见的认证机制

在了解 jwt 是什么之前需要知道有哪些认证机制。

HTTP Basic Auth

HTTP Basic Auth简单点说明就是每次请求API时都提供用户的username和password,简言之, Basic Auth是配合RESTful API 使用的最简单的认证方式,只需提供用户名密码即可,但由于有把用户 名密码暴露给第三方客户端的风险,在生产环境下被使用的越来越少。因此,在开发对外开放的 RESTful API时,尽量避免采用HTTP Basic Auth。

Cookie认证机制就是为一次请求认证在服务端创建一个Session对象,同时在客户端的浏览器端创建了一个Cookie对象;通过客户端带上来Cookie对象来与服务器端的session对象匹配来实现状态管理 的。

默认的,当我们关闭浏览器的时候,cookie会被删除。但可以通过修改cookie 的expire time使 cookie在一定时间内有效。

OAuth(开放授权,Open Authorization)是一个开放的授权标准,允许用户让第三方应用访问该用 户在某一web服务上存储的私密的资源(如照片,视频,联系人列表),而无需将用户名和密码提供给 第三方应用。如网站通过微信、微博登录等,主要用于第三方登录。

OAuth允许用户提供一个令牌,而不是用户名和密码来访问他们存放在特定服务提供者的数据。每一 个令牌授权一个特定的第三方系统(例如,视频编辑网站)在特定的时段(例如,接下来的2小时内)内 访问特定的资源(例如仅仅是某一相册中的视频)。这样,OAuth让用户可以授权第三方网站访问他们 存储在另外服务提供者的某些特定信息,而非所有内容。

下面是OAuth2.0的流程:

这种基于OAuth的认证机制适用于个人消费者类的互联网产品,如社交类APP等应用,但是不太适合 拥有自有认证权限管理的企业应用。

使用基于 Token 的身份验证方法,在服务端不需要存储用户的登录记录。大概的流程是这样的:

2. 服务端收到请求,去验证用户名与密码 3. 验证成功后,服务端会签发一个 Token,再把这个 Token 发送给客户端 4. 客户端收到 Token 以后可以把它存储起来,比如放在 Cookie 里,或者 header 里面 5. 客户端每次向服务端请求资源的时候需要带着服务端签发的 Token 6. 服务端收到请求,然后去验证客户端请求里面带着的 Token,如果验证成功,就向客户端返回请 求的数据

比第一种方式更安全,比第二种方式更节约服务器资源,比第三种方式更加轻量。

具体,Token Auth的优点( Token机制相对于Cookie机制又有什么好处呢? ):

  1. 支持跨域访问: Cookie是不允许垮域访问的,这一点对Token机制是不存在的,前提是传输的用户 认证信息通过HTTP头传输。
  2. 无状态(也称:服务端可扩展行):Token机制在服务端不需要存储session信息,因为Token 自身包 含了所有登录用户的信息,只需要在客户端的cookie或本地介质存储状态信息。
  3. 更适用CDN: 可以通过内容分发网络请求你服务端的所有资料(如:javascript,HTML,图片 等),而你的服务端只要提供API即可。
  4. 去耦: 不需要绑定到一个特定的身份验证方案。Token可以在任何地方生成,只要在你的API被调用 的时候,你可以进行Token生成调用即可。
  5. 更适用于移动应用: 当你的客户端是一个原生平台(iOS, Android,Windows 10等)时,Cookie 是不被支持的(你需要通过Cookie容器进行处理),这时采用Token认证机制就会简单得多。
  6. CSRF:因为不再依赖于Cookie,所以你就不需要考虑对CSRF(跨站请求伪造)的防范。
  7. 性能: 一次网络往返时间(通过数据库查询session信息)总比做一次HMACSHA256计算的Token 验证和解析要费时得多。
  8. 不需要为登录页面做特殊处理: 如果你使用Protractor 做功能测试的时候,不再需要为登录页面做 特殊处理。
  9. 基于标准化:你的API可以采用标准化的 JSON Web Token (JWT). 这个标准已经存在多个后端库 (.NET, Ruby, Java,Python, PHP)和多家公司的支持(如:Firebase,Google, Microsoft)。

在本章中我们 Token Auth 中使用的保存的 token 是 jwt 。

Jwt 简介

什么是 Jwt

JSON Web Token(JWT)是一个开放的行业标准(RFC 7519),它定义了一种简介的、自包含的协 议格式,用于在通信双方传递json对象,传递的信息经过数字签名可以被验证和信任。JWT可以使用 HMAC算法或使用RSA的公钥/私钥对来签名,防止被篡改。

Jwt 在线解析网站:JWT.IO

JWT令牌的优点:

  1. jwt基于json,非常方便解析。
  2. 可以在令牌中自定义丰富的内容,易扩展。
  3. 通过非对称加密算法及数字签名技术,JWT防止篡改,安全性高。
  4. 资源服务使用JWT可不依赖认证服务即可完成授权。

缺点:

  1. JWT令牌较长,占存储空间比较大。

Jwt 的组成

一个JWT实际上就是一个字符串,它由三部分组成,头部、载荷与签名。

Header

头部用于描述关于该 JWT 的最基本的信息,例如其类型(即 JW T)以及签名所用的算法(如 HMAC SHA256 或 RSA)等。这也可以被表示成一个 JSON 对象。

{
    "alg": "HS256",
    "typ":"JWT"
 }
  • typ :是类型。
  • alg :签名的算法,这里使用的算法是 HS256 算法

我们对头部的 json 字符串进行 BASE64 编码(网上有很多在线编码的网站),编码后的字符串如下:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9

Base64 是一种基于 64 个可打印字符来表示二进制数据的表示方法。由于2的6次方等于 64,所以每6 个比特为一个单元,对应某个可打印字符。三个字节有 24 个比特,对应于 4 个 Base64 单元,即3个字节 需要用 4 个可打印字符来表示。JDK 中提供了非常方便的 BASE64Encoder 和 BASE64Decoder ,用它们 可以非常方便的完成基于 BASE64 的编码和解码。

Payload

第二部分是负载,就是存放有效信息的地方。这个名字像是特指飞机上承载的货品,这些有效信息包含 三个部分:

  1. 标准中注册的声明(建议但不强制使用)

    • iss: jwt 签发者
    • sub: jwt 所面向的用户
    • aud: 接收j wt 的一方
    • exp: jwt 的过期时间,这个过期时间必须要大于签发时间
    • nbf: 定义在什么时间之前,该 jwt 都是不可用的.
    • iat: jwt 的签发时间
    • jti: jwt 的唯一身份标识,主要用来作为一次性 token,从而回避重放攻击。
  2. 公共的声明

    公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.但不建议添加 敏感信息,因为该部分在客户端可解密.

  3. 私有的声明

    私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为 base64 是对称解密 的,意味着该部分信息可以归类为明文信息。

    这个指的就是自定义的 claim。比如下面那个举例中的 name 都属于自定的 claim 。这些 claim 跟 JWT 标 准规定的 claim 区别在于:JWT 规定的 claim,JWT 的接收方在拿到 JWT 之后,都知道怎么对这些标准的 claim 进行验证(还不知道是否能够验证);而private claims不会验证,除非明确告诉接收方要对这些 claim 进行验证以及规则才行。

    {
      "sub": "1234567890",
      "name": "John Doe",
      "iat": 1516239022
    }
    

    其中sub 是标准的声明, name 是自定义的声明(公共的或私有的)。然后将其进行base64编码,得到Jwt的第二部分:

    eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkphbWVzIiwiYWRtaW4iOnRydWV9
    

Signature

jwt的第三部分是一个签证信息,这个签证信息由三部分组成:

  1. header (base64后的)
  2. payload (base64后的)
  3. secret(盐,一定要保密)

这个部分需要 base64 加密后的 header 和 base64 加密后的 payload 使用。连接组成的字符串,然后通过 header 中声明的加密方式进行加盐 secret 组合加密,然后就构成了 jwt 的第三部分:

8HI-Lod0ncfVDnbKIPJJqLH998duF9DSDGkx3gRPNVI

将这三部分用.连接成一个完整的字符串,构成了最终的jwt:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4 gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.8HI-Lod0ncfVDnbKIPJJqLH998duF9DSDGkx3gRPNVI

注意:secret 是保存在服务器端的, jwt 的签发生成也是在服务器端的, secret 就是用来进行 jwt 的签发和jwt 的验证,所以,它就是你服务端的私钥,在任何场景都不应该流露出去。一旦客户端得知 这个secret , 那就意味着客户端是可以自我签发 jwt 了。

Jwt 与 Spring Boot 的初步整合

JJWT是一个提供端到端的JWT创建和验证的Java库。永远免费和开源(Apache License,版本2.0),JJW 很容易使用和理解。它被设计成一个以建筑为中心的流畅界面,隐藏了它的大部分复杂性。

依赖导入

<!--pom.xml-->
<!--JWT依赖-->
<dependency>
  <groupId>io.jsonwebtoken</groupId>
  <artifactId>jjwt</artifactId>
  <version>0.9.0</version>
</dependency>

如何创建一个 Jwt 字符串
使用 jjwt 提供的 JwtBuilder。测试代码如下

/**
 * 测试 Jwt 生成
 */
@Test
public void testCreateToken(){
  //创建一个JwtBuilder对象
  JwtBuilder jwtBuilder = Jwts.builder()
      //声明的标识{"jti":"888"}
      .setId("888")
      // 主体,用户{"sub":"Rose"}
      .setSubject("Rose")
      // 创建日期{"ita":"xxxxxx"}
      .setIssuedAt(new Date())
      //签名手段,参数1:算法,参数2:盐
      .signWith(SignatureAlgorithm.HS256,"xxxx");
  String token = jwtBuilder.compact();
  System.out.println(token);
  // 三部分的 Base64 解密
  System.out.println("--------------------");
  String[] split = token.split("\\.");
    // header 包含了 加密算法, 类型
  System.out.println(Base64Codec.BASE64.decodeToString(split[0]));
    // payload 包含了 签发者,用户,接收方,过期时间,签发时间,身份标识...
  System.out.println(Base64Codec.BASE64.decodeToString(split[1]));
    //无法解密 第三部分是采用加密算法生成的,无法使用 Base64 解密
  System.out.println(Base64Codec.BASE64.decodeToString(split[2]));
}

如何将一个 jwt token 解析出来,jwt 中的内容放在哪儿?
jjwt 提供了一个 Claims 类,拓展了 Map 接口,可以用于存放 jwt 中的键值对。测试代码如下

/**
 * 测试 Token 解析
 */
@Test
public void testParseToken(){
	String token="eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODgiLCJzdWIiOiJSb3NlIiwiaWF0IjoxNjE0NTAzNjIyfQ.YcIJ5hOZxPELf9yAtWt0jCOK6_EgHiOjSWZdeUdV7gs";
	//解析token获取负载中的声明对象
	Claims claims = Jwts.parser()
			.setSigningKey("xxxx")
			.parseClaimsJws(token)
			.getBody();
	//解析token获取负载中的声明对象
	System.out.println(claims.getId());
	System.out.println(claims.getSubject());
	System.out.println(claims.getIssuedAt());
}

如何给一个 jwt 设置过期时间?
在使用 Jwts.builder() 时 setExpiration 就可以得到一个有过期时间的 jwt token 了。示例代码如下

public void testCreateTokenHasExp(){
	//当前系统时间的长整型
	long now=System.currentTimeMillis();
	//过期时间,这里是1分钟后的时间长整型
	long exp=now+60*1000;
	//当前系统时间的长整型
	JwtBuilder builder = Jwts.builder()
			//声明的标识{"jti":"888"}
			.setId("888")
			//主体,用户{"sub":"Rose"}
			.setSubject("Rose")
			//创建日期{"ita":"xxxxxx"}
			.setIssuedAt(new Date())
			//签名手段,参数1:算法,参数2:盐
			.signWith(SignatureAlgorithm.HS256, "xxxx")
			//设置过期时间
			.setExpiration(new Date(exp));
	String token= builder.compact();
	System.out.println(token);
}

如何解析出 jwt token 中的过期时间?

直接从 Claims 中获取 Expiration 就行了。示例代码如下

/**
 * 测试带有过期时间的 jwt token 解析
 */
@Test
public void testParseTokenHasExp(){
	String token="eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODgiLCJzdWIiOiJSb3NlIiwiaWF0IjoxNjE0NTA0Mjk4LCJleHAiOjE2MTQ1MDQzNTh9.vmp1nnQIC4i5JKQ6yDLBaqqTvBJa4ONcsNdWraOF9UM";
	//解析token获取负载中的声明对象
	Claims claims = Jwts.parser()
			.setSigningKey("xxxx")
			.parseClaimsJws(token)
			.getBody();

	System.out.println("id:" + claims.getId());
	System.out.println("subject:" + claims.getSubject());
	System.out.println("issuedAt:" + claims.getIssuedAt());
	DateFormat sf =new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
	System.out.println("签发时间:"+sf.format(claims.getIssuedAt()));
	System.out.println("过期时间:"+sf.format(claims.getExpiration()));
	System.out.println("当前时间:"+sf.format(new Date()));
}

在之前我们提到过,jwt 的 playload 中可以自定义添加属性。 如何生成一个有自定义属性的 jwt token?
在 Jwts.builder() 时 claim 需要的属性就行了。当然,你也可以将你需要的添加的多个属性组成一个 Map,之后 claim 也行。具体见示例代码

/**
 * 测试带自定义 claims 的 token 生成
 */
@Test
public void testCreateTokenByClaims(){
	//当前系统时间的长整型
	long now = System.currentTimeMillis();
	//过期时间,这里是1分钟后的时间长整型
	long exp = now + 60 * 1000;
	//创建一个JwtBuilder对象
	JwtBuilder jwtBuilder = Jwts.builder()
			//声明的标识{"jti":"888"}
			.setId("888")
			//主体,用户{"sub":"Rose"}
			.setSubject("Rose")
			//创建日期{"ita":"xxxxxx"}
			.setIssuedAt(new Date())
			//签名手段,参数1:算法,参数2:盐
			.signWith(SignatureAlgorithm.HS256, "xxxx")
			//设置过期时间
			.setExpiration(new Date(exp)).claim("roles","admin")
			//直接传入map
			// .addClaims(map)
			.claim("roles","admin")
			.claim("logo","shsxt.jpg");
	//获取jwt的token
	String token = jwtBuilder.compact();
	System.out.println(token);
}

如何将自定义的属性从 jwt token 中解析出来呢?
直接从 Claims 中取就行了。示例代码

/**
 * 测试带 自定义 claims 的 token 解析
 */
@Test public void testParseTokenByClaims() {
	String token ="eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODgiLCJzdWIiOiJSb3NlIiwiaWF0IjoxNjE0NTA0ODIxLCJleHAiOjE2MTQ1MDQ4ODEsInJvbGVzIjoiYWRtaW4iLCJsb2dvIjoic2hzeHQuanBnIn0.8RN8y-XR1aFQc0ZzlhoU_uXVzb1WVqNxJqHZZToIdTo";
	//解析token获取负载中的声明对象
	Claims claims = Jwts.parser()
			.setSigningKey("xxxx")
			.parseClaimsJws(token)
			.getBody();
	//打印声明的属性
	System.out.println("id:" + claims.getId());
	System.out.println("subject:" + claims.getSubject());
	System.out.println("issuedAt:" + claims.getIssuedAt());
	DateFormat sf =new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
	System.out.println("签发时间:"+sf.format(claims.getIssuedAt()));
	System.out.println("过期时间:"+sf.format(claims.getExpiration()));
	System.out.println("当前时间:"+sf.format(new Date()));
	System.out.println("roles:"+claims.get("roles"));
	System.out.println("logo:"+claims.get("logo"));
}

Jwt 工具类

/**
 * JWT生产Token工具类
 */
@Component
public class JWTTokenUtils {
  /**
   * 时间格式化
   */
  private static final DateTimeFormatter df = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

  @Autowired
  private SysUserDetailsService sysUserDetailsService;

  private static JWTTokenUtils jwtTokenUtils;

  @PostConstruct
  public void init() {
    jwtTokenUtils = this;
    jwtTokenUtils.sysUserDetailsService = this.sysUserDetailsService;
  }

  /**
   * 创建Token
   *
   * @param sysUserDetails 用户信息
   * @return
   */
  public static String createAccessToken(SysUserDetails sysUserDetails) {
    String token = Jwts.builder()// 设置JWT
        .setId(sysUserDetails.getId().toString()) // 用户Id
        .setSubject(sysUserDetails.getUsername()) // 主题
        .setIssuedAt(new Date()) // 签发时间
        .setIssuer("Mvbbb") // 签发者
        .setExpiration(new Date(System.currentTimeMillis() + JWTConfig.expiration)) // 过期时间
        .signWith(SignatureAlgorithm.HS512, JWTConfig.secret) // 签名算法、密钥
        .claim("authorities", JSON.toJSONString(sysUserDetails.getAuthorities()))// 自定义其他属性,如用户组织机构ID,用户所拥有的角色,用户权限信息等
        .claim("ip", sysUserDetails.getIp()).compact();
    return JWTConfig.tokenPrefix + token;
  }

  /**
   * 刷新Token
   *
   * @param oldToken 过期但未超过刷新时间的Token
   * @return
   */
  public static String refreshAccessToken(String oldToken) {
    String username = JWTTokenUtils.getUserNameByToken(oldToken);
    SysUserDetails sysUserDetails = (SysUserDetails) jwtTokenUtils.sysUserDetailsService
        .loadUserByUsername(username);
    sysUserDetails.setIp(JWTTokenUtils.getIpByToken(oldToken));
    return createAccessToken(sysUserDetails);
  }

  /**
   * 解析Token
   *
   * @param token Token信息
   * @return
   */
  public static SysUserDetails parseAccessToken(String token) {
    SysUserDetails sysUserDetails = null;
    if (StringUtils.isNotEmpty(token)) {
      try {
        // 去除JWT前缀
        token = token.substring(JWTConfig.tokenPrefix.length());

        // 解析Token
        Claims claims = Jwts.parser().setSigningKey(JWTConfig.secret).parseClaimsJws(token).getBody();

        // 获取用户信息
        sysUserDetails = new SysUserDetails();
        sysUserDetails.setId(Long.parseLong(claims.getId()));
        sysUserDetails.setUsername(claims.getSubject());

        // 获取角色
        Set<GrantedAuthority> authorities = new HashSet<GrantedAuthority>();
        String authority = claims.get("authorities").toString();
        if (StringUtils.isNotEmpty(authority)) {
          List<Map<String, String>> authorityList = JSON.parseObject(authority,
              new TypeReference<List<Map<String, String>>>() {
              });
          for (Map<String, String> role : authorityList) {
            if (!role.isEmpty()) {
              authorities.add(new SimpleGrantedAuthority(role.get("authority")));
            }
          }
        }

        sysUserDetails.setAuthorities(authorities);

        // 获取IP
        String ip = claims.get("ip").toString();
        sysUserDetails.setIp(ip);
      } catch (Exception e) {
        log.error("解析Token异常:" + e);
      }
    }
    return sysUserDetails;
  }

  /**
   * 保存Token信息到Redis中
   *
   * @param token    Token信息
   * @param username 用户名
   * @param ip       IP
   */
  public static void setTokenInfo(String token, String username, String ip) {
    if (StringUtils.isNotEmpty(token)) {
      // 去除JWT前缀
      token = token.substring(JWTConfig.tokenPrefix.length());

      Integer refreshTime = JWTConfig.refreshTime;
      LocalDateTime localDateTime = LocalDateTime.now();

      RedisUtils.hset(token, "username", username, refreshTime);
      RedisUtils.hset(token, "ip", ip, refreshTime);
      RedisUtils.hset(token, "refreshTime",
          df.format(localDateTime.plus(JWTConfig.refreshTime, ChronoUnit.MILLIS)), refreshTime);
      RedisUtils.hset(token, "expiration", df.format(localDateTime.plus(JWTConfig.expiration, ChronoUnit.MILLIS)),
          refreshTime);
    }
  }

  /**
   * 将Token放到黑名单中
   *
   * @param token Token信息
   */
  public static void addBlackList(String token) {
    if (StringUtils.isNotEmpty(token)) {
      // 去除JWT前缀
      token = token.substring(JWTConfig.tokenPrefix.length());
      RedisUtils.hset("blackList", token, df.format(LocalDateTime.now()));
    }
  }

  /**
   * Redis中删除Token
   *
   * @param token Token信息
   */
  public static void deleteRedisToken(String token) {
    if (StringUtils.isNotEmpty(token)) {
      // 去除JWT前缀
      token = token.substring(JWTConfig.tokenPrefix.length());
      RedisUtils.deleteKey(token);
    }
  }

  /**
   * 判断当前Token是否在黑名单中
   *
   * @param token Token信息
   */
  public static boolean isBlackList(String token) {
    if (StringUtils.isNotEmpty(token)) {
      // 去除JWT前缀
      token = token.substring(JWTConfig.tokenPrefix.length());
      return RedisUtils.hasKey("blackList", token);
    }
    return false;
  }

  /**
   * 是否过期
   *
   * @param expiration 过期时间,字符串
   * @return 过期返回True,未过期返回false
   */
  public static boolean isExpiration(String expiration) {
    LocalDateTime expirationTime = LocalDateTime.parse(expiration, df);
    LocalDateTime localDateTime = LocalDateTime.now();
    if (localDateTime.compareTo(expirationTime) > 0) {
      return true;
    }
    return false;
  }

  /**
   * 是否有效
   *
   * @param refreshTime 刷新时间,字符串
   * @return 有效返回True,无效返回false
   */
  public static boolean isValid(String refreshTime) {
    LocalDateTime validTime = LocalDateTime.parse(refreshTime, df);
    LocalDateTime localDateTime = LocalDateTime.now();
    if (localDateTime.compareTo(validTime) > 0) {
      return false;
    }
    return true;
  }

  /**
   * 检查Redis中是否存在Token
   *
   * @param token Token信息
   * @return
   */
  public static boolean hasToken(String token) {
    if (StringUtils.isNotEmpty(token)) {
      // 去除JWT前缀
      token = token.substring(JWTConfig.tokenPrefix.length());
      return RedisUtils.hasKey(token);
    }
    return false;
  }

  /**
   * 从Redis中获取过期时间
   *
   * @param token Token信息
   * @return 过期时间,字符串
   */
  public static String getExpirationByToken(String token) {
    if (StringUtils.isNotEmpty(token)) {
      // 去除JWT前缀
      token = token.substring(JWTConfig.tokenPrefix.length());
      return RedisUtils.hget(token, "expiration").toString();
    }
    return null;
  }

  /**
   * 从Redis中获取刷新时间
   *
   * @param token Token信息
   * @return 刷新时间,字符串
   */
  public static String getRefreshTimeByToken(String token) {
    if (StringUtils.isNotEmpty(token)) {
      // 去除JWT前缀
      token = token.substring(JWTConfig.tokenPrefix.length());
      return RedisUtils.hget(token, "refreshTime").toString();
    }
    return null;
  }

  /**
   * 从Redis中获取用户名
   *
   * @param token Token信息
   * @return
   */
  public static String getUserNameByToken(String token) {
    if (StringUtils.isNotEmpty(token)) {
      // 去除JWT前缀
      token = token.substring(JWTConfig.tokenPrefix.length());
      return RedisUtils.hget(token, "username").toString();
    }
    return null;
  }

  /**
   * 从Redis中获取IP
   *
   * @param token Token信息
   * @return
   */
  public static String getIpByToken(String token) {
    if (StringUtils.isNotEmpty(token)) {
      // 去除JWT前缀
      token = token.substring(JWTConfig.tokenPrefix.length());
      return RedisUtils.hget(token, "ip").toString();
    }
    return null;
  }

}