SpringBoot中双token实现无感刷新

 更新时间:2025年07月10日 09:31:35   作者:悟能不能悟  
本文介绍双Token无感刷新机制,前端React与后端SpringBoot实现,采用HttpOnly Cookie和短期Token设计,具有一定的参考价值,感兴趣的可以了解一下

一、方案说明

1. 核心流程

  1. 用户登录
    • 提交账号密码 → 服务端验证 → 返回Access Token(前端存储) + Refresh Token(HttpOnly Cookie)
  2. 业务请求
    • 请求头携带Access Token → 服务端验证有效性 → 有效则返回数据
  3. Token过期处理
    • 若Access Token过期 → 前端拦截401错误 → 自动用Refresh Token请求新Token → 刷新后重试原请求
  4. Refresh Token失效
    • 清除登录态 → 跳转登录页

2. 安全设计

  • Access Token
    • 存储:前端内存(如Vuex/Redux)或sessionStorage
    • 有效期:2小时
    • 传输:Authorization: Bearer <token>
  • Refresh Token
    • 存储:HttpOnly + Secure + SameSite=Strict Cookie
    • 有效期:7天
    • 刷新机制:单次使用后更新,旧Token立即失效

二、前端实现(React示例)

1. Axios封装(src/utils/http.js)

import axios from 'axios';
 
const http = axios.create({
  baseURL: process.env.REACT_APP_API_URL,
});
 
// 请求拦截器:注入Access Token
http.interceptors.request.use(config => {
  const accessToken = sessionStorage.getItem('access_token');
  if (accessToken) {
    config.headers.Authorization = `Bearer ${accessToken}`;
  }
  return config;
});
 
// 响应拦截器:处理Token过期
http.interceptors.response.use(
  response => response,
  async error => {
    const originalRequest = error.config;
    
    // 检测401错误且未重试过
    if (error.response?.status === 401 && !originalRequest._retry) {
      originalRequest._retry = true;
      
      try {
        // 发起刷新Token请求
        const { accessToken } = await refreshToken();
        
        // 存储新Token
        sessionStorage.setItem('access_token', accessToken);
        
        // 重试原请求
        originalRequest.headers.Authorization = `Bearer ${accessToken}`;
        return http(originalRequest);
      } catch (refreshError) {
        // 刷新失败:清除Token,跳转登录
        sessionStorage.removeItem('access_token');
        window.location.href = '/login';
        return Promise.reject(refreshError);
      }
    }
    
    return Promise.reject(error);
  }
);
 
// 刷新Token函数
async function refreshToken() {
  const res = await axios.post(
    `${process.env.REACT_APP_API_URL}/auth/refresh`,
    {},
    { withCredentials: true } // 自动携带Cookie
  );
  return res.data;
}
 
export default http;

2. 登录逻辑(src/pages/Login.js)

const LoginPage = () => {
  const handleSubmit = async (e) => {
    e.preventDefault();
    try {
      const res = await axios.post('/auth/login', {
        username: 'user',
        password: 'pass'
      }, { withCredentials: true });
      
      // 存储Access Token
      sessionStorage.setItem('access_token', res.data.accessToken);
      
      // 跳转主页
      window.location.href = '/';
    } catch (err) {
      alert('登录失败');
    }
  };
 
  return (
    <form onSubmit={handleSubmit}>
      {/* 登录表单 */}
    </form>
  );
};

三、后端实现(Spring Boot)

1. JWT工具类(JwtUtil.java)

@Component
public class JwtUtil {
    @Value("${jwt.secret}")
    private String secret;
 
    @Value("${jwt.access.expiration}")
    private Long accessExpiration;
 
    @Value("${jwt.refresh.expiration}")
    private Long refreshExpiration;
 
    // 生成Access Token
    public String generateAccessToken(UserDetails user) {
        return buildToken(user, accessExpiration);
    }
 
    // 生成Refresh Token
    public String generateRefreshToken(UserDetails user) {
        return buildToken(user, refreshExpiration);
    }
 
    private String buildToken(UserDetails user, Long expiration) {
        return Jwts.builder()
                .setSubject(user.getUsername())
                .setIssuedAt(new Date())
                .setExpiration(new Date(System.currentTimeMillis() + expiration))
                .signWith(SignatureAlgorithm.HS256, secret)
                .compact();
    }
 
