一文彻底搞懂JWT


一、概述

什么是JWT?

JSON Web Token(JWT)是⼀个开放标准(RFC?7519),它定义了⼀种紧凑的、⾃包含的⽅式,⽤于作为JSON对象在各⽅之间安全地传输信息。该信息可以被验证和信任,因为它是数字签名的。

什么时候应该⽤JWT?

Authorization?(授权):
这是使⽤JWT的最常⻅场景。⼀旦⽤⼾登录,后续每个请求都将包含JWT,允许⽤⼾访问该令牌允许的路由、服务和资源。单点登录是现在⼴泛使⽤的JWT的⼀个特性,因为它的开销很⼩,并且可以轻松地跨域使⽤。

Information?Exchange?(信息交换):
对于安全的在各⽅之间传输信息⽽⾔,JSON?Web?Tokens⽆疑是⼀种很好的⽅式。因为JWT可以被签名,例如,⽤公钥/私钥对,你可以确定发送⼈就是它们所说的那个⼈。另外,由于签名是使⽤头和有效负载计算的,您还可以验证内容没有被篡改。

认证流程

image.png

  • ⾸先,前端通过Web表单将⾃⼰的⽤⼾名和密码发送到后端的接⼝。这⼀过程⼀般是⼀个HTTP POST请求。建议的⽅式是通过SSL加密的传输(https协议)?,从⽽避免敏感信息被嗅探。

  • 后端核对⽤⼾名和密码成功后,将⽤⼾的id等其他信息作为JWT?Payload?(负载),将其与头部分别进⾏Base64编码拼接后签名,形成⼀个JWT(Token)。形成的JWT就是⼀个形同 xxx.yyy.zzz 的字符串。token:head.payload.signature

  • 后端将JWT字符串作为登录成功的返回结果返回给前端。前端可以将返回的结果保存在localStorage或sessionStorage上,退出登录时前端删除保存的JWT即可。

  • 前端在每次请求时将JWT放⼊HTTP?Header中的Authorization位。(解决XSS和XSRF问题)

  • 后端检查是否存在,如存在验证JWT的有效性

    • 检查签名是否正确;
    • 检查Token是否过期;
    • 检查Token的接收⽅是否是⾃⼰(可选);
  • 验证通过后后端使⽤JWT中包含的⽤⼾信息进⾏其他逻辑操作,返回相应结果

    JWT优势在哪?

  • 简洁(Compact):?可以通过URL,POST参数或者在HTTP header发送,数据量⼩,传输速度快

  • ⾃包含(Self-contained):负载中包含了所有⽤⼾所需要的信息,避免了多次查询数据库

  • 因为Token是以JSON加密的形式保存在客⼾端的,所以JWT是跨语⾔的,原则上任何web形式都⽀持

  • 不需要在服务端保存会话信息,特别适⽤于分布式微服务

    JWT具体包含信息

    标头通常由两部分组成:令牌的类型(即JWT) 和所使⽤的签名算法,例如HMAC、SHA256或RSA。
    它会使⽤Base64编码组成JWT结构的第⼀部分
    注意:Base64是⼀种编码方式,它是可以被翻译回原来的样⼦来的。它并不是⼀种加密过程
    未使用Base64编码前的样式:

    {
        "alg":"HS256",
        "typ":"JWT"
    }

    Payload

    令牌的第⼆部分是有效负载,其中包含声明。声明是有关实体(通常是⽤⼾)和其他数据的声明。同样
    的,它会使⽤Base64?编码组成JWT结构的第⼆部分

    {
      "sub" : "HS256"
      "name" : "yjiewei"
      "admin" : "true"
    }

    Signature

    header和payload都是结果Base64编码过的,中间⽤.隔开,第三部分就是前⾯两部分合起来做签名,密钥绝对⾃⼰保管好,签名值同样做Base64编码拼接在JWT后⾯。(签名并编码)

    HMACSHA256 (base64Ur1Encode(header) + "." + base64Ur1Encode(payload) , secret);

    二、SpringBoot整合JWT

    2.1 引入maven依赖

    <parent>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-parent</artifactId>
      <version>2.2.5.RELEASE</version>
      <relativePath/> <!-- lookup parent from repository -->
    </parent>
    
    <dependencies>
      <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-thymeleaf</artifactId>
      </dependency>
    
      <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
      </dependency>
      <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
      </dependency>
      <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
      </dependency>
      <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-devtools</artifactId>
        <scope>runtime</scope>
        <optional>true</optional>
      </dependency>
    
      <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <scope>runtime</scope>
        <version>8.0.25</version>
      </dependency>
      <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-boot-starter</artifactId>
        <version>3.4.2</version>
      </dependency>
    
      <dependency>
        <groupId>com.auth0</groupId>
        <artifactId>java-jwt</artifactId>
        <version>3.18.1</version>
      </dependency>
    </dependencies>

    2.2 封装工具类

    2.2.1 封装JWT工具类JWTUtils

    public class JWTUtils {
        private static String  SECRET  = "xiong@#$%123456";		//一定要保密
    
        public static String getToken(Map<String,Object> map){
            JWTCreator.Builder builder = JWT.create();
    
            //payload
            map.forEach((k,v)->{
                builder.withClaim(k, (String) v);
            });
    
            Calendar instance = Calendar.getInstance();
            instance.add(Calendar.DATE,7);       //过期时间
    
            builder.withExpiresAt(instance.getTime());
            String token = builder.sign(Algorithm.HMAC256(SECRET));
    
            return token;
        }
    
        /*验证token*/
        public static DecodedJWT verify(String token){
            Verification require = JWT.require(Algorithm.HMAC256(SECRET));
            DecodedJWT verify = require.build().verify(token);      //没有报错就是验证成功
            return verify;
        }
    }

    2.2.2 封装RespBean返回对象

    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public class RespBean {
        private Integer status;
        private String msg;
        private Object result;
    
    
        public static RespBean build(){
            return new RespBean();
        }
    
        public static RespBean ok(String msg){
            return new RespBean(200,msg,null);
        }
    
        public static RespBean ok(String msg, Object obj){
            return new RespBean(200,msg,obj);
        }
    
        public static RespBean error(String msg){
            return new RespBean(500,msg,null);
        }
    
        public static RespBean error(String msg, Object obj){
            return new RespBean(500,msg,obj);
        }
    }

    2.3 Controller层

    @Slf4j
    @RestController
    public class UserController {
        @Autowired
        private TUserService tUserService;
    
        //需要表单post传入username password
        @PostMapping("/user/login")
        public RespBean toLogin(TUser tUser){
            RespBean build = RespBean.build();
            System.out.println(tUser);      //自动将传入的username password 封装成TUser
    
            try {
                //根据TUser 从数据库中查找
                TUser tUserDB = tUserService.queryByUsernamePassword(tUser);
                log.info("查找到用户[{}]",tUserDB);
                HashMap<String, Object> payload = new HashMap<>();
                payload.put("uid",tUserDB.getId().toString());
                payload.put("username",tUserDB.getUsername());
                String token = JWTUtils.getToken(payload);
    
                build.setStatus(200);
                build.setMsg(token);
            }catch (Exception e){
                build.setStatus(500);
                build.setResult(e.getMessage());
            }
    
            return build;
        }
    
        //测试-没有将token放到请求头中
        @GetMapping("/test")
        public RespBean test(String token){
            RespBean build = RespBean.build();
    
            DecodedJWT verify = JWTUtils.verify(token);
            String username = verify.getClaim("username").asString();
            String uid = verify.getClaim("uid").asString();
            log.warn("用户username={}",username);
            log.warn("用户id={}",uid);
    
            return build;
        }
    
        @GetMapping("/admin")
        public RespBean testAdmin(HttpServletRequest request){
            RespBean build = RespBean.build();
    
            String token = request.getHeader("token");
            //获取验证后解码的的token对象
            DecodedJWT verify = JWTUtils.verify(token);
            String un = verify.getClaim("username").asString();
            String uid = verify.getClaim("uid").asString();
            log.warn("用户username={}",un);
            log.warn("用户id={}",uid);
    
            HashMap<String, Object> map = new HashMap<>();
            map.put("username",un);
            map.put("uid",uid);
    
            build.setStatus(200);
            build.setResult(map);
    
            return build;
        }
    }

    2.4 Service层

    @Service
    public class TUserServiceImpl implements TUserService {
        @Autowired
        private TUserMapper tUserMapper;
    
    
        @Override
        public TUser queryByUsernamePassword(TUser tUser) {
            return tUserMapper.queryTUserByUsernamePassword(tUser);
        }
    }

    2.5 Mapper层

    <mapper namespace="com.xjt.mapper.TUserMapper">
        <select id="queryTUserByUsernamePassword" resultType="com.xjt.entity.TUser">
            select  * from t_user where username=#{username} and password=#{password}
        </select>
    </mapper>

    2.6 拦截器

    2.6.1 自定义JWTInterceptor拦截器

    自定义JWTInterceptor拦截器要实现HandlerInterceptor接口,可以重写3个方法,一般重写 preHandle比较多,返回true时继续执行

    public class JWTInterceptor implements HandlerInterceptor {
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
            RespBean build = RespBean.build();
    
            String token = request.getHeader("token");
            try{
                JWTUtils.verify(token);
                return true;
            }catch (SignatureVerificationException e){
                e.printStackTrace();
                build.setMsg("无效签名");
            }catch (AlgorithmMismatchException e){
                e.printStackTrace();
                build.setMsg("token算法不匹配");
            }catch (TokenExpiredException e){
                e.printStackTrace();
                build.setMsg("token过期了");
            }catch (Exception e){
                e.printStackTrace();
                build.setMsg(e.getMessage());
            }
            build.setStatus(500);
            String json = new ObjectMapper().writeValueAsString(build);
            response.setContentType("application/json;charset=utf-8");
            response.getWriter().println(json);
    
            return false;
        }
    }

    2.6.2 配置拦截器

    public class InterceptorConfig implements WebMvcConfigurer {
        @Override
        public void addInterceptors(InterceptorRegistry registry) {
            registry.addInterceptor(new JWTInterceptor())
                    .addPathPatterns("/**")     //所有请求都要token验证
                    .excludePathPatterns("/user/**");       //以 /user 开头的访问放行
        }
    }

    补充:拦截器知识
    参考:https://blog.csdn.net/levae1024/article/details/83512292
    image.png
    image.png
    两个拦截器时:
    image.png
    在Springboot中 config/InterceptorConfig.java中配置多个拦截器:
    image.png

    2.7 测试

    访问/user/login

    image.png

    访问/admin

    image.png


文章作者: CoderXiong
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 CoderXiong !
  目录