Springboot,Redis
最近在做用户中心,需要向其他服务签发JWT Token,使用Token来获取用户信息,保证用户信息安全可靠,不会被重放攻击。
JWT Token设置有效期,一旦失效用户就要重新登录,这样的体验非常差,需要做到用户在无感知的情况下,解决如何刷新Token的问题。
看了很多文章,大都是通过refresh Token刷新,其实做法上是类似的。
正常Token:Token未过期,且未达到建议更换时间。
濒死Token:Token未过期,已达到建议更换时间。
正常过期Token:Token已过期,但存在于缓存中。
非正常过期Token:Token已过期,不存在于缓存中。
Token过期时间越短越安全,如设置Token过期时间15分钟,建议更换时间设置为Token前5分钟,则Token生命周期如下:
时间 | Token类型 | 说明 |
---|---|---|
0-10分钟 | 正常Token | 正常访问 |
10-15分钟 | 濒死Token | 正常访问,返回新Token,建议使用新Token |
>15分钟 | 过期Token | 需校验是否正常过期。正常过期则能访问,并返回新Token; 非过期Token拒绝访问 |
在缓存中,通过用户标识查询用户当前Token,校验该Token是否为正常Token,如正常则返回;不正常则生成一个正常Token。
当正常Token请求时,返回当前Token。
当濒死Token请求时,获取一个正常Token并返回。
当正常过期Token请求时,获取一个正常Token并返回。
当非正常过期Token请求时,返回错误信息,需重新登录。
Maven依赖
<dependency>
<groupId>io.jsonwebtokengroupId>
<artifactId>jjwtartifactId>
<version>0.7.0version>
dependency>
jwt token工具类
/**
* 获取用户从token中
*/
public String getUserFromToken(String token) {
return getClaimFromToken(token).getSubject();
}
/**
*
* 验证token是否失效
* true:过期 false:没过期
*
*/
public Boolean isTokenExpired(String token) {
try {
final Date expiration = getExpirationDateFromToken(token);
return expiration.before(new Date());
} catch (ExpiredJwtException expiredJwtException) {
return true;
}
}
/**
* 获取可用的token
* 如该用户当前token可用,即返回
* 当前token不可用,则返回一个新token
* @param userId
* @return
*/
public String getGoodToken(String userId){
String token = redisTemplate.opsForValue().get("userJwtToken_"+userId);
boolean flag = this.checkToken(token);
//校验当前token能否使用,不能使用则生成新token
if(flag){
return token;
}else{
String newToken = this.createToken(userId);
//初始化新token
this.initNewToken(userId, newToken);
return newToken;
}
}
/**
* 判断过期token是否合法
* @param token
* @return
*/
public String checkExpireToken(String token){
//判断token是否需要更新
boolean expireFlag = this.checkToken(token);
//false:不建议使用
if(!expireFlag){
String userId = redisTemplate.opsForValue().get(token);
if(ToolUtil.isNotEmpty(userId)){
return userId + "-1";
}
}else{
String userId = this.getUserFromToken(token);
return userId;
}
return "";
}
/**
* 检查当前token是否还能继续使用
* true:可以 false:不建议
* @param token
* @return
*/
public boolean checkToken(String token){
SecretKey secretKey = this.createSecretKey();
try {
// jwt正常情况 则判断失效时间是否大于5分钟
long expireTime = Jwts.parser() //得到DefaultJwtParser
.setSigningKey(secretKey) //设置签名的秘钥
.parseClaimsJws(token.replace("jwt_", ""))
.getBody().getExpiration().getTime();
long diff = expireTime - System.currentTimeMillis();
//如果有效期小于5分钟,则不建议继续使用该token
if (diff < ADVANCE_EXPIRE_TIME) {
return false;
}
} catch (Exception e) {
return false;
}
return true;
}
/**
* 创建新token
* @param userId 用户ID
* @return
*/
public String createToken(String userId){
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256; //指定签名的时候使用的签名算法,也就是header那部分,jjwt已经将这部分内容封装好了。
long nowMillis = System.currentTimeMillis();//生成JWT的时间
Date now = new Date(nowMillis);
// Map claims = new HashMap();//创建payload的私有声明(根据特定的业务需要添加,如果要拿这个做验证,一般是需要和jwt的接收方提前沟通好验证方式的)
SecretKey secretKey = createSecretKey();//生成签名的时候使用的秘钥secret,这个方法本地封装了的,一般可以从本地配置文件中读取,切记这个秘钥不能外露哦。它就是你服务端的私钥,在任何场景都不应该流露出去。一旦客户端得知这个secret, 那就意味着客户端是可以自我签发jwt了。
//下面就是在为payload添加各种标准声明和私有声明了
JwtBuilder builder = Jwts.builder() //这里其实就是new一个JwtBuilder,设置jwt的body
// .setClaims(claims) //如果有私有声明,一定要先设置这个自己创建的私有的声明,这个是给builder的claim赋值,一旦写在标准的声明赋值之后,就是覆盖了那些标准的声明的
.setId(UUID.randomUUID().toString()) //设置jti(JWT ID):是JWT的唯一标识,根据业务需要,这个可以设置为一个不重复的值,主要用来作为一次性token,从而回避重放攻击。
.setIssuedAt(now) //iat: jwt的签发时间
.setSubject(userId + "-" + jwtVersion) //sub(Subject):代表这个JWT的主体,即它的所有人,这个是一个json格式的字符串,可以存放什么userid,roldid之类的,作为什么用户的唯一标志。
.signWith(signatureAlgorithm, secretKey);//设置签名使用的签名算法和签名使用的秘钥
//设置过期时间
if (JWT_EXPIRE_TIME_LONG >= 0) {
long expMillis = nowMillis + JWT_EXPIRE_TIME_LONG;
Date exp = new Date(expMillis);
builder.setExpiration(exp);
}
String newToken = "jwt_" + builder.compact();
return newToken;
}
/**
* 生成新token时,初始化token
* @param userId
* @param newToken
*/
public void initNewToken(String userId, String newToken){
String token = redisTemplate.opsForValue().get("userJwtToken_"+userId);
if(ToolUtil.isNotEmpty(token)){
//老token设置过期时间 5分钟
redisTemplate.opsForValue().set(token, userId, OLD_TOKEN_EXPIRE_TIME, TimeUnit.MINUTES);
}
//新token初始化
redisTemplate.opsForValue().set(newToken, userId);
redisTemplate.opsForValue().set("userJwtToken_"+userId, newToken);
}
/**
* 获取jwt失效时间
*/
public Date getExpirationDateFromToken(String token) {
return getClaimFromToken(token).getExpiration();
}
/**
* 获取jwt的payload部分
*/
public Claims getClaimFromToken(String token) {
SecretKey secretKey = createSecretKey();
return Jwts.parser() //得到DefaultJwtParser
.setSigningKey(secretKey) //设置签名的秘钥
.parseClaimsJws(token.replace("jwt_", ""))
.getBody();
}
// 签名私钥
private SecretKey createSecretKey(){
byte[] encodedKey = Base64.decodeBase64(signKey);//本地的密码解码
SecretKey secretKey = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");// 根据给定的字节数组使用AES加密算法构造一个密钥,使用 encodedKey中的始于且包含 0 到前 leng 个字节这是当然是所有。(后面的文章中马上回推出讲解Java加密和解密的一些算法)
return secretKey;
}
拦截器
//判断过期token是否合法
String userId = jwtTokenUtil.checkExpireToken(token);
try {
if(ToolUtil.isEmpty(userId)) {
userId = jwtTokenUtil.getUserFromToken(token);
}
if (userId != null) {
String[] split = userId.split("-")[1].split(",");
ArrayList<GrantedAuthority> authorities = new ArrayList<>();
for (int i = 0; i < split.length; i++) {
authorities.add(new GrantedAuthorityImpl(split[i]));
}
//判断token是否需要更新,返回当前可用token
String newToken = jwtTokenUtil.getGoodToken(userId.split("-")[0]);
//每次认证把uId塞入UserIdThreadLocal
//这样可以在当前请求线程里面 通过 UidHelper.get()获取用户id
UidHelper.set(userId.split("-")[0]);
response.setHeader("token", newToken);
return new UsernamePasswordAuthenticationToken(userId, null, authorities);
}
logger.error("解析用户ID为空,token:{}",token);
} catch (Exception e) {
logger.error("Token已过期,token:{}",token);
}