双token验证登陆过期
JWT
不了解token或者JWT的可以先看这里JWT 基础概念详解 | JavaGuide,了解后可以直接跳过这一部分
简单来说,Token是用来鉴别用户身份的令牌,JWT是JSON Web Token一种规范化之后的 JSON 结构的 Token。
JWT 本质上就是一组字串,通过(.
)切分成三个为 Base64 编码的部分:
JWT通常长这样子:
示例:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
JWT在加密之前是json格式的,加密后才变得如此抽象,用.
分割的三部分在加密前都是JSON格式的数据,分别如下:
-
Header : 描述 JWT 的元数据,定义了生成签名的算法以及
Token
的类型 - Payload : 用来存放实际需要传递的数据。其中我认为最主要的两个字段:sub字段存储想要保存的信息,exp字段保存该JWT的过期时间
- Signature(签名):服务器通过 Payload、Header 和一个密钥(Secret)使用 Header 里面指定的签名算法(默认是 HMAC SHA256)生成。
而服务器端通过加密算法生成JWT,并将诸如用户ID,用户权限等信息放入Payload的sub字段,前端调用后端接口时把token作为参数(一般以http头的形式)传给后端,后端解析后得知调用这个参数的用户是哪一个,由此做出相应的响应。
为什么要使用双token
由于token的过期时间在token生成后就难以更改,所有token的过期时长不宜设置过长:
- 当用户信息发生更改时,token中的信息无法及时更改
- 增加token中信息泄露的风险
然而token过期时间短的话,用户需要不停登陆,体验较差
解决方案:使用双token验证登陆过期:
- 一个accessToken,过期时间较短,储存用户信息权限等
- 一个refreshToken,过期时间较长,不储存额外信息,只储存用户id
双token运作流程
- 前端请求登录/注册接口后后端会返回accessToken和refreshToken
- 请求需要登录的接口时,在请求头携带accessToken,key为“Authorization”,value为accessToken的值
- 后端首先验证accessToken是否过期,没过期就正常处理请求、返回结果;过期就返回errCode=409,errDesc="用户未登录"的结果
- 前端如果接收到了上述过期的结果,需要不携带任何请求头请求/api/user/refreshToken接口,传入参数refreshToken=“xxxx”。
- 如果refreshToken没过期,后端会返回新的accessToken和refreshToken,此时前端可以用拿到的新的accessToken重新请求登陆接口
- 如果refreshToken也过期了,/api/user/refreshToken接口同样会返回errCode=409,errDesc="用户未登录"的结果,此时前端需要提示用户长时间未操作,需要重新登陆(这次登陆不携带token,需要用户输入账号密码等方式登陆)
这个流程需要前端配合:
- 登陆接口收到409的响应后,需要请求/api/user/refreshToken接口
- /api/user/refreshToken接口返回新的accessToken后需要重新请求登陆接口
- /api/user/refreshToken接口返回409后需要跳转到登陆界面
后端双token代码:
:
JWT工具类,可以直接照搬使用
package com.neu.deliveryPlatform.util;
/**
* @Author laobuzhang
* @Description: jwt工具类,通过UUID算法生成JWT的id,通过subject参数将要储存的信息放在jwt的sub字段中
*/
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;
import java.util.Date;
import java.util.UUID;
/**
* JWT工具类
*/
public class JwtUtil {
//有效期为
public static final Long JWT_TTL = 60 * 60 * 24 * 1000L;// 一天
//设置秘钥明文
public static final String JWT_KEY = "BuXiWanGongZuoShi";
public static String getUUID() {
String token = UUID.randomUUID().toString().replaceAll("-", "");
return token;
}
/**
* 生成jtw
*
* @param subject token中要存放的数据(json格式)
* @return
*/
public static String createJWT(String subject) {
JwtBuilder builder = getJwtBuilder(subject, null, getUUID());
return builder.compact();
}
/**
* 创建token
*
* @param id
* @param subject
* @param ttlMillis
* @return
*/
public static String createJWT(String id, String subject, Long ttlMillis) {
JwtBuilder builder = getJwtBuilder(subject, ttlMillis, id);// 设置过期时间
return builder.compact();
}
/**
* 生成jtw
*
* @param subject token中要存放的数据(json格式)
* @param ttlMillis token超时时间
* @return
*/
public static String createJWT(String subject, Long ttlMillis) {
JwtBuilder builder = getJwtBuilder(subject, ttlMillis, getUUID());// 设置过期时间
return builder.compact();
}
private static JwtBuilder getJwtBuilder(String subject, Long ttlMillis, String uuid) {
// jwt签名加密算法HS256
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
// 密钥
SecretKey secretKey = generalKey();
long nowMillis = System.currentTimeMillis();
Date now = new Date(nowMillis);
if (ttlMillis == null) {
ttlMillis = JwtUtil.JWT_TTL;
}
long expMillis = nowMillis + ttlMillis;
Date expDate = new Date(expMillis);
return Jwts.builder()
.setId(uuid) // 唯一的ID,通过这个ID可以在redis中找到该JWT对应的用户信息
.setSubject(subject) // 主题 可以是JSON数据
.setIssuer("sg") // 签发者
.setIssuedAt(now) // 签发时间
.signWith(signatureAlgorithm, secretKey) //使用HS256对称加密算法签名, 第二个参数为秘钥
.setExpiration(expDate);
}
/**
* 生成加密后的秘钥 secretKey
*
* @return
*/
public static SecretKey generalKey() {
// 使用base64将密钥编码成二进制
byte[] encodedKey = Base64.getDecoder().decode(JwtUtil.JWT_KEY);
// 使用AES算法加密二进制密钥
SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
return key;
}
/**
* 解析jwt,最主要目的拿到jwt的id
*
* @param jwt
* @return
* @throws Exception
*/
public static Claims parseJWT(String jwt) throws Exception {
SecretKey secretKey = generalKey();
return Jwts.parser()
.setSigningKey(secretKey)
.parseClaimsJws(jwt)
.getBody();
}
}
:
Response,openid等是我的项目中才有的,你的项目需要按照情况更改
public Response login(String userName,String password){
//写一些判断登陆信息是否正确的逻辑
if(成功登陆){
// openid是微信登陆的凭证,你的项目没有就可以不要这个参数
// user是登陆成功后从数据库中查到的用户信息实体类变量
return Response.of(getLoginResponse(openid,user));
}else{
throw new BizException(ErrorCode.LOGIN_ERROR);
}
}
private Map<String, Object> getLoginResponse(String openid, User user) {
Long userId = user.getId();
//查询权限信息
List<String> roles = userMapper.getRoles(userId);
List<String> authorities = userMapper.getAuthorities(userId);
UserPermission userPermission = new UserPermission();
userPermission.setUserId(String.valueOf(userId));
userPermission.setOpenid(openid);
userPermission.setRoles(roles);
userPermission.setAuthorities(authorities);
//存入redis
String key = KeyProperties.TOKEN_PREFIX + userId;
String value = JSON.toJSONString(userPermission);
stringRedisTemplate.opsForValue().set(key, value, configProperties.getAccessTokenExpiration(), TimeUnit.MILLISECONDS);
stringRedisTemplate.opsForValue().set(key, String.valueOf(userId), configProperties.getRefreshTokenExpiration(), TimeUnit.MILLISECONDS);
//根据userId生成jwt
String accessToken = JwtUtil.createJWT(String.valueOf(userId), configProperties.getAccessTokenExpiration());
String refreshToken = JwtUtil.createJWT(String.valueOf(userId), configProperties.getRefreshTokenExpiration());
//封装jwt为token返回
Map<String, Object> resp = new HashMap<>();
resp.put(ACCESS_TOKEN, accessToken);
resp.put(REFRESH_TOKEN, refreshToken);
resp.put(USERNAME, user.getUsername());
resp.put(USER_ID, userId);
return resp;
}
public Response refreshToken(String refreshToken) {
//解析token
String userId = "";
Claims claims = null;
try {
claims = JwtUtil.parseJWT(refreshToken);
} catch (Exception e) {
throw new BizException(ErrorCode.TOKEN_PARSE_ERROR);
}
userId = claims.getSubject();
//从redis获取用户信息
String key = KeyProperties.TOKEN_PREFIX + userId;
String userInfo = stringRedisTemplate.opsForValue().get(key);
//这之前应该加一个refreashToken过期的判断,这里没有写完全
//重新设置redis Key
stringRedisTemplate.delete(key);
stringRedisTemplate.opsForValue()
.set(key, userInfo, configProperties.getRefreshTokenExpiration(), TimeUnit.MILLISECONDS);
//生成新的token
//根据userId生成jwt
String accessToken = JwtUtil.createJWT(String.valueOf(userId), configProperties.getAccessTokenExpiration());
refreshToken = JwtUtil.createJWT(String.valueOf(userId), configProperties.getRefreshTokenExpiration());
//封装jwt为token返回
Map<String, String> resp = new HashMap<>(2);
resp.put(ACCESS_TOKEN, accessToken);
resp.put(REFRESH_TOKEN, refreshToken);
return Response.of(resp);
}