JWT应用功能

news2024/11/14 4:08:46

JWT

一、 JWT 实现无状态 Web 服务

1、什么是有状态

有状态服务,即服务端需要记录每次会话的客户端信息,从而识别客户端身份,根据用户身份进行请求的处理,典型的设计如tomcat中的session。

例如登录:用户登录后,我们把登录者的信息保存在服务端session中,并且给用户一个cookie值,记录对应的session。然后下次请求,用户携带cookie值来,我们就能识别到对应session,从而找到用户的信息。

缺点是什么?

  • 服务端保存大量数据,增加服务端压力
  • 服务端保存用户状态,无法进行水平扩展
  • 客户端请求依赖服务端,多次请求必须访问同一台服务器

2、什么是无状态

服务器不需要记录客户端的状态信息,即:

  • 服务端不保存任何客户端请求者信息
  • 客户端的每次请求必须具备自描述信息,通过这些信息识别客户端身份

带来的好处是什么呢?

  • 客户端请求不依赖服务端的信息,任何多次请求不需要必须访问到同一台服务
  • 服务端的集群和状态对客户端透明
  • 服务端可以任意的迁移和伸缩
  • 减小服务端存储压力

3、如何实现无状态

无状态登录的流程:

  • 当客户端第一次请求服务时,服务端对用户进行信息认证(登录)
  • 认证通过,将用户信息进行加密形成token,返回给客户端,作为登录凭证
  • 以后每次请求,客户端都携带认证的token
  • 服务的对token进行解密,判断是否有效。

流程图:

​ 客户端请求登录,登录之后颁发凭证

在这里插入图片描述

整个登录过程中,最关键的点是什么?

token的安全性

token是识别客户端身份的唯一标示,如果加密不够严密,被人伪造那就完蛋了。

采用何种方式加密才是安全可靠的呢?

我们将采用:JWT + RSA非对称加密

4、JWT简介

JWT全称是Json Web Token, 是JSON风格轻量级的授权和身份认证规范,可实现无状态、分布式的Web应用授权;官网:https://jwt.io

JWT包含三部分数据:

  • Header:头部,通常头部有两部分信息:

    • 声明类型,这里是JWT 自描述信息

    我们会对头部进行base64编码,得到第一部分数据 base64编码和解码的

  • Payload:载荷,就是有效数据,一般包含下面信息:

    • 用户身份信息(注意,这里因为采用base64编码,可解码 是可逆的,因此不要存放敏感信息)
    • 注册声明:如token的签发时间,过期时间,签发人等 这部分内容 好比身份证的信息

    这部分也会采用base64编码,得到第二部分数据

  • Signature:签名,是整个数据的认证信息。一般根据前两步的数据,再加上服务的密钥(secret)(不要泄漏,最好周期性更换),通过加密算法(不可逆的)生成一个签名。用于验证整个数据完整和可靠性。

生成的数据格式:

在这里插入图片描述

可以看到分为3段,每段就是上面的一部分数据

5、JWT交互流程

流程图:

在这里插入图片描述

步骤翻译:

  • 1、用户登录
  • 2、服务的认证,通过后根据secret生成token
  • 3、将生成的token返回给浏览器
  • 4、用户每次请求携带token
  • 5、服务端利用公钥解读jwt签名,判断签名有效后,从Payload中获取用户信息
  • 6、处理请求,返回响应结果

因为JWT签发的token中已经包含了用户的身份信息,并且每次请求都会携带,这样服务的就无需保存用户信息,甚至无需去数据库查询,完全符合了Rest的无状态规范。

6、非对称加密

加密技术是对信息进行编码和解码的技术,编码是把原来可读信息(又称明文)译成代码形式(又称密文),其逆过程就是解码(解密),加密技术的要点是加密算法,加密算法可以分为三类:

  • 对称加密,如AES
    • 基本原理:将明文分成N个组,然后使用密钥对各个组进行加密,形成各自的密文,最后把所有的分组密文进行合并,形成最终的密文。
    • 优势:算法公开、计算量小、加密速度快、加密效率高
    • 缺陷:双方都使用同样密钥,安全性得不到保证
  • 非对称加密,如RSA
    • 基本原理:同时生成两把密钥:私钥和公钥,私钥隐秘保存,公钥可以下发给信任客户端
      • 私钥加密,持有私钥或公钥才可以解密
      • 公钥加密,持有私钥才可解密
    • 优点:安全,难以破解
    • 缺点:算法比较耗时
  • 不可逆加密,如MD5,SHA
    • 基本原理:加密过程中不需要使用密钥,输入明文后由系统直接经过加密算法处理成密文,这种加密后的数据是无法被解密的,无法根据密文推算出明文。

