• SpringBoot集成JWT实现token验证


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

    JWT官网: https://jwt.io/
    JWT(Java版)的github地址:https://github.com/jwtk/jjwt

    什么是JWT

    Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC 7519).定义了一种简洁的,自包含的方法用于通信双方之间以JSON对象的形式安全的传递信息。因为数字签名的存在,这些信息是可信的,JWT可以使用HMAC算法或者是RSA的公私秘钥对进行签名。

    JWT请求流程

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

    JWT的主要应用场景

    身份认证在这种场景下,一旦用户完成了登陆,在接下来的每个请求中包含JWT,可以用来验证用户身份以及对路由,服务和资源的访问权限进行验证。由于它的开销非常小,可以轻松的在不同域名的系统中传递,所有目前在单点登录(SSO)中比较广泛的使用了该技术。 信息交换在通信的双方之间使用JWT对数据进行编码是一种非常安全的方式,由于它的信息是经过签名的,可以确保发送者发送的信息是没有经过伪造的。

    优点

    1.简洁(Compact): 可以通过URLPOST参数或者在HTTP header发送,因为数据量小,传输速度也很快
    2.自包含(Self-contained):负载中包含了所有用户所需要的信息,避免了多次查询数据库
    3.因为Token是以JSON加密的形式保存在客户端的,所以JWT是跨语言的,原则上任何web形式都支持。
    4.不需要在服务端保存会话信息,特别适用于分布式微服务。

    `

    JWT的结构

    JWT是由三段信息构成的,将这三段信息文本用.连接一起就构成了JWT字符串。
    就像这样:

    1 eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

    JWT包含了三部分:
    Header 头部(标题包含了令牌的元数据,并且包含签名和/或加密算法的类型)
    Payload 负载 (类似于飞机上承载的物品)
    Signature 签名/签证

    Header

    JWT的头部承载两部分信息:token类型和采用的加密算法。

    1 { 
    2   "alg": "HS256",
    3    "typ": "JWT"
    4 } 

    声明类型:这里是jwt
    声明加密的算法:通常直接使用 HMAC SHA256

    加密算法是单向函数散列算法,常见的有MD5、SHA、HAMC。
    MD5(message-digest algorithm 5) (信息-摘要算法)缩写,广泛用于加密和解密技术,常用于文件校验。校验?不管文件多大,经过MD5后都能生成唯一的MD5值
    SHA (Secure Hash Algorithm,安全散列算法),数字签名等密码学应用中重要的工具,安全性高于MD5
    HMAC (Hash Message Authentication Code),散列消息鉴别码,基于密钥的Hash算法的认证协议。用公开函数和密钥产生一个固定长度的值作为认证标识,用这个标识鉴别消息的完整性。常用于接口签名验证

    Payload

    载荷就是存放有效信息的地方。
    有效信息包含三个部分
    1.标准中注册的声明
    2.公共的声明
    3.私有的声明

    标准中注册的声明 (建议但不强制使用) :

    iss: jwt签发者
    sub: 面向的用户(jwt所面向的用户)
    aud: 接收jwt的一方
    exp: 过期时间戳(jwt的过期时间,这个过期时间必须要大于签发时间)
    nbf: 定义在什么时间之前,该jwt都是不可用的.
    iat: jwt的签发时间
    jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。

    公共的声明 :

    公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.但不建议添加敏感信息,因为该部分在客户端可解密.

    私有的声明 :

    私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息。

    Signature

    jwt的第三部分是一个签证信息
    这个部分需要base64加密后的headerbase64加密后的payload使用.连接组成的字符串,然后通过header中声明的加密方式进行加盐secret组合加密,然后就构成了jwt的第三部分。
    密钥secret是保存在服务端的,服务端会根据这个密钥进行生成token和进行验证,所以需要保护好。

    下面来进行SpringBoot和JWT的集成

    引入JWT依赖,由于是基于Java,所以需要的是java-jwt

    1 <dependency>
    2       <groupId>com.auth0</groupId>
    3       <artifactId>java-jwt</artifactId>
    4       <version>3.4.0</version>
    5 </dependency>

    需要自定义两个注解

    用来跳过验证的 PassToken

    1 @Target({ElementType.METHOD, ElementType.TYPE})
    2 @Retention(RetentionPolicy.RUNTIME)
    3 public @interface PassToken {
    4     boolean required() default true;
    5 }

    需要登录才能进行操作的注解 UserLoginToken

    1 @Target({ElementType.METHOD, ElementType.TYPE})
    2 @Retention(RetentionPolicy.RUNTIME)
    3 public @interface UserLoginToken {
    4     boolean required() default true;
    5 }
    @Target:注解的作用目标

    @Target(ElementType.TYPE)——接口、类、枚举、注解
    @Target(ElementType.FIELD)——字段、枚举的常量
    @Target(ElementType.METHOD)——方法
    @Target(ElementType.PARAMETER)——方法参数
    @Target(ElementType.CONSTRUCTOR) ——构造函数
    @Target(ElementType.LOCAL_VARIABLE)——局部变量
    @Target(ElementType.ANNOTATION_TYPE)——注解
    @Target(ElementType.PACKAGE)——包

    @Retention:注解的保留位置

    RetentionPolicy.SOURCE:这种类型的Annotations只在源代码级别保留,编译时就会被忽略,在class字节码文件中不包含。
    RetentionPolicy.CLASS:这种类型的Annotations编译时被保留,默认的保留策略,在class文件中存在,但JVM将会忽略,运行时无法获得。
    RetentionPolicy.RUNTIME:这种类型的Annotations将被JVM保留,所以他们能在运行时被JVM或其他使用反射机制的代码所读取和使用。

    @Document:说明该注解将被包含在javadoc
    @Inherited:说明子类可以继承父类中的该注解

    简单自定义一个实体类User,使用lombok简化实体类的编写

    1 @Data
    2 @AllArgsConstructor
    3 @NoArgsConstructor
    4 public class User {
    5     String Id;
    6     String username;
    7     String password;
    8 }

    需要写token的生成方法

    1 public String getToken(User user) {
    2         String token="";
    3         token= JWT.create().withAudience(user.getId())
    4                 .sign(Algorithm.HMAC256(user.getPassword()));
    5         return token;
    6     }

    Algorithm.HMAC256():使用HS256生成token,密钥则是用户的密码,唯一密钥的话可以保存在服务端。
    withAudience()存入需要保存在token的信息,这里我把用户ID存入token

    接下来需要写一个拦截器去获取token并验证token

     1 public class AuthenticationInterceptor implements HandlerInterceptor {
     2     @Autowired
     3     UserService userService;
     4     @Override
     5     public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object object) throws Exception {
     6         String token = httpServletRequest.getHeader("token");// 从 http 请求头中取出 token
     7         // 如果不是映射到方法直接通过
     8         if(!(object instanceof HandlerMethod)){
     9             return true;
    10         }
    11         HandlerMethod handlerMethod=(HandlerMethod)object;
    12         Method method=handlerMethod.getMethod();
    13         //检查是否有passtoken注释,有则跳过认证
    14         if (method.isAnnotationPresent(PassToken.class)) {
    15             PassToken passToken = method.getAnnotation(PassToken.class);
    16             if (passToken.required()) {
    17                 return true;
    18             }
    19         }
    20         //检查有没有需要用户权限的注解
    21         if (method.isAnnotationPresent(UserLoginToken.class)) {
    22             UserLoginToken userLoginToken = method.getAnnotation(UserLoginToken.class);
    23             if (userLoginToken.required()) {
    24                 // 执行认证
    25                 if (token == null) {
    26                     throw new RuntimeException("无token,请重新登录");
    27                 }
    28                 // 获取 token 中的 user id
    29                 String userId;
    30                 try {
    31                     userId = JWT.decode(token).getAudience().get(0);
    32                 } catch (JWTDecodeException j) {
    33                     throw new RuntimeException("401");
    34                 }
    35                 User user = userService.findUserById(userId);
    36                 if (user == null) {
    37                     throw new RuntimeException("用户不存在,请重新登录");
    38                 }
    39                 // 验证 token
    40                 JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(user.getPassword())).build();
    41                 try {
    42                     jwtVerifier.verify(token);
    43                 } catch (JWTVerificationException e) {
    44                     throw new RuntimeException("401");
    45                 }
    46                 return true;
    47             }
    48         }
    49         return true;
    50     }
    51 
    52     @Override
    53     public void postHandle(HttpServletRequest httpServletRequest, 
    54                                   HttpServletResponse httpServletResponse, 
    55                             Object o, ModelAndView modelAndView) throws Exception {
    56 
    57     }
    58     @Override
    59     public void afterCompletion(HttpServletRequest httpServletRequest, 
    60                                           HttpServletResponse httpServletResponse, 
    61                                           Object o, Exception e) throws Exception {
    62     }

    实现一个拦截器就需要实现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.认证通过则可以访问,不通过会报相关错误信息

    配置拦截器

    在配置类上添加了注解@Configuration,标明了该类是一个配置类并且会将该类作为一个SpringBean添加到IOC容器内

     1 @Configuration
     2 public class InterceptorConfig extends WebMvcConfigurerAdapter {
     3     @Override
     4     public void addInterceptors(InterceptorRegistry registry) {
     5         registry.addInterceptor(authenticationInterceptor())
     6                 .addPathPatterns("/**");    // 拦截所有请求,通过判断是否有 @LoginRequired 注解 决定是否需要登录
     7     }
     8     @Bean
     9     public AuthenticationInterceptor authenticationInterceptor() {
    10         return new AuthenticationInterceptor();
    11     }
    12 }

    WebMvcConfigurerAdapter该抽象类其实里面没有任何的方法实现,只是空实现了接口
    WebMvcConfigurer内的全部方法,并没有给出任何的业务逻辑处理,这一点设计恰到好处的让我们不必去实现那些我们不用的方法,都交由WebMvcConfigurerAdapter抽象类空实现,如果我们需要针对具体的某一个方法做出逻辑处理,仅仅需要在
    WebMvcConfigurerAdapter子类中@Override对应方法就可以了。

    注:
    SpringBoot2.0Spring 5.0WebMvcConfigurerAdapter已被废弃
    网上有说改为继承WebMvcConfigurationSupport,不过试了下,还是过期的

    解决方法:

    直接实现WebMvcConfigurer (官方推荐)

     1 @Configuration
     2 public class InterceptorConfig implements WebMvcConfigurer {
     3     @Override
     4     public void addInterceptors(InterceptorRegistry registry) {
     5         registry.addInterceptor(authenticationInterceptor())
     6                 .addPathPatterns("/**");   
     7     }
     8     @Bean
     9     public AuthenticationInterceptor authenticationInterceptor() {
    10         return new AuthenticationInterceptor();
    11     }
    12 }

    InterceptorRegistry内的addInterceptor需要一个实现HandlerInterceptor接口的拦截器实例,addPathPatterns方法用于设置拦截器的过滤路径规则。
    这里我拦截所有请求,通过判断是否有@LoginRequired注解 决定是否需要登录

    在数据访问接口中加入登录操作注解

     1 @RestController
     2 @RequestMapping("api")
     3 public class UserApi {
     4     @Autowired
     5     UserService userService;
     6     @Autowired
     7     TokenService tokenService;
     8     //登录
     9     @PostMapping("/login")
    10     public Object login(@RequestBody User user){
    11         JSONObject jsonObject=new JSONObject();
    12         User userForBase=userService.findByUsername(user);
    13         if(userForBase==null){
    14             jsonObject.put("message","登录失败,用户不存在");
    15             return jsonObject;
    16         }else {
    17             if (!userForBase.getPassword().equals(user.getPassword())){
    18                 jsonObject.put("message","登录失败,密码错误");
    19                 return jsonObject;
    20             }else {
    21                 String token = tokenService.getToken(userForBase);
    22                 jsonObject.put("token", token);
    23                 jsonObject.put("user", userForBase);
    24                 return jsonObject;
    25             }
    26         }
    27     }
    28     @UserLoginToken
    29     @GetMapping("/getMessage")
    30     public String getMessage(){
    31         return "你已通过验证";
    32     }
    33 }

    不加注解的话默认不验证,登录接口一般是不验证的。在getMessage()中我加上了登录注解,说明该接口必须登录获取token后,在请求头中加上token并通过验证才可以访问

    下面进行测试,启动项目,使用postman测试接口

    在没token的情况下访问api/getMessage接口

     
                  image.png

    我这里使用了统一异常处理,所以只看到错误message

    下面进行登录,从而获取token

     
    image.png

    登录操作我没加验证注解,所以可以直接访问

    token加在请求头中,再次访问api/getMessage接口

     
    image.png

    注意:这里的key一定不能错,因为在拦截器中是取关键字token的值
    String token = httpServletRequest.getHeader("token");
    加上token之后就可以顺利通过验证和进行接口访问了

    github项目源码地址:https://github.com/JinBinPeng/springboot-jwt









  • 相关阅读:
    Nginx负载均衡+代理+ssl+压力测试
    Nginx配置文件详解
    HDU ACM 1690 Bus System (SPFA)
    HDU ACM 1224 Free DIY Tour (SPFA)
    HDU ACM 1869 六度分离(Floyd)
    HDU ACM 2066 一个人的旅行
    HDU ACM 3790 最短路径问题
    HDU ACM 1879 继续畅通工程
    HDU ACM 1856 More is better(并查集)
    HDU ACM 1325 / POJ 1308 Is It A Tree?
  • 原文地址:https://www.cnblogs.com/jichuang/p/12205918.html
Copyright © 2020-2023  润新知