一般项目的登录可以用JWT进行登录认证,主要作用是
- 认证授权(比如有没有资格请求某个接口)
- 信息交换(可以存放一些业务信息)
JWT的主要构成有三部分
- 头部 声明类型 和 使用签名的算法(如RSA256 非对称加密算法 HS256 对称加密)
{
"typ": "JWT",
"alg": "RSA256"
}
- 负载 存放一些有效的信息,除官方定义的外,也可以只定义加入字段和内容(结合业务)存放一些不敏感的信息,不要存密码等。
一些信息
iss:该JWT的签发者
sub : 该JWT所面向的用户
aud : 接收该JWT的一方
exp(expires) : 什么时候过期,这里是一个Unix时间戳
iat(issued at): 在什么时候签发的
{
"userId" : "1",
"userName" : "wjj"
}
- 签名
包含三个部分 编码( base64)后的头部(header)和负载(payload)以及一个secret(私钥)
签名过程如下
RSA256(base64UrlEncode(header) + "." +base64UrlEncode(payload),secret)
用的是头部定义的加密算法,secret是私钥,要在服务端保存好,泄露了就可以伪造token(token是在服务端生成的),还可以根据发来的token进行校验发送方。
JWT工作流程
(网上找的)
解密流程
解密的时候,只要客户端带着jwt来发起请求,服务端就直接使用secret进行解密,解签证解出第一部分和第二部分,然后比对第二部分的信息和客户端穿过来的信息是否一致。如果一致验证成功,否则验证失败。第二部分的信息还可以判断有没有过期。
结合官网
结合项目来看jwt认证流程
public class TokenUtil {
//签发者
private static final String ISSUE = "签发者";
/**
* 生成token
* @param userId
* @return
* @throws Exception
*/
public static String generateToken(Long userId) throws Exception {
Algorithm algorithm = Algorithm.RSA256(RSAUtil.getPublicKey(), RSAUtil.getPrivateKey());
Calendar calendar = Calendar.getInstance();
calendar.setTime(new Date());
// 30秒过期
calendar.add(Calendar.MINUTE, 30);
return JWT.create().
// 负载
//用户信息 这里存放的userId
withKeyId(String.valueOf(userId))
//签发者
.withIssuer(ISSUE)
//过期时间
.withExpiresAt(calendar.getTime())
//签名 使用加密算法 用私钥签名
.sign(algorithm);
}
/**
* 解析token
* @param token
* @return
*/
public static Long verifyToken(String token) {
try {
Algorithm algorithm = Algorithm.RSA256(RSAUtil.getPublicKey(), RSAUtil.getPrivateKey());
JWTVerifier verifier = JWT.require(algorithm).build();
DecodedJWT jwt = verifier.verify(token);
// 获取解析token信息
String userId = jwt.getKeyId();
return Long.valueOf(userId);
} catch (TokenExpiredException e) {
throw new ConditionException("555", "token过期");
}catch (Exception e){
throw new ConditionException("非法用户token!");
}
}
/**
* 刷新token
* @param userId
* @return
* @throws Exception
*/
public static String generateRefreshToken(Long userId) throws Exception {
Algorithm algorithm = Algorithm.RSA256(RSAUtil.getPublicKey(), RSAUtil.getPrivateKey());
Calendar calendar = Calendar.getInstance();
calendar.setTime(new Date());
calendar.add(Calendar.DAY_OF_MONTH, 30);
return JWT.create().
//用户信息 这里存放的userId
withKeyId(String.valueOf(userId))
//签发者
.withIssuer(ISSUE)
//过期时间
.withExpiresAt(calendar.getTime())
//使用加密算法
.sign(algorithm);
}
}
登录请求是不用认证token的,登录是用来生成token的
登录中和token相关的代码
TokenUtil tokenUtil = new TokenUtil();
//根据id生成token
return tokenUtil.generateToken(dbUser.getId());
这里的根据id生成token,id是放在负载里面的(这个可以用于信息交换),这里用id生成token,根据不同的业务需求,可以根据不用的信息生成token。
为什么要在token中加入业务信息呢
把信息放在token里面,就不用每次取redis或者数据库中取了,消耗一些解析token的时间,可以减少一些IO操作。比如我们每次请求的时候需要获取userId的信息,我们可以不用从数据库中取,直接从token中获得,这是一种信息的获取。
代码如下
@Component
public class UserSupport {
public Long getCurrentUserId() {
//抓取请求上下文
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
String token = requestAttributes.getRequest().getHeader("token");
Long userId = TokenUtil.verifyToken(token);
if (userId < 0) {
throw new ConditionException("非法用户!");
}
return userId;
}
}
获取请求头中的token,解析出token中的userId,就可以直接使用了。
在请求接口加上
Long userId = userSupport.getCurrentUserId();
除此之外也可以使用不含负载信息的token, 把token信息存到redis里面
比如
key value
userId token
token userId
这样也可以获取信息。
生成token的时候,前端会在local storage中储存token,下一次使用的时候可以直接携带token。
此外还要如何刷新token的问题…
双token机制
refresh token
- 退出登录了(防止使用还在有效期的token)token就不能再用了(与数据库关联解决)
- 无感刷新,不能用着用着就不能用了
工具类
/**
* 刷新token 时间变长了
* @param userId
* @return
* @throws Exception
*/
public static String generateRefreshToken(Long userId) throws Exception {
Algorithm algorithm = Algorithm.RSA256(RSAUtil.getPublicKey(), RSAUtil.getPrivateKey());
Calendar calendar = Calendar.getInstance();
calendar.setTime(new Date());
//7天
calendar.add(Calendar.DAY_OF_MONTH, 7);
return JWT.create().
//用户信息 这里存放的userId
withKeyId(String.valueOf(userId))
//签发者
.withIssuer(ISSUE)
//过期时间
.withExpiresAt(calendar.getTime())
//使用加密算法
.sign(algorithm);
}
生成双token
String phone = user.getPhone() == null ? "" : user.getPhone();
String email = user.getEmail() == null ? "" : user.getEmail();
if(StringUtils.isNullOrEmpty(phone) && StringUtils.isNullOrEmpty(email)){
throw new ConditionException("参数异常!");
}
User dbUser = userDao.getUserByPhoneOrEmail(phone, email);
if (dbUser == null) {
throw new ConditionException("当前用户不存在");
}
String password = user.getPassword();
String rawPassword;
try {
rawPassword = RSAUtil.decrypt(password);
} catch (Exception e) {
throw new ConditionException("密码解密失败");
}
String salt = dbUser.getSalt();
String md5Password = MD5Util.sign(rawPassword, salt, "UTF-8");
if (!md5Password.equals(dbUser.getPassword())){
throw new ConditionException("密码错误");
}
TokenUtil tokenUtil = new TokenUtil();
Long userId = dbUser.getId();
//根据id生成token
//接入token
String accessToken = tokenUtil.generateToken(userId);
//刷新token
String refreshToken = TokenUtil.generateRefreshToken(userId);
//保存refresh token到数据库
userDao.deleteRefreshToken(refreshToken, userId);
userDao.addRefreshToken(refreshToken, userId, new Date());
Map<String, Object> result = new HashMap<>();
result.put("accessToken", accessToken);
result.put("refreshToken", refreshToken);
return result;
退出登录
userDao.deleteRefreshToken(refreshToken, userId);
实现无感登录,生成新的token
/**
* 刷新accessToken生成新的token
* @param request
* @return
*/
@PostMapping("/access-tokens")
public JsonResponse<String> refreshAccessToken(HttpServletRequest request) throws Exception {
String refreshToken = request.getHeader("refreshToken");
String accessToken = userService.refreshAccessToken(refreshToken);
return new JsonResponse<>(accessToken);
}
public String refreshAccessToken(String refreshToken) throws Exception {
RefreshTokenDetail refreshTokenDetail = userDao.getRefreshTokenDetail(refreshToken);
if (refreshTokenDetail == null) {
throw new ConditionException("555", "token过期!");
}
Long userId = refreshTokenDetail.getUserId();
return TokenUtil.generateToken(userId);
}
前端可以在状态码为555的时候调用刷新token接口,生成新token完成无感登录。