RSA算法历史:

1977年,三位数学家Rivest、Shamir 和 Adleman 设计了一种算法,可以实现非对称加密。这种算法用他们三个人的名字缩写:RSA

目前流行的还有oauth2

二、nimbus-jose-jwt 库

nimbus-jose-jwt、jose4j、java-jwt 和 jjwt 是几个 Java 中常见的操作 JWT 的库。就使用细节而言,nimbus-jos-jwt(和jose4j)要好于 java-jwt 和 jjwt 。

nimbus-jose-jwt 官网:https://connect2id.com/products/nimbus-jose-jwt

所需坐标

    <dependency>
        <groupId>com.nimbusds</groupId>
        <artifactId>nimbus-jose-jwt</artifactId>
        <version>9.11.1</version>
    </dependency>

1、 JWT 和 JWS

这里我们需要了解下 JWT、JWS、JWE 三者之间的关系:

  • JWT(JSON Web Token)指的是一种规范,这种规范允许我们使用 JWT 在两个组织之间传递安全可靠的信息。
  • JWS(JSON Web Signature)和 JWE(JSON Web Encryption)是 JWT 规范的两种不同实现,我们平时最常使用的实现就是 JWS 。

简单来说,JWT 和 JWS、JWE 类似于接口与实现类。由于,我们使用的是 JWS ,所以,后续内容中,就直接列举 JWS 相关类,不再细分 JWS 和 JWE 了,numbus-jose-jwt 中的 JWE 相关类和接口我们也不会使用到

2、加密算法

  • 对称加密』指的是使用相同的秘钥来进行加密和解密,如果你的秘钥不想暴露给解密方,考虑使用非对称加密。在加密方和解密方是同一个人(或利益关系紧密)的情况下可以使用它。
  • 非对称加密』指的是使用公钥和私钥来进行加密解密操作。对于加密操作,公钥负责加密,私钥负责解密,对于签名操作,私钥负责签名,公钥负责验证。非对称加密在 JWT 中的使用显然属于签名操作。在加密方和解密方是不同人(或不同利益方)的情况下可以使用它。

nimbus-jose-jwt 支持的算法都在它的 JWSAlgorithm 和 JWEAlgorithm 类中有定义。

例如:JWSAlgorithm algorithm = JWSAlgorithm.HS256

3、核心 API