    // 验证Token
    public boolean validateToken(String token) {
        try {
            Jwts.parser().setSigningKey(secret).parseClaimsJws(token);
            return true;
        } catch (JwtException | IllegalArgumentException e) {
            throw new JwtException("Token验证失败");
        }
    }
 
    // 从Token中提取用户名
    public String getUsernameFromToken(String token) {
        return Jwts.parser()
                .setSigningKey(secret)
                .parseClaimsJws(token)
                .getBody()
                .getSubject();
    }
}

2. 认证接口(AuthController.java)

@RestController
@RequestMapping("/auth")
public class AuthController {
    @Autowired
    private JwtUtil jwtUtil;
    
    @Autowired
    private UserDetailsService userDetailsService;
    
    @Autowired
    private RefreshTokenService refreshTokenService;
 
    // 登录接口
    @PostMapping("/login")
    public ResponseEntity<?> login(@RequestBody LoginRequest request) {
        UserDetails user = userDetailsService.loadUserByUsername(request.getUsername());
        
        // 密码验证
        if (!passwordEncoder.matches(request.getPassword(), user.getPassword())) {
            throw new BadCredentialsException("密码错误");
        }
 
        // 生成Token
        String accessToken = jwtUtil.generateAccessToken(user);
        String refreshToken = jwtUtil.generateRefreshToken(user);
 
        // 存储Refresh Token
        refreshTokenService.saveRefreshToken(user.getUsername(), refreshToken);
 
        // 设置Refresh Token到Cookie
        ResponseCookie cookie = ResponseCookie.from("refreshToken", refreshToken)
                .httpOnly(true)
                .secure(true)
                .sameSite("Strict")
                .maxAge(jwtUtil.getRefreshExpiration() / 1000)
                .path("/auth/refresh")
                .build();
 
        return ResponseEntity.ok()
                .header(HttpHeaders.SET_COOKIE, cookie.toString())
                .body(new AuthResponse(accessToken));
    }
 
    // 刷新Token接口
    @PostMapping("/refresh")
    public ResponseEntity<?> refreshToken(@CookieValue("refreshToken") String refreshToken) {
        // 验证Refresh Token
        if (!jwtUtil.validateToken(refreshToken)) {
            throw new JwtException("无效Token");
        }
 
        String username = jwtUtil.getUsernameFromToken(refreshToken);
        
        // 检查是否与存储的Token一致
        if (!refreshTokenService.validateRefreshToken(username, refreshToken)) {
            throw new JwtException("Token已失效");
        }
 
        // 生成新Token
        UserDetails user = userDetailsService.loadUserByUsername(username);
        String newAccessToken = jwtUtil.generateAccessToken(user);
        String newRefreshToken = jwtUtil.generateRefreshToken(user);
 
        // 更新存储的Refresh Token
        refreshTokenService.updateRefreshToken(username, newRefreshToken);
 
        // 返回新Token
        ResponseCookie cookie = ResponseCookie.from("refreshToken", newRefreshToken)
                .httpOnly(true)
                .secure(true)
                .sameSite("Strict")
                .maxAge(jwtUtil.getRefreshExpiration() / 1000)
                .path("/auth/refresh")
                .build();
 
        return ResponseEntity.ok()
                .header(HttpHeaders.SET_COOKIE, cookie.toString())
                .body(new AuthResponse(newAccessToken));
    }
}

3. Refresh Token服务(RefreshTokenService.java)

@Service
public class RefreshTokenService {
    @Autowired
    private RefreshTokenRepository repository;
 
    public void saveRefreshToken(String username, String token) {
        RefreshToken refreshToken = new RefreshToken();
        refreshToken.setUsername(username);
        refreshToken.setToken(token);
        refreshToken.setExpiryDate(jwtUtil.getExpirationDateFromToken(token));
        repository.save(refreshToken);
    }
 
    public boolean validateRefreshToken(String username, String token) {
        return repository.findByUsernameAndToken(username, token)
                .map(t -> t.getExpiryDate().after(new Date()))
                .orElse(false);
    }
 
    public void updateRefreshToken(String username, String newToken) {
        repository.deleteByUsername(username);
        saveRefreshToken(username, newToken);
    }
}

