开发者

SpringBoot中双token实现无感刷新

目录
  • 一、方案说明
    • 1. 核心流程
    • 2. 安全设计
  • 二、前端实现(React示例)
    • 1. AxIOS封装(src/utils/http.js)
    • 2. 登录逻辑(src/pages/Login.js)
  • 三、后端实现(Spring Boot)
    • 1. JWT工具类(JwtUtil.Java)
    • 2. 认证接口(AuthController.java)
    • 3. Refresh Token服务(RefreshTokenService.java)
  • 四、安全配置(SecurityConfig.java)
    • 五、配置参数(application.yml)
      • 六、数据库表结构(mysql)

        一、方案说明

        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: t编程客栈rue } // 自动携带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(www.devze.comrequest.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.ohttp://www.devze.comk()
                        .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 {
               RYHBtTpY 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 &amjavascriptp;& 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无感刷新内容请搜索编程客栈(www.devze.com)以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程客栈(www.devze.com)! 

        0

        上一篇:

        下一篇:

        精彩评论

        暂无评论...
        验证码 换一张
        取 消

        最新开发

        开发排行榜