a、加密过程

  • 在 nimbus-jose-jwt 中,使用 Header 类代表 JWT 的头部,不过,Header 类是一个抽象类,我们使用的是它的子类 JWSHeader

    创建头部对象:

     @Test
     public void createToken(){
         //创建头部对象
         JWSHeader jwsHeader =
                 new JWSHeader.Builder(JWSAlgorithm.HS256)       // 加密算法
                         .type(JOSEObjectType.JWT) // 静态常量
                         .build();
         System.out.println(jwsHeader);
     }
    

    你可以通过 .toBase64URL() 方法求得头部信息的 Base64 形式(这也是 JWT 中的实际头部信息):

  • 使用 Payload 类的代表 JWT 的荷载部分

    创建荷载部对象:

    @Test
        public void createToken(){
            //创建头部对象
            JWSHeader jwsHeader =
                    new JWSHeader.Builder(JWSAlgorithm.HS256)       // 加密算法
                            .type(JOSEObjectType.JWT) // 静态常量
                            .build();
            System.out.println(jwsHeader);
    
            //创建载荷
            Payload payload = new Payload("hello world");
            System.out.println(payload);
        }
    

    你可以通过 .toBase64URL() 方法求得荷载部信息的 Base64 形式(这也是 JWT 中的实际荷载部信息):

  • 签名部分

    ​ 签名部分没有专门的类表示,只有通用类 Base64URL ,而且签名部分并非你自己创建出来的,而是靠 头部 + 荷载部 + 加密算法 算出来的。在 nimbus-jose-jwt 中,签名算法由 JWSAlgorithm 表示。

    注意:在创建 JWSHeader 对象时就需要指定签名算法,因为在标准中,头部需要保存签名算法名字。

    ​ 用头部和荷载部分,再加上指定的签名算法和密钥来生成签名部分的过程,在 nimbus-jose-jwt 中被称为『签名(sign)』。nimbus-jose-jwt 专门提供了一个签名器 JWSSigner ,用来参与到签名过程中。密钥就是在创建签名器的时候指定的:

    JWSSigner jwsSigner = new MACSigner("密钥");  //MACSigner()中要指定一个密钥
    

    最终,整个 JWT 由一个 JWSObject 对象表示:

    JWSObject jwsObject = new JWSObject(jwsHeader, payload);
    // 进行签名(根据前两部分生成第三部分)
    jwsObject.sign(jwsSigner);
    

    在 nimbus-jose-jwt 中 JWSObject 是有状态的:未签名、已签名和签名中。很显然,在执行完 .sign() 方法之后,JWSObject 对象就变成了已签名状态。

    当然,我们最终『要』的是 JWT 字符串,而不是对象,这里接着对代表 JWT 的 JWSObject 对象调用 .serialize() 方法即可:

    String token = jwsObject.serialize();
    

    完整示例:

        @Test
        public void createToken() throws JOSEException {
    
            //创建头部对象
            JWSHeader jwsHeader =
                    new JWSHeader.Builder(JWSAlgorithm.HS256)       // 加密算法
                            .type(JOSEObjectType.JWT) // 静态常量
                            .build();
            //创建载荷
            Payload payload = new Payload("hello world");
    
            //创建签名器
            JWSSigner jwsSigner = new MACSigner("woniu");//woniu为密钥
    
            //创建签名
            JWSObject jwsObject = new JWSObject(jwsHeader, payload);// 头部+载荷
            jwsObject.sign(jwsSigner);//再+签名部分
    
            //生成token字符串
            String token = jwsObject.serialize();
            System.out.println(token);
        }
    

    如果出现:com.nimbusds.jose.KeyLengthException: The secret length must be at least 256 bits异常,是因为密钥的长度不够增加密钥长度即可

    //创建签名器
    JWSSigner jwsSigner = new MACSigner("woniuwoniuwoniuwoniuwoniuwoniuwoniuwoniuwoniuwoniuwoniuwoniuwoniuwoniuwoniu");
    
    生成的token如下:
    	eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.aGVsbG8gd29ybGQ.p1E7jMNXs4zzCHDDbFzQXOko6s9NtT7Sqt15-T-7KVY
    

b、获得token中的内容

 /**
     * 取出token中的内容
     */
    @Test
    public void getToken() throws IOException {
        //获得token头部内容
        String jwsHeader="eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9";
        //反编码
        BASE64Decoder decoder=new BASE64Decoder();
        byte[] txtByte= jwsHeader.getBytes(StandardCharsets.UTF_8);
        System.out.println(new String(decoder.decodeBuffer(jwsHeader),"UTF-8"));

        //获得token载荷中的内容
        String payload="aGVsbG8gd29ybGQ";
        System.out.println(new String(decoder.decodeBuffer(payload),"UTF-8"));
    }

b、 解密

反向的解密和验证过程核心 API 就 2 个:JWSObject 的静态方法 parse 方法和验证其 JWSVerifier 对象。

JWSObject.parse() 方法是上面的 serialize 方法的反向操作,它可以通过一个 JWT 串生成 JWSObject 。有了 JWObject 之后,你就可以获得 header 和 payload 部分了。

如果你想直接验证 JWSObject 对象的合法性,你需要创建一个 JWSVerifier 对象。

//创建验证器
JWSVerifier jwsVerifier = new MACVerifier("密钥");//密钥要和加密时的相同

然后直接调用 jwsObject 对象的 verify 方法:

if (!jwsObject.verify(jwsVerifier)) {
    throw new RuntimeException("token 签名不合法!");
}

案例二:验证token是否合法

 /**
     * 验证token
     */
    @Test
    public void validationToken() throws Exception {
        //获得token
        String token="eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.aGVsbG8gd29ybGQ.p1E7jMNXs4zzCHDDbFzQXOko6s9NtT7Sqt15-T-7KVYA";
        //创建验证器
        JWSVerifier jwsVerifier = new MACVerifier("woniuwoniuwoniuwoniuwoniuwoniuwoniuwoniuwoniuwoniuwoniuwoniuwoniuwoniuwoniu");

        //获得签名对象
        JWSObject jwsObject=JWSObject.parse(token);
        //使用jws较验器,验证用户传递token是否合法
        if (!jwsObject.verify(jwsVerifier)) {
            throw new RuntimeException("token 签名不合法!");
        }
    }

4、创建jwt工具类

/**
 * jwt工具类
 */
@Component
public class JwtUtils {
    //使用uuid生成密钥
    private  final String secret= UUID.randomUUID().toString();
    //用户数据的key
    private  final String usernameKey="usernameKey";

