JWT其实是一套用来登录的经典方式了,写这篇文章之前,我也看了网上的文章,其实有很多人都写过相关教程,并且也都有与各种登录方式进行对比,基本上看个两篇三篇左右,就可以懂了。但是好像没看到有完整的前后端代码,这里写一个前后端完整实现,记录一下。

关键词:JWT的前后端方式,guava,前端http拦截器,服务端拦截器,自定义注解@interface

1、JWT介绍

jwt其实就是当用户与服务器通信时,客户在请求中下发token,服务器仅依赖于这个token对象来标识用户。 为了防止用户篡改数据,服务器将在生成对象时添加签名。 服务器不保存任何会话数据,即服务器变为无状态,使其更容易扩展。

2、JWT的数据结构

A - header 头信息
B - payload (有效荷载,用于记录用户非隐私数据)
C - Signature 签名

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ7XCJ1c2VyTmFtZVwiOlwicXp3XCIsXCJ1c2VySWRcIjpcImZlNmE0OGQ4ODVjMDQ3MGE4YjZlZTc2M2YwM2NlNTZjXCJ9IiwiZXhwIjoxNjcwMTU5NDY1fQ.TrbRfNIr4Z1lCMRUedFsPazX1TsnrIFpRJ21Hsh1yuA

3、实现流程图

springboot 中使用 javaagent springboot+jwt_springboot

4、项目实战

4.1、先引入jwt包

<dependency>
  <groupId>com.auth0</groupId>
  <artifactId>java-jwt</artifactId>
  <version>3.10.3</version>
</dependency>
<dependency>
  <groupId>com.google.guava</groupId>
  <artifactId>guava</artifactId>
  <version>24.0-jre</version>
</dependency>

4.2、前端使用vue开发,并且http使用Axios

对于前端来说,在登录的时候,前端需要提交用户名和密码,服务端需要生成token,并且下发。前端请求的时候,需要监听request,并且塞入token,初次之外,前端还需要监听服务端传来的返回码,并且与后端约定好特定的服务码进行token过期跳转。

//监听请求参数,只要有token,就把token放入请求头
axios.interceptors.request.use(
   (config) => {
    // Do something before request is sent
    var accessToken = localStorage.getItem('token');
    if (accessToken) {
      // 添加headers
      config.headers.common['token'] = accessToken // token
    }
    return config
  },
  (error) => {
    // Do something with request error
    return Promise.reject(error)
  }
);
//监听返回参数,如果服务端返回3200,则表示token已过期,并且跳转登录页
axios.interceptors.response.use(
  (response) => {
    if(response.data.respCode === "3200") {
      window.location.href = "/login";
    }
    return response.data;
  }
);

前端登录接口,也可以设置过期时间,但是我这里没有去做,严格来讲,是需要的呦!!! 

login() {
     this.$http.post("/sys/userInfo/login", this.$qs.stringify({
        。。。
         })).then((rs) => {
            if (rs.respCode === '1000') {
            // 关闭tab页,数据消失
            sessionStorage.setItem("token", rs.token);
            localStorage.setItem("userId", rs.userId);
            //把用户名存储在localstorege
            localStorage.setItem("token", rs.token);

            // 关闭浏览器,数据消失
            // document.cookie = "token=" + JSON.stringify(rs.token);
            // 这里需要登录跳转
            } 
       }).catch((rs) => {
             
    })
}

4.3、对于服务端,这里使用的是guava缓存,理论上用redis会更好,但是我这是针对与单体项目,使用guava就不需要再起一个redis服务了,而且guava有两种过期方式:写过期和访问过期。

这里需要使用的是访问过期,即规定时间内无访问,则过期。

@Component
public class GuavaCacheConfig {
    // expireAfterAccess(long, TimeUnit)
    // 缓存在给定的时间内没有被访问则被回收
    // expireAfterWrite(long, TimeUnit)
    // 缓存没有在给定的时间内被写访问(创建或者覆盖)则被回收。
    private static final Cache<String, Object> CACHE = CacheBuilder.newBuilder()
            .expireAfterAccess(1800, TimeUnit.SECONDS).maximumSize(1800).build();

    public Object get(String key) {
        return CACHE.getIfPresent(key);
    }

    public void set(String key, Object value) {
        CACHE.put(key, value);
    }
}

对于token过期校验属于通用模块,可以使用切面或者拦截器来进行处理,这样解耦相当一部分代码,如果需要修改,也不用每个接口都修改了。

这里可以需要定义一个自定义注解

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface ValidateToken {
    boolean required() default true;
}

然后针对需要登录校验的接口,只需要加上注解,就会进入token校验,例如下面查询定时任务的接口

@PostMapping("/list")
@ValidateToken
public String list(@Validated(value = QuartzForm.ListQuartz.class)QuartzForm form) {
   return JsonUtils.objectToJsonString(quartzService.list(form));
}

对于校验逻辑,需要先设置一个拦截器,并且配置好拦截方式,就可以实现拦截了

@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(tokenHandler())
                .addPathPatterns("/**");
    }
    @Bean
    public TokenInterceptor tokenHandler() {
        return new TokenInterceptor();
    }
}
public class TokenInterceptor implements HandlerInterceptor {


    @Autowired
    private GuavaCacheConfig guavaCacheConfig;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
                             Object handler) throws Exception {
        // 从 http 请求头中取出 token
        String token = request.getHeader("token");
        // 如果不是映射到方法直接通过
        if (!(handler instanceof HandlerMethod)) {
            return true;
        }
        HandlerMethod handlerMethod = (HandlerMethod) handler;
        Method method = handlerMethod.getMethod();
        //检查是否有 validateToken 注释,有则跳过认证
        if (method.isAnnotationPresent(ValidateToken.class)) {
            ValidateToken passToken = method.getAnnotation(ValidateToken.class);
            if (passToken.required()) {
                if (token == null){
                    throw new RespException(ResponseEnum.ERR_HANDLER_TOKEN_EXPAIRE);
                }
                JsonObject tokenJson = JsonUtils.stringToJsonObject(token);
                String userId = JsonUtils.getAsString(tokenJson, Params.USER_ID);
                this.validateUserInfo(userId);
                。。。
                return true;
            }
        }
        return true;
    }

    private void validateUserInfo(String userId) {
        String token = (String) guavaCacheConfig.get(Utils.getTokenKey(userId));
        if (token == null) {
            throw new RespException(ResponseEnum.ERR_HANDLER_TOKEN_ERROR);
        }
    }
}

就这样就可以实现过期后自动跳转登录页面了

springboot 中使用 javaagent springboot+jwt_服务端_02