一、JWT介绍

1、JWT简介

Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC 7519).该token被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。

官网:https://jwt.io/introduction/

2、JWT认证和session认证的区别

session认证

http协议是一种无状态的协议,而这就意味着如果用户向我们的应用提供了用户名和密码来进行用户认证,那么下一次请求时,用户还要再一次进行用户认证才行,因为根据http协议,我们并不能知道是哪个用户发送的请求,所以为了让我们的应用能识别是哪个用户发出的,我们只能在服务器存储一份用户登陆的信息,这份登陆信息会在响应时传递给浏览器,告诉其保存为cookie,以便下次请求时发送给我们的应用,这样我们的应用个就能识别请求来自哪个用户了,这就是传统的基于sessino认证。

JWT认证

基于token的鉴权机制类似于http协议也是无状态的,它不需要在服务端去保留用户的认证信息或会话信息。这也就意味着JWT认证机制的应用不需要去考虑用户在哪一台服务器登录了,这就为应用的扩展提供了便利。

3、JWT认证过程

spring boot实现token以及token续期 springboot token认证_ide

  1. 用户使用账号和密码发出post请求;
  2. 服务器使用私钥创建一个jwt;
  3. 服务器返回这个jwt给浏览器;
  4. 浏览器将该jwt串在请求头中像服务器发送请求;
  5. 服务器验证该jwt;
  6. 返回响应的资源给浏览器

4、JWT结构

JWT是由三段信息构成的,将这三段信息文本用.连接一起就构成了JWT字符串。就像这样:xxxxx.yyyyy.zzzzz。JWT包含了三部分:
Header 头部(标题包含了令牌的元数据,并且包含签名和/或加密算法的类型)
Payload 负载 (类似于飞机上承载的物品)
Signature 签名/签证

二、springboot和JWT整合

pom.xml中添加依赖

<dependency>
    <groupId>com.auth0</groupId>
    <artifactId>java-jwt</artifactId>
   <!--注意版本-->
    <version>3.4.0</version>
</dependency>

创建annotation自定义注解包,在包下创建PassTokenUserLoginToken两个注解

//方法上有该注解就放行
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface PassToken {
    boolean required() default true;
}
//表示需要token验证
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface UserLoginToken {
    boolean required() default true;
}

配置拦截器,实现一个拦截器就需要实现HandlerInterceptor接口

HandlerInterceptor接口主要定义了三个方法
1.boolean preHandle ()
预处理回调方法,实现处理器的预处理,第三个参数为响应的处理器,自定义Controller,返回值为true表示继续流程(如调用下一个拦截器或处理器)或者接着执行
postHandle()afterCompletion()false表示流程中断,不会继续调用其他的拦截器或处理器,中断执行。

2.void postHandle()
后处理回调方法,实现处理器的后处理(DispatcherServlet进行视图返回渲染之前进行调用),此时我们可以通过modelAndView(模型和视图对象)对模型数据进行处理或对视图进行处理,modelAndView也可能为null

3.void afterCompletion():
整个请求处理完毕回调方法,该方法也是需要当前对应的InterceptorpreHandle()的返回值为true时才会执行,也就是在DispatcherServlet渲染了对应的视图之后执行。用于进行资源清理。整个请求处理完毕回调方法。如性能监控中我们可以在此记录结束时间并输出消耗时间,还可以进行一些资源清理,类似于try-catch-finally中的finally,但仅调用处理器执行链中

代码主要流程:

1.从 http 请求头中取出 token
2.判断是否映射到方法
3.检查是否有passtoken注释,有则跳过认证
4.检查有没有需要用户登录的注解,有则需要取出并验证
5.认证通过则可以访问,不通过会报相关错误信息

public class MyInterceptor implements HandlerInterceptor {

    private static final Logger logger = LoggerFactory.getLogger(MyInterceptor.class);


    @Autowired
    IUserService userService;

    @Override
    public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse,
                             Object object) throws Exception {
        // 从 http 请求头中取出 token
        String token = httpServletRequest.getHeader("token");
        // 如果不是映射到方法直接通过
        if(!(object instanceof HandlerMethod)){
            return true;
        }
        HandlerMethod handlerMethod=(HandlerMethod)object;
        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)) {
            UserLoginToken userLoginToken = method.getAnnotation(UserLoginToken.class);
            if (userLoginToken.required()) {
                // 执行认证
                if (token == null) {
                    throw new RuntimeException("无token,请重新登录");
                }
                // 获取 token 中的 user id
                String userId;
                try {
                    userId = JWT.decode(token).getAudience().get(0);
                } catch (JWTDecodeException j) {
                    throw new RuntimeException("401");
                }
                //这里根据userId从数据库查找user
                User user = userService.findUserById(userId);
                if (user == null) {
                    throw new RuntimeException("用户不存在,请重新登录");
                }
                // 验证 token
                JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(user.getPwd())).build();
                try {
                    jwtVerifier.verify(token);
                } catch (JWTVerificationException e) {
                    throw new RuntimeException("401");
                }
                return true;
            }
        }
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        // logger.info("执行完方法之后进执行(Controller方法调用之后),但是此时还没进行视图渲染");
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        // logger.info("整个请求都处理完咯,DispatcherServlet也渲染了对应的视图咯,此时我可以做一些清理的工作了");
    }
}

其中我的User类(已简化)

@Data
public class User{

    private Integer userId;

    private String userName;
    
    private String pwd;
}

最后还要配置拦截器,顺便把跨域配置给解决了,这里的WebMvcConfigurer可以扩展其他自定义功能,详情可查看spring官网

@Configuration
public class MyInterceptorConfig implements WebMvcConfigurer {

    static final String[] ORIGINS = new String[] { "GET", "POST", "PUT", "DELETE" };
    /**
     解决跨域问题
     */
    @Override
    public void addCorsMappings(CorsRegistry registry) {
    //新版springboot将.allowedOrigins替换成.allowedOriginPatterns即可
      registry.addMapping("/**").allowedOrigins("*").allowCredentials(true)
      .allowedMethods(ORIGINS).maxAge(3600);
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 实现WebMvcConfigurer不会导致静态资源被拦截
        registry.addInterceptor(new MyInterceptor())
                // 拦截所有url
                .addPathPatterns("/**");
    }
}

创建TokenService

public class TokenService {

    public static  String getToken(User user) {
        Date start = new Date();
        //一小时有效时间
        long currentTime = System.currentTimeMillis() + 60* 60 * 1000;
        Date end = new Date(currentTime);
        String token = "";

        token = JWT.create().withAudience(String.valueOf(user.getUserId())).withIssuedAt(start).withExpiresAt(end).sign(Algorithm.HMAC256(user.getPwd()));
        return token;
    }
}

最终只需要在登录的时候调用TokenService.getToken(user)即可获得token,返回给前端,下次请求其他接口在header带上token即可


参考:

https://github.com/jwtk/jjwt

https://zhuanlan.zhihu.com/p/91420328

https://www.jianshu.com/p/e88d3f8151db