    /**
     * 生成token
     * @param username 用户名
     * @return
     */
    public  String createJwtToken(String username) throws Exception {
        //创建头部对象
        JWSHeader jwsHeader =
                new JWSHeader.Builder(JWSAlgorithm.HS256)       // 加密算法
                        .type(JOSEObjectType.JWT) // 静态常量
                        .build();

        //创建载荷
        Map<String,Object> map=new HashMap<String,Object>();
        map.put(usernameKey, username);
        Payload payload= new Payload(map);

        //创建签名器
        JWSSigner jwsSigner = new MACSigner(secret);//密钥

        //创建签名
        JWSObject jwsObject = new JWSObject(jwsHeader, payload);// 头部+载荷
        jwsObject.sign(jwsSigner);//再+签名部分

        //生成token字符串
        return jwsObject.serialize();
    }

    /**
     * 验证jwt token是否合法
     * @param jwtStr
     * @return
     */
    @SneakyThrows
    public  boolean verify(String jwtStr) {
        JWSObject jwsObject=JWSObject.parse(jwtStr);
        JWSVerifier jwsVerifier=new MACVerifier(secret);
        return jwsObject.verify(jwsVerifier);
    }

    /**
     * 从token中解析出用户名
     * @param jwtStr
     * @return
     */
    @SneakyThrows
    public  String getUserNameFormJwt(String jwtStr){
        JWSObject jwsObject=JWSObject.parse(jwtStr);
        Map<String,Object> map=jwsObject.getPayload().toJSONObject();
        return (String) map.get(usernameKey);
    }
}

三、根据帐号密码登录

(不整合security 情况下用拦截器实现,整合时交给security管理)

1、登录步骤

  • 输入帐号密码到数据库中查询

  • 查询成功根据帐号生成token保存到redis中,同时发送token给浏览器

    • service代码

      @Service
      @Transactional
      public class UserService {
          @Autowired
          private StringRedisTemplate redisTemplate;
          @Autowired
          private JwtUtils jwtUtils;
      
          /**
           * 登录业务
           * @param account  帐号
           * @param password  密码
           * @return
           */
          @SneakyThrows
          public String getLogin(String account, String password){
              String token=null;
              if(account.equals("tom") && password.equals("123")){
                  //登录成功,生成token保存到redis中
                  token=jwtUtils.createJwtToken(account);
                  //保存token到redis中,有效期一分钟
                  ValueOperations<String,String> operations=redisTemplate.opsForValue();
                  operations.set("login:token:"+account, token, 60, TimeUnit.SECONDS);
              }
              return token;
          }
      }
      
    • controller代码

      @RestController
      public class LoginController {
      
          @Autowired
          private UserService userService;
      
          /**
           * 登录
           * @param account
           * @param password
           * @return
           */
          @GetMapping("login")
          public ResponseResult<Object> login(String account,String password){
              String token=userService.getLogin(account, password);
              if(token!=null){
                  return new ResponseResult<Object>(2000,"OK",token);
              }
              return new ResponseResult<Object>(2001,"帐号或密码错误");
          }
          
      }
      

