SpringBoot:基于JWT的token校验、单点登录等
前言
用户鉴权一直是我先前的一个问题,以前我用户接口鉴权是通过传入参数进行鉴权,只要是验证用户的地方就写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;@Configurationpublic 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;@Componentpublic 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;@Componentpublic 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,今天就到这里了.....