java 将token解析为用户信息 java登录token_java不同项目加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;
        }        ....    }


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


java 将token解析为用户信息 java登录token_java不同项目加token访问_02


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


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


java 将token解析为用户信息 java登录token_java不同项目加token访问_03


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

InterceptorConfig : Spring boot2.0 官方推荐实现 WebMvcConfigurer 接口配置拦截器.JwtConfig : token的相关方法工具类.TokenInterceptor : 拦截器PassTokenUserLoginToken

具体代码


首先,我们对上面的类或者注解进行一个详细的说明.

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


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,今天就到这里了.....