2、非登录步骤

  • 发送请求将token一起发给服务器

  • 服务端获得token对tken进行合法性验证

  • 验证成功放行到controller,验证失败则提示

    • controller代码

      @RestController
      public class UserController {
          /**
           * 查询用户列表
           * @return
           */
          public ResponseResult<Void> userList(){
              return new ResponseResult<Void>(2000,"用户列表成功");
          }
      }
      
    • 拦截器代码

      /**
       * token验证拦截器
       */
      @Component
      public class TokenHandler implements HandlerInterceptor {
      
          @Autowired
          private StringRedisTemplate redisMapper;
      
          @Autowired
          private JwtUtils jwtUtils;
      
          //json工具
          private ObjectMapper objectMapper=new ObjectMapper();
      
          /**
           * 在请求到达conttroller前判断权限
           * @param request
           * @param response
           * @param object
           * @return
           * @throws Exception
           */
          @Override
          public boolean preHandle(HttpServletRequest request,
                                   HttpServletResponse response,
                                   Object object) throws Exception {
      
              //获得请求头中的token
              String token=request.getHeader("x-jwt-token");
      
              //请求头中没有token
              if(token==null){
                  String json=objectMapper.writeValueAsString(new ResponseResult<Void>(6001,"请求中没有token"));
                  response.setContentType("application/json;charset=UTF-8");
                  response.getWriter().write(json);
                  return false;
              }
      
              //验证请求头中的token是否合法
              if(jwtUtils.verify(token)==false){
                  String json=objectMapper.writeValueAsString(new ResponseResult<Void>(6002,"token不合法"));
                  response.setContentType("application/json;charset=UTF-8");
                  response.getWriter().write(json);
                  return false;
              }
      
              //token合法则取出token中保存的帐号
              String account=jwtUtils.getUserNameFormJwt(token);
              //根据帐号验证redis中保存的token是否存在
              if(redisMapper.hasKey("login:token:"+account)==false){
                  String json=objectMapper.writeValueAsString(new ResponseResult<Void>(6003,"未登录或token已过期"));
                  response.setContentType("application/json;charset=UTF-8");
                  response.getWriter().write(json);
                  return false;
              }
      
             return true;
          }
      
          @Override
          public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
              HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
          }
      
          @Override
          public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
              HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
          }
      }
      
    • springmvc配置类

      @Configuration
      public class WebConfigurer implements WebMvcConfigurer {
      
          @Autowired
          private TokenHandler tokenHandler;
      
          /**
           * 注册自定义拦截器
           * @param registry
           */
          @Override
          public void addInterceptors(InterceptorRegistry registry) {
              registry.addInterceptor(tokenHandler)
                      .addPathPatterns("/**")  //拦截哪些url   /**表示拦截所有
                      .excludePathPatterns("/","/login");
              //放行哪些请求  所有在登录前需要直接显示的就放行
          }
      }
      

四、JWT手机登录

