token由3部分组成:Header,Payload,Signature。
其中Header记录了签名算法和token 的类型。
Payload是以明文存储的一些信息,包括用户自定义信息。
Signature是使用签名算法,对Payload结合服务端才知道的私钥进行签名后得出的结果。
服务端对这3部分使用base64编码,然后以.号分隔,就得到了token字符串,格式为:
xxxxxx.yyyyyy.zzzzzz
每次前端进行请求时,带上这个token字符串。当服务端收到请求后,就会对Payload计算签名,然后与token中的Signature进行比较,若一致,则通过。
逻辑简单,完全可以自己写一个token类来实现以上功能。
Spring Boot中提供了一个第三方的token库,叫jwt。
- 在pom.xml中引入依赖:
<!-- JWT Json Web Token 依赖 -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.7.0</version>
</dependency>
- 生成token:
public String getAccessToken() {
JwtBuilder jwtBuilder = Jwts.builder();
long nowMillis = System.currentTimeMillis();
// 7个官方Payload字段
jwtBuilder.setId("1.0"); // 编号/版本
jwtBuilder.setIssuer("ISSUER"); // 发行人
jwtBuilder.setSubject("SUBJECT"); // 主题
jwtBuilder.setAudience("AUDIENCE"); // 受众
jwtBuilder.setIssuedAt(new Date(nowMillis)); // 签发时间
jwtBuilder.setNotBefore(new Date(nowMillis)); // 生效时间
jwtBuilder.setExpiration(new Date(nowMillis + (60 * 60 * 1000))); // 失效时间
// 用户自定义字段
jwtBuilder.claim("id", "AXDBDCD");
jwtBuilder.claim("name", "testname");
jwtBuilder.claim("value", "123456");
// 定义私钥
String HS256KEY = "xxxxxx";
// 签名算法
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
// 计算签名key值
Key signingKey = new SecretKeySpec(Base64.decodeBase64(HS256KEY), signatureAlgorithm.getJcaName());
// 进行签名
jwtBuilder.signWith(signatureAlgorithm, signingKey);
// 获取token字符串
String tokenString = jwtBuilder.compact();
return tokenString;
}
注意:
- 要先为jwtBuilder设置claims,然后再调用jwtBuilder的set接口来设置7个官方payload字段。这是因为jwtBuilder是将7个payload字段存储在claims中的。若先设置了payload,再设置claims,会将已设置的payload覆盖掉。
- payload字段中:id通常设为版本号;issuer是发行人,通常设为公司名;subject是主题,通常设为工程名;audience是受众,通常设为获取token 的接口名。一般来说,audience多被设为” login”,也就是登录。
- 7个官方payload字段中的3个时间:签发时间(issued at),生效时间(not before),失效时间(expiration)。这3个时间使用的单位是秒。调用jwtBuilder的set接口来设置这3个时间时,只需要传入Date()型数据即可,JwbBuilder会自动将其转换为秒存储在claims的相应字段里。详见附录。
- 解析token:
public boolean isTokenValid(String token) {
try {
Claims claims = Jwts.parser().setSigningKey(signingKey).parseClaimsJws(token.trim()).getBody();
Date date = claims.getExpiration();
return new Date().before(date);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
将token字符串解析为Jws对象,然后获取claims,这样就能拿到claims中存储的各个字段。然后自行判断即可。其中解析用的signingKey与2中生成的signingKey是同一个。
由于(当前解析的时间<生效时间)或(当前解析的时间>失效时间)均会抛出异常,所以需要对异常进行捕获,返回false给前端,表示token已失效。
附录
- claims的设置
JwtBuilder允许用户自定义claims,并提供了3个相关函数:
JwtBuilder setClaims(Claims var1);
JwtBuilder setClaims(Map<String, Object> var1);
JwtBuilder claim(String var1, Object var2);
其中:
- 两个setClaims会将之前已存在的claims覆盖掉。
- claim会将新的值push给当前claims。
JwtBuilder将7个官方payload字段也存储在claims中。也就是说,用户自定义的字段和官方的payload字段是存放在一起的。
因此,若已经为JwtBuilder设置了claims字段,则不能再调用setClaims(),否则已设置的会被覆盖。推荐使用claim()函数。
JwtBuilder提供了7个set函数来设置7个官方payload字段。以setExpiration()为例。
·若还没有调用JwtBuilder的setClaims(),则调用setExpiration(),会先为JwtBuilder添加一个空的claims,然后将expiration设置进去。
·若已经调用了JwtBuilder的setClaims(),则调用setExpiration(),会将expiration的值push到已存在的claims中。
- not before与expiration
7个官方payload字段中,有3个时间:
·issue at:签发时间
·not before:生效时间
·expiration:失效时间
当Jwts对token字符串进行解析时,若存在not before和expiration,则会进行判断:
当前解析的时间必须>=not before
当前解析的时间必须<=expiration
否则会抛出异常。所以解析时需要对异常进行捕获处理。
若这2个时间都没有进行设置,则不会进行判断。
然而,其流程是:
JwtBuilder调用setNotBefore()来设置时间→生成token返回给用户→用户请求→对请求的token进行解析。
其中:
- setNotBefore()来设置时间时,将时间转换为秒,是一个long型的数据,存储到claims中。
- 解析时,将claims中的秒取出来,转成Date型数据,然后与当前时间的Date比较,来判断是否抛出异常。
因此:
jwtBuilder.setNotBefore(new Date());
与
jwtBuilder.claim("nbf", new Date().getTime()/1000);
效果是相同的。
其中7个字段名(例如"nbf")定义在Claims接口中(例如Claims.NOT_BEFORE)。