四、安全配置(SecurityConfig.java)

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
    @Autowired
    private JwtAuthenticationFilter jwtFilter;
 
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .csrf().disable()
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and()
            .authorizeRequests()
                .antMatchers("/auth/​**​").permitAll()
                .anyRequest().authenticated()
            .and()
            .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
    }
}
 
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
    
    @Autowired
    private JwtUtil jwtUtil;
 
    @Override
    protected void doFilterInternal(HttpServletRequest request, 
                                    HttpServletResponse response, 
                                    FilterChain chain) throws IOException, ServletException {
        String header = request.getHeader("Authorization");
        if (header != null && header.startsWith("Bearer ")) {
            String token = header.substring(7);
            if (jwtUtil.validateToken(token)) {
                String username = jwtUtil.getUsernameFromToken(token);
                UsernamePasswordAuthenticationToken auth = 
                    new UsernamePasswordAuthenticationToken(username, null, new ArrayList<>());
                SecurityContextHolder.getContext().setAuthentication(auth);
            }
        }
        chain.doFilter(request, response);
    }
}

五、配置参数(application.yml)

jwt:
  secret: "your-256-bit-secret-key-here" # 通过环境变量注入
  access:
    expiration: 7200000 # 2小时(毫秒)
  refresh:
    expiration: 604800000 # 7天(毫秒)

六、数据库表结构(MySQL)

CREATE TABLE refresh_tokens (
  id INT AUTO_INCREMENT PRIMARY KEY,
  username VARCHAR(255) NOT NULL,
  token VARCHAR(512) NOT NULL,
  expiry_date DATETIME NOT NULL,
  UNIQUE KEY (username)
);

此方案完整实现了双Token无感刷新机制,具备以下特点:

  1. 完整的前后端代码示例,可直接集成到项目中
  2. 遵循安全最佳实践(HttpOnly Cookie、短期Token)
  3. 支持并发请求处理和Token主动吊销
  4. 清晰的模块划分,易于扩展维护

到此这篇关于SpringBoot中双token实现无感刷新的文章就介绍到这了,更多相关SpringBoot 双token无感刷新内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家! 

相关文章

  • Java中的lambda和stream实现排序

    Java中的lambda和stream实现排序

    这篇文章主要介绍了Java中的lambda和stream实现排序,文章围绕主题展开详细的内容介绍,具有一定的参考价值,需要的小伙伴可以参考一下
    2022-09-09
  • Java并发编程之synchronized底层实现原理分析

    Java并发编程之synchronized底层实现原理分析

    这篇文章主要介绍了Java并发编程之synchronized底层实现原理,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2024-02-02
  • mybatis时间范围查询代码示例

    mybatis时间范围查询代码示例

    这篇文章主要给大家介绍了关于mybatis时间范围查询的相关资料,在项⽬中避免不了要⽤到时间范围查询,文中通过代码示例介绍的非常详细,需要的朋友可以参考下
    2023-08-08
  • elasticsearch插件开发教程

    elasticsearch插件开发教程

    这篇文章主要为大家详细介绍了elasticsearch插件开发教程,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2017-06-06
  • Java中replace、replaceAll和replaceFirst函数的用法小结

    Java中replace、replaceAll和replaceFirst函数的用法小结

    相信会java的同学估计都用过replace、replaceAll、replaceFirst这三个函数,可是,我们真的懂他们吗?下面通过这篇文章大家再来好好学习学习下这几个函数。
    2016-09-09
  • java jdk动态代理详解

    java jdk动态代理详解

    动态代理类的Class实例是怎么生成的呢,是通过ProxyGenerator类来生成动态代理类的class字节流,把它载入方法区
    2013-09-09
  • SpringBoot集成Swagger2构建在线API文档的代码详解

    SpringBoot集成Swagger2构建在线API文档的代码详解

    这篇文章主要介绍了SpringBoot集成Swagger2构建在线API文档,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2020-12-12
  • mybatis if test条件判断语句中的判断问题分析

    mybatis if test条件判断语句中的判断问题分析

    这篇文章主要介绍了mybatis if test条件判断语句中的判断问题,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-03-03
  • 关于Idea的Invalidate Caches/Restart使用

    关于Idea的Invalidate Caches/Restart使用

    项目类导入爆红可能因Idea缓存异常导致Maven依赖识别失败,解决方法为通过Invalidate Caches/Restart清除缓存,等待重新构建索引后重新进入项目
    2025-07-07
  • RabbitMQ工作模式之通配符模式详解

    RabbitMQ工作模式之通配符模式详解

    通配符模式是一种灵活的消息传递模式,可以根据消息的路由键(routing key)和绑定(binding)模式来实现精确的消息过滤和匹配,本文给大家介绍RabbitMQ工作模式之通配符模式,感兴趣的朋友跟随小编一起看看吧
    2026-01-01

最新评论