1、登录流程

  • 输入手机号,根据手机号生成验证码保存到redis中并将验证码发给用户

    • service
    @Service
    @Transactional
    public class PhoneService {
    
        @Autowired
        private StringRedisTemplate redisTemplate;
        @Autowired
        private JwtUtils jwtUtils;
    
        //生成验证码
        public String createdCode(String phone) {
            // 随机产生4位激活码
            String[] codes = {"0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F", "G", "H",
                    "J", "K", "L", "M", "N", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z"};
            String code = "";
            Random random = new Random();
            for (int i = 0; i < 4; i++) {
                String str = codes[random.nextInt(codes.length)];
                code += str;
            }
    		return code;
           
        }
    }
    
    • controller

      @RestController
      public class PhoneController {
          @Autowired
          private PhoneService phoneService;
          
          /**
           * 根据手机号生成验证码
           * @param phone
           * @return
           */
          @GetMapping("phone/code")
          public ResponseResult<Object> getCode(String phone){
              String code=phoneService.createdCode(phone);
              if(!code.equals("")){
                  //保存验证码到redis中,有效时间2分钟
                  //setIfAbsent:如果redis中存在某个键则添加失败
                  redisTemplate.opsForValue().setIfAbsent("login:code:"+phone, code, 2, TimeUnit.MINUTES);
                  return new ResponseResult<Object>(2000,"OK",code);
              } else {
                   return new ResponseResult<Object>(5000,"验证码发送失败");
              }
          } 
      }
      
      
  • 发送手机号和验证码到服务器,服务端对验证码进行验证

  • 验证成功,根据手机号到数据库中查询用户信息

  • 没有查到信息表示手机号没注册

  • 查到信息将用户帐号保存到redis中

    • service
    @Service
    @Transactional
    public class PhoneService {
    
        @Autowired
        private StringRedisTemplate redisTemplate;
        @Autowired
        private JwtUtils jwtUtils;
    
        //生成验证码
        public String createdCode(String phone) {
            // 随机产生4位激活码
            String[] codes = {"0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F", "G", "H",
                    "J", "K", "L", "M", "N", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z"};
            String code = "";
            Random random = new Random();
            for (int i = 0; i < 4; i++) {
                String str = codes[random.nextInt(codes.length)];
                code += str;
            }
            return code;
        }
    
        /**
         * 生成token
         *
         * @param account 帐号
         * @return
         */
        @SneakyThrows
        public String createdToken(String account) {
            //登录成功,生成token保存到redis中
            String token = jwtUtils.createJwtToken(account);
            //保存token到redis中,有效期一分钟
            ValueOperations<String, String> operations = redisTemplate.opsForValue();
            operations.set("login:token:" + account, token, 60, TimeUnit.SECONDS);
            return token;
        }
    }
    
    • controller

      @RestController
      public class PhoneController {
        @Autowired
          private PhoneService phoneService;
      
          @Autowired
          private StringRedisTemplate redisTemplate;
      
          /**
           * 根据手机号生成验证码,放入redis 设置过期时间
           * @param phone
           * @return
           */
          @GetMapping("phone/code")
          public ResponseResult<Object> getCode(String phone){
              String code=phoneService.createdCode(phone);
              if(!code.equals("")){
                  //保存验证码到redis中,有效时间2分钟
                  //setIfAbsent:如果redis中存在某个键则添加失败
                  redisTemplate.opsForValue().setIfAbsent("login:code:"+phone, code, 2, TimeUnit.MINUTES);
                  return new ResponseResult<Object>(2000,"OK",code);
              } else {
                  return new ResponseResult<Object>(5000,"验证码发送失败");
              }
          }
      
          /**
           * 该手机用发送的验证吗登录
           * @param phone
           * @return
           */
          @GetMapping("phone/login")
          public ResponseResult<Object> phoneLogin(String phone,String code){
              //根据手机号取出redis中的验证码
              String redisCode=redisTemplate.opsForValue().get("login:code:"+phone);
              if(redisCode==null){
                  return new ResponseResult<Object>(1009,"验证码已过期");
              }
              if(!redisCode.toUpperCase().equals(code)){
                  return new ResponseResult<Object>(1010,"验证码输入错误");
              }
      
              //模仿根据手机号去查询数据库,去查用户名信息,判断是否注册
              List<User> list=new ArrayList<User>();
              list.add(new User("tom", "123", "123123"));
              list.add(new User("jack", "123", "456456"));
              //模拟查询结果
              User user=null;
             for(User u:list){
                 if(u.getPhone().equals(phone)){
                     user=u;
                     break;
                 }
             }
      
             if(user==null){
                 return new ResponseResult<Object>(2010,"手机号未注册");
             }
      
             String token= phoneService.createdToken(user.getAccount());
             return new ResponseResult<Object>(2000,"OK",token);
        }
      }
      

五、token续期

在实际的开发中,token不可能一直有效,比如30分钟内一次都没有进行操作,则认证过期,需要重新登录,如果一直在进行请求访问则token一直有效,直到上一次访问距离下一次访问的时间超过了30分钟,则认证过期。

基于上面的需求:如果用户一直在进行请求,则认证一直有效,如何做到这一点。需要对token进行续期操作。对token进行续期有很多种方案,常见的方案是:使用redis控制过期时间,使用双Token。我们采用redis方案

  • 修改token拦截器

    /**
     * token验证拦截器
     */
    @Component
    public class JwtTokenHandler implements HandlerInterceptor {
    
        @Autowired
        private StringRedisTemplate redisMapper;
    
        @Autowired
        private JwtUtils jwtUtils;
    
        //json工具
        private ObjectMapper objectMapper=new ObjectMapper();
    
        /**
         * 在请求到达conttroller前判断权限
         * @param request
         * @param response
         * @param object
         * @return
         * @throws Exception
         */
        @Override
        public boolean preHandle(HttpServletRequest request,
                                 HttpServletResponse response,
                                 Object object) throws Exception {
    
            //获得请求头中的token
            String token=request.getHeader("x-jwt-token");
    
            //请求头中没有token
            if(token==null){
                String json=objectMapper.writeValueAsString(new ResponseResult<Void>(6001,"未登录,请先登录"));
                response.setContentType("application/json;charset=UTF-8");
                response.getWriter().write(json);
                return false;
            }
    
            //验证请求头中的token是否合法
            if(jwtUtils.verify(token)==false){
                String json=objectMapper.writeValueAsString(new ResponseResult<Void>(6001,"未登录,请先登录"));
                response.setContentType("application/json;charset=UTF-8");
                response.getWriter().write(json);
                return false;
            }
    
            //token合法则取出token中保存的帐号
            String account=jwtUtils.getUserNameFormJwt(token);
    
            //根据帐号验证redis中保存的token是否存在
            if(redisMapper.hasKey("login:token:"+account)==false){
                String json=objectMapper.writeValueAsString(new ResponseResult<Void>(6003,"token已过期"));
                response.setContentType("application/json;charset=UTF-8");
                response.getWriter().write(json);
                return false;
            }
    
            //对token进行续期
            if(redisMapper.hasKey("login:token:"+account)){
                String jwtToken=redisMapper.opsForValue().get("login:token:"+account);
                //判断是不是同一个token
                if(token.equals(jwtToken)){
                    //给login:token 重新设置时间
                    //redisMapper.getExpire("login:token:"+account);
                    redisMapper.opsForValue().set("login:token:"+account, jwtToken,1, TimeUnit.MINUTES);
                    return true;
                }else{
                    String json=objectMapper.writeValueAsString(new ResponseResult<Void>(6003,"未登录或token已过期"));
                    response.setContentType("application/json;charset=UTF-8");
                    response.getWriter().write(json);
                    return false;
                }
    
            }
            return true;
        }
    
        @Override
        public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
            HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
        }
    
        @Override
        public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
            HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
        }
    }
    
          }else{
                String json=objectMapper.writeValueAsString(new ResponseResult<Void>(6003,"未登录或token已过期"));
                response.setContentType("application/json;charset=UTF-8");
                response.getWriter().write(json);
                return false;
            }
    
        }
        return true;
    }
    
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
    }
    
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
    }
    

    }

    
    
    
    
    
    
    
    

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/834012.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

