SpringBoot:基于JWT的token校验、单点登录等

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;        }        ....    }

反正一句话来说,自己太菜了...

SpringBoot:基于JWT的token校验、单点登录等

其实很久之前,就有了相应的解决方案,那就是利用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相关 -->

对于需要创建的类来说,主要有以下几个类.

SpringBoot:基于JWT的token校验、单点登录等

下面我们简单看一下各个文件的作用.

InterceptorConfig : Spring boot2.0 官方推荐实现 WebMvcConfigurer 接口配置拦截器.
JwtConfig : token的相关方法工具类.
TokenInterceptor : 拦截器
PassTokenUserLoginToken : 自定义注解,用于标注接口或者类是否需要进行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,今天就到这里了.....

您可能还会对下面的文章感兴趣: