Json Web Tokn 配合拦截器 / 自定义注解 实现鉴定权限

概述

JWT 主要是一种验证用户身份的方式.相对于 session 而言,服务器不需要再进行保存一个 session 的会话信息. 而通过根据自定义的加密方式去加密用户的相关数据当做一个令牌(token).

只看它的名字便知道,数据采用的是 json 格式. 一般的,分为三个部分:

  • Header 头部(标题包含了令牌的元数据,并且包含签名和/或加密算法的类型)
  • Payload 负载 (我们所有的信息都存放在这里)
  • Signature 签名/签证

简单的说,我们通过用户所提供的信息去生成一个令牌,而这个令牌是基于我们的私钥进行去创建的.用户如果拿不到我们的秘钥,只要修改其中一个字符,令牌即失效.并且 token 中会去保存令牌失效时间.如果时间到了令牌也会失效.

引入

    <dependency>
        <groupId>com.auth0</groupId>
        <artifactId>java-jwt</artifactId>
        <version>3.5.0</version>
    </dependency>

JWT 工具类

public class JwtUtil {

/**
 * 过期时间为一天
 * TODO 正式上线更换为15分钟
 */
private static final long EXPIRE_TIME = 24 * 60 * 60 * 1000;

/**
 * token私钥
 */
private static final String TOKEN_SECRET = "2333";

/**
 * 生成签名,15分钟后过期
 *
 * @param userName
 * @param userLevel
 * @return
 */
public static String sign(String userName, String userLevel) {
    //过期时间
    Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);
    //私钥及加密算法
    Algorithm algorithm = Algorithm.HMAC256(TOKEN_SECRET);
    //设置头信息
    HashMap<String, Object> header = new HashMap<>(2);
    header.put("typ", "JWT");
    header.put("alg", "HS256");
    return JWT.create().withHeader(header).withClaim("userName", userName)
            .withClaim("userLevel", userLevel).withExpiresAt(date).sign(algorithm);
}
//验证 token 是否有效
public static int verity(String token) {
    try {
        Algorithm algorithm = Algorithm.HMAC256(TOKEN_SECRET);
        JWTVerifier verifier = JWT.require(algorithm).build();
        DecodedJWT jwt = verifier.verify(token);
        return 0;
    } catch (IllegalArgumentException | JWTVerificationException e) {
        return 1;
    }

}
//获得 token 中的载荷信息
public JSONObject getPayload(String token) {
    try {
        Algorithm algorithm = Algorithm.HMAC256(TOKEN_SECRET);
        JWTVerifier verifier = JWT.require(algorithm).build();
        DecodedJWT jwt = verifier.verify(token);
        String payload = jwt.getPayload();

        return (JSONObject) JSON.parse(Base64.getDecoder().decode(payload));

    } catch (IllegalArgumentException | JWTVerificationException e) {
        return null;
    }
}

}

sign 方法中可以看到,我们通过withClaim方法直接将我们要写进 token 的信息加载了进去.并且通过我们的私钥进行加密.那么如果用户想要伪造令牌就需要得到这个私钥~ 因此用户即使随便更改一个字符那么所对应的令牌不可能成立.所以 token 是不可伪造的~

因此当用户登录成功之后我们可以生成一个 token 发送给前端,然后每次发起请求的时候,前端在请求的协议头中添加 token,后端在得到请求的时候去证伪就可以了~

证伪

以下是编写一个接口,一般情况下我们是通过HttpServletRquest参数 去获得协议头最后获得 token 进行鉴定.

/*
  鉴定 token 是否伪造.
 */
@GetMapping("/getTokenVerity")
public int getTokenVerity(@RequestParam("token") String token) {

    return JwtUtil.verity(token);

}

拦截器与注解

学会了前面的方法,后期在项目中我们不可能每编写一个接口之后都要去证伪 token 如果这样,那么代码冗余也太高了.所以我们可以直接通过拦截器去拦截方法,在方法执行前就去判断 token 是否存在或是否过期.

但是我们并不是每一次都需要去判断,所以可以通过注解进行声明这个方法需要去验证.

注解

@Target(ElementType.METHOD)  //只作用于方法之上
@Retention(RetentionPolicy.RUNTIME)  //在运行时生效
public @interface TokenVerify {
}

拦截器

我们需要去实现HandlerInterceptor 接口.

public class AuthenticationInterceptor implements HandlerInterceptor {

该接口一共有三个方法,分别是:
1.boolean preHandle ()
主要含义为,要执行那个方法的时候可以通过这个方法做一些预先处理.拥有 HttpServletRequest HttpServletResponse ,以及涵盖对应类对象的参数.
当返回 true 是通过该处理器,反之是停止该请求.
2.void postHandle()
即将返回视图层的时候进行调用,该方法在preHandler 方法的基础上拥有modeAndView 参数.
3.void afterCompletion()
当视图解析完毕后执行,主要做请求完毕的回调方法.

看完以上三个方法之后,思路很明显.我们需要在 preHandle 加入判断方法是否存在 TokenVerify 注解.如果存在那么就通过 HttpServletRequest 方法去获得协议头中的 token ,然后再去判断是否存在.
如果不存在,我们就通过HttpServletResponse去设置返回.

preHandler

首先我们需要编写一下代码:

    if(!(handler instanceof HandlerMethod)){
        return true;
    }

instanceof 的意思是,判断 Handler 参数是否属于 handlerMethod类. 主要原因是,spring 在初始化阶段会将所有的 controller 方法进行组建映射,而如果是控制器方法必然会使用 handlerMethod 类进行包装.如果当前拦截到的目标不属于这个类,那也没有拦截的必要了,所以直接返回 true 进行放行.

    String token = request.getHeader("token");
    HandlerMethod handlerMethod = (HandlerMethod) handler;
    Method method = handlerMethod.getMethod();
    if (method.isAnnotationPresent(TokenVerify.class)){
        if (token == null || JwtUtil.verity(token) == 1){
            response.setHeader("Content-type", "text/html;charset=UTF-8");
            response.setCharacterEncoding("UTF-8");
            response.getWriter().append(new Msg(1,"身份无效或已过期!",null).toJson());
            return false;

        }

    }

接下来通过 handlerMethod 去获得我们的方法对象,通过isAnnotationPresent去判断该方法是否加载了 TokenVerify 注解.如果拥有便从协议头中去获得 token 并对其证伪.

创建拦截器配置类

当我们写完拦截器之后,需要再编写一个配置类使其启动.


@Configuration
public class InterceptorConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(authenticationInterceptor())
                .addPathPatterns("/**");
    }
    @Bean
    public AuthenticationInterceptor authenticationInterceptor() {
        return new AuthenticationInterceptor();
    }

}