SpringCloud《Eureka、Ribbon、Feign、Hystrix、Zuul》作用简单介绍

概述 SpringCloud是一个全家桶&#xff0c;包含多个组件。 本文主要介绍几个重要组件&#xff0c;也就是Eureka、Ribbon、Feign、Hystrix、Zuul这几个组件。 一、业务场景介绍 业务流程&#xff0c;支付订单功能 订单服务改变为已支付订单服务调用库存服务&#xff0c;扣减…

Webpack开启本地服务器;HMR热模块替换;devServer配置;开发与生成环境的区分与配置

目录 1_开启本地服务器1.1_开启本地服务器原因1.2_webpack-dev-server 2_HMR热模块替换2.1_认识2.2_开启HMR2.3_框架的HMR 3_devServer配置3.1_host配置3.2_port、open、compress 4_开发与生成环境4.1_如何区分开发环境4.2_入口文件解析4.3_区分开发和生成环境配置 1_开启本地服…

743. 网络延迟时间

有 n 个网络节点&#xff0c;标记为 1 到 n。 给你一个列表 times&#xff0c;表示信号经过 有向 边的传递时间。 times[i] (ui, vi, wi)&#xff0c;其中 ui 是源节点&#xff0c;vi 是目标节点&#xff0c; wi 是一个信号从源节点传递到目标节点的时间。 现在&#xff0c;…

iMX6ULL驱动开发 | OLED显示屏SPI驱动实现(SH1106,ssd1306)

周日业余时间太无聊&#xff0c;又不喜欢玩游戏&#xff0c;大家的兴趣爱好都是啥&#xff1f;我觉得敲代码也是一种兴趣爱好。正巧手边有一块儿0.96寸的OLED显示屏&#xff0c;一直在吃灰&#xff0c;何不把玩一把&#xff1f;于是说干就干&#xff0c;最后在我的imax6ul的lin…

泛微最近的漏洞利用工具

WeaverExploit_All 0x01 介绍 泛微最近的漏洞利用工具&#xff08;PS&#xff1a;2023&#xff09; 集成了QVD-2023-5012、CVE-2023-2523、CVE-2023-2648、getloginid_ofsLogin 漏洞利用 新增&#xff1a;WorkflowServiceXml 内存马注入、uploaderOperate文件上传漏洞、Del…

go-zero超强工具goctl的常用命令api,rpc,model及其构建的服务解析

goctl api 详情移步&#xff1a; go-zero的路由机制解析 基于go-zero的api服务刨析并对比与gin的区别 goctl rpc goctl支持多种rpc&#xff0c;较为流行的是google开源的grpc&#xff0c;这里主要介绍goctl rpc protoc的代码生成与使用。 protoc是grpc的命令&#xff0c;作用…

SpringBoot的pom文件、容器、组件

一、pom文件 <?xml version"1.0" encoding"UTF-8"?> <project xmlns"http://maven.apache.org/POM/4.0.0" xmlns:xsi"http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation"http://maven.apache.org/POM/4…

springboot-mybatis的增删改查

目录 一、准备工作 二、常用配置 三、尝试 四、增删改查 1、增加 2、删除 3、修改 4、查询 五、XML的映射方法 一、准备工作 实施前的准备工作&#xff1a; 准备数据库表 创建一个新的springboot工程&#xff0c;选择引入对应的起步依赖&#xff08;mybatis、mysql驱动…

基于springboot的课程作业管理系统【附开题|ppt|万字文档(LW)和搭建文档】

主要功能 学生登录&#xff1a; ①首页、个人中心&#xff1a;修改密码、个人信息管理等 ②公告信息管理、课程信息管理、学生选课管理、作业布置管理、作业提交管理、作业评分管理、课程评价管理、课程资源管理 教师登录&#xff1a; ①首页、个人中心&#xff1a;修改密码、…

