前言
用户鉴权一直是我先前的一个问题,以前我用户接口鉴权是通过传入参数进行鉴权,只要是验证用户的地方就写token验证,虽然后面也把token验证方法提取到基类中,但是整体来说仍然不是太雅观,当时的接口如下所示.
@RequestMapping(value = "like",method = RequestMethod.POST)
public ResultMap userLikeOrDisLikeAction(@RequestParam(value = "shopId") String shopId,
@RequestParam(value = "userId") String userId,
@RequestParam(value = "islike") int islike,
@RequestParam(value = "token") String token,
@RequestParam(value = "timestamp") String timestamp
)
{
ResultMap map = new ResultMap();
if (!verifyTokenString(token,timestamp)){
map.code = Constants.ERROR_CODE_TOKEN_NOT_EQUAL;
map.msg = "token错误";
return map;
}
....
}
反正一句话来说,自己太菜了...
其实很久之前,就有了相应的解决方案,那就是利用AOP在拦截器中统一处理token校验的问题,那我们一起看看SpringBoot中如何使用JWT来做Token校验和单点登录的.
JWT集成
项目是基于Maven来架构的,所以我们先导入JWT的依赖.整体如下所示.
<!-- JWT的用户token相关 -->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.10.3</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.7.0</version>
</dependency>
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>2.3.1</version>
</dependency>
<!-- JWT的用户token相关 -->
对于需要创建的类来说,主要有以下几个类.
下面我们简单看一下各个文件的作用.
InterceptorConfig : Spring boot2.0 官方推荐实现 WebMvcConfigurer 接口配置拦截器.
JwtConfig : token的相关方法工具类.
TokenInterceptor : 拦截器
PassToken 、UserLoginToken : 自定义注解,用于标注接口或者类是否需要进行token验证.
具体代码
首先,我们对上面的类或者注解进行一个详细的说明.
InterceptorConfig
该类主要是用来配置拦截器的,具体代码如下所示.
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import javax.annotation.Resource;
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
@Resource
private TokenInterceptor tokenInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(tokenInterceptor)
.addPathPatterns("/**");
}
}
JwtConfig
该类主要是用来定义token的相关方法.例如,创建token,创建刷新token等等,验证token是否过期,获取token中的用户信息等等.
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.apache.juli.logging.Log;
import org.apache.juli.logging.LogFactory;
import org.springframework.stereotype.Component;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
@Component
public class JwtConfig {
private static final Log log = LogFactory.getLog(JwtConfig.class);
private String secret = "秘钥,请自己定义";
// 外部http请求中 header中 token的 键值
private String header = "token";
private static Map<String, String> tokenMap = new HashMap<>();
/**
* 生成token
*
* @param subject
* @return
*/
public String createToken(String subject) {
Date nowDate = new Date();
Calendar calendar = Calendar.getInstance();
calendar.setTime(nowDate);
calendar.add(Calendar.DAY_OF_MONTH, 10);
Date expireDate = calendar.getTime();
String userToken = Jwts.builder()
.setHeaderParam("typ", "JWT")
.setSubject(subject)
.setIssuedAt(nowDate)
.setExpiration(expireDate)
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
// 把token添加到缓存中
tokenMap.put(subject, userToken);
return userToken;
}
public String createRefreshToken(String subject) {
Date nowDate = new Date();
return Jwts.builder()
.setHeaderParam("typ", "JWT")
.setSubject(subject)
.setIssuedAt(nowDate)
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}
/**
* 获取token中注册信息
*
* @param token
* @return
*/
public Claims getTokenClaim(String token) {
try {
return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
} catch (Exception e) {
return null;
}
}
/**
* 验证token是否过期失效
*
* @param expirationTime
* @return
*/
public boolean isTokenExpired(Date expirationTime) {
return expirationTime.before(new Date());
}
/**
* 获取token失效时间
*
* @param token
* @return
*/
public Date getExpirationDateFromToken(String token) {
return getTokenClaim(token).getExpiration();
}
/**
* 获取用户名从token中
*/
public String getUsernameFromToken(String token) {
return getTokenClaim(token).getSubject();
}
/**
* 获取jwt发布时间
*/
public Date getIssuedAtDateFromToken(String token) {
return getTokenClaim(token).getIssuedAt();
}
// --------------------- getter & setter ---------------------
public String getSecret() {
return secret;
}
public void setSecret(String secret) {
this.secret = secret;
}
public String getHeader() {
return header;
}
public void setHeader(String header) {
this.header = header;
}
public Map<String, String> getTokenMap() {
return tokenMap;
}
}
PassToken
定义一个哪些类或者接口跳过验证的注解,不添加也也判定是跳过验证.具体实现代码如下所示.
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface PassToken {
boolean required() default true;
}
UserLoginToken
定义一个哪些类或者接口需要验证的注解,具体实现代码如下所示.
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface UserLoginToken {
boolean required() default true;
}
TokenInterceptor
拦截器,继承于 HandlerInterceptorAdapter 这个抽象类, 实现接口拦截验证功能,具体代码如下所示.
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.SignatureException;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.lang.reflect.Method;
@Component
public class TokenInterceptor extends HandlerInterceptorAdapter {
@Resource
private JwtConfig jwtConfig;
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws SignatureException, IOException {
String uri = request.getRequestURI();
HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod();
/** 检查是否有passtoken注释,有则跳过认证 */
if (method.isAnnotationPresent(PassToken.class)) {
PassToken passToken = method.getAnnotation(PassToken.class);
if (passToken.required()) {
return true;
}
}
/** 检查有没有需要用户权限的注解 */
if (method.isAnnotationPresent(UserLoginToken.class)) {
/** Token 验证 */
String token = request.getHeader(jwtConfig.getHeader());
if (StringUtils.isEmpty(token)) {
token = request.getParameter(jwtConfig.getHeader());
}
if (StringUtils.isEmpty(token)) {
response.sendError(401, "token信息不能为空");
return false;
}
String userName = jwtConfig.getUsernameFromToken(token);
String compareToken = jwtConfig.getTokenMap().get(userName);
if (compareToken != null && !compareToken.equals(token)) {
response.sendError(400, "token已经失效,请重新登录");
return false;
}
UserLoginToken userLoginToken = method.getAnnotation(UserLoginToken.class);
if (userLoginToken.required()) {
Claims claims = null;
try {
claims = jwtConfig.getTokenClaim(token);
if (claims == null || jwtConfig.isTokenExpired(claims.getExpiration())) {
response.sendError(400, "token已经失效,请重新登录");
return false;
}
} catch (Exception e) {
response.sendError(400, "token已经失效,请重新登录");
return false;
}
/** 设置 identityId 用户身份ID */
request.setAttribute("identityId", claims.getSubject());
return true;
}
if (compareToken == null) {
// 由于服务器war重新上传导致临时数据丢失,需要重新存储
jwtConfig.getTokenMap().put(userName, token);
}
}
return true;
}
}
Token验证
Token验证的过程主要是在拦截器中,用户在登录过程中,我们需要把生成好的token 、refreshToken(刷新token)、expirationDate(过期时间)发送给用户.然后再需要的接口的header中传入token信息用于验证.
验证过程主要是在 preHandle 方法中实现的.
首先我们验证是否含有 @PassToken 这个注解,如果有,那么直接跳过验证.
if (method.isAnnotationPresent(PassToken.class)) {
PassToken passToken = method.getAnnotation(PassToken.class);
if (passToken.required()) {
return true;
}
}
然后只有含有 @UserLoginToken 的接口中才去验证token.验证Token主要是验证它的过期时间.代码如下所示.
if (userLoginToken.required()) {
Claims claims = null;
try {
claims = jwtConfig.getTokenClaim(token);
if (claims == null || jwtConfig.isTokenExpired(claims.getExpiration())) {
response.sendError(400, "token已经失效,请重新登录");
return false;
}
} catch (Exception e) {
response.sendError(400, "token已经失效,请重新登录");
return false;
}
/** 设置 identityId 用户身份ID */
request.setAttribute("identityId", claims.getSubject());
return true;
}
单点登录
如何简单实现一个单点登录呢?我们需要维护一个全局的HaspMap,以 Token中的 subject (这里我使用的不会重复的username) 作为键值,以token为value存储. Map定义在 JwtConfig 中,代码如下所示.
private static Map<String, String> tokenMap = new HashMap<>();
在创建token的方法中,我们认定前面的token都失效了,所以我们直接添加即可,如果存在旧的token就进行覆盖操作,如果没有就进行添加.代码如下所示.
public String createToken(String subject) {
....
String userToken = ....
tokenMap.put(subject, userToken);
....
}
在拦截器中的拦截方法中我们需要去验证 传入的token是否是我们存储中的token,如果不是,那么就直接返回token过期.
String userName = jwtConfig.getUsernameFromToken(token);
String compareToken = jwtConfig.getTokenMap().get(userName);
if (compareToken != null && !compareToken.equals(token)) {
response.sendError(400, "token已经失效,请重新登录");
return false;
}
由于HashMap存储在缓存中,当下次服务重启的时候,HashMap所有值就会失效.这时候我们该如何做呢?我们需要在拦截方法最后把当前验证完毕的token 重新填入 Map中即可.
if (compareToken == null) {
// 由于服务器war重新上传导致临时数据丢失,需要重新存储
jwtConfig.getTokenMap().put(userName, token);
}
刷新token
当token过期之后,我们允许用户进行token的刷新.这时候我们需要定义一个生成刷新token的方法,如下所示.
public String createRefreshToken(String subject) {
Date nowDate = new Date();
return Jwts.builder()
.setHeaderParam("typ", "JWT")
.setSubject(subject)
.setIssuedAt(nowDate)
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}
我们已经在登录之时把该refreshToken 返回给用户,只要我们定义接口实现新token的创建即可.这样就完成token的刷新了.
结语
基于JWT的token校验、单点登录、刷新token整体来说还是比较简单的,如果有问题,欢迎各位大佬在评论区指导批评,谢谢啦~OK,今天就到这里了.....