操作系统专栏4-网络专题from小林coding

网络专题 文件传输mmapwritesend file大文件传输过程 文件传输 传统的文件传输过程 在这个过程中发生了4次用户态与内核态之间的切换,4次数据拷贝分别是 read系统调用陷入内核,read完成返回write调用陷入内核,write返回 4次数据拷贝分别是 磁盘->内核缓冲区->用户缓冲…

改进正弦算法引导的蜣螂优化算法(MSADBO)

概述 蜣螂优化算法由于其寻优速度和收敛精度&#xff0c;自2023年问世以来&#xff0c;热度一直很高。本篇文章对蜣螂算法进行改进&#xff0c;改进思路是参考2023年6月25号发表在知网的一篇文献&#xff08;文献放在了文章末尾&#xff09;。 改进的蜣螂优化算法融合了改进的正…

Redis 变慢了 解决方案

一、Redis为什么变慢了 1.Redis真的变慢了吗&#xff1f; 对 Redis 进行基准性能测试 例如&#xff0c;我的机器配置比较低&#xff0c;当延迟为 2ms 时&#xff0c;我就认为 Redis 变慢了&#xff0c;但是如果你的硬件配置比较高&#xff0c;那么在你的运行环境下&#xff…

Cloud Keys Delphi Edition Crack

Cloud Keys Delphi Edition Crack 云密钥使基于云的密钥和秘密管理与任何支持的平台或开发技术轻松集成。这些易于使用的组件可用于与流行的云密钥管理提供商安全集成&#xff0c;如Amazon KMS、Amazon AWS Secrets、Azure key Vault、Google cloud KMS和Google Secret Manager…

自动化测试工具在软件测试中扮演了重要的角色

随着软件开发行业的发展和变革&#xff0c;软件测试变得愈发重要。在传统的软件测试过程中&#xff0c;测试人员需要手动执行测试用例&#xff0c;耗费大量时间和资源。然而&#xff0c;随着自动化测试工具的出现&#xff0c;软件测试的效率和质量得到了极大的提升。 自动化测试…

用Python自制软件,看视频畅通无阻

前言 大家早好、午好、晚好吖 ❤ ~欢迎光临本文章 一个账号只能登录一台设备&#xff1f;涨价就涨价&#xff0c;至少还能借借朋友的&#xff0c;谁还没几个朋友&#xff0c;搞限制登录这一出&#xff0c;瞬间不稀罕了 这个年头谁还不会点技术了&#xff0c;直接拿python自制一…

Excel中——日期列后添加星期

需求&#xff1a;在日期列中添加星期几&#xff1f; 第一步&#xff1a;打开需要添加星期的Excel文件&#xff0c;在日期后面添加日期 第二步&#xff1a;选择日期列&#xff0c;点击鼠标右键&#xff0c;在下拉列表中&#xff0c;选择“设置单元格格式” 第三步&#xff1a; 在…

Miniled透明屏:超薄、轻便,还有哪些特点?

Miniled透明屏是一种新型的显示屏技术&#xff0c;它采用了微小的LED灯珠作为显示单元&#xff0c;通过透明的材料进行封装&#xff0c;使得整个屏幕具有透明的特性。Miniled透明屏具有以下几个特点&#xff1a; 首先&#xff0c;Miniled透明屏具有高亮度和高对比度的特点。 由…

TypeScript实战篇 - TS实战: 服务层开发-令牌

目录 接口说明 服务设计 WHY NOT Socket&#xff1f; huatian/svc【node.js 接口服务端】 huatian-svc/package.json huatian-svc/tsconfig.json huatian-svc/src/main.ts huatian-svc/nodemon.json huatian-svc/src/context/AccountContext.ts huatian-svc/src/repo…

软件测试面试真题 | 什么是PO设计模式?

面试官问&#xff1a;UI自动化测试中有使用过设计模式吗&#xff1f;了解什么是PO设计模式吗&#xff1f; 考察点 《page object 设计模式》&#xff1a;PageObject设计模式的设计思想、设计原则 《web自动化测试实战》&#xff1a;结合PageObject在真实项目中的实践与应用情…

活动目录密码更改

定期更改密码是一种健康的习惯&#xff0c;因为它有助于阻止使用被盗凭据的网络攻击&#xff0c;安全专家建议管理员应确保用户使用有效的密码过期策略更改其密码。 管理员可以通过电子邮件通知用户在密码即将过期时更改其密码&#xff0c;但在许多组织中&#xff0c;用户只能…