Access Token + Refresh Token 全解析:前后端分离架构的认证与安全方案

目录

  1. 前言
  2. 前后端分离架构的认证挑战
  3. [传统Session vs Token认证](#传统Session vs Token认证 "#%E4%BC%A0%E7%BB%9Fsession-vs-token%E8%AE%A4%E8%AF%81")
  4. 双Token机制核心概念
  5. 完整的认证流程设计
  6. 技术实现方案
  7. 安全性考虑
  8. 最佳实践
  9. 总结

前言

在现代Web应用开发中,前后端分离架构已成为主流。这种架构模式带来了开发效率的提升和技术栈的解耦,但同时也引入了新的挑战,其中最关键的就是用户认证和会话管理。本文将深入分析如何通过双Token机制(Access Token + Refresh Token)实现前后端分离架构下的无缝用户体验和安全的访问控制。

1. 前后端分离架构的认证挑战

传统架构 vs 前后端分离架构

传统架构的认证方式:

  • 基于Cookie和Session的状态管理
  • 服务器端维护用户会话状态
  • 依赖浏览器的Cookie机制
  • 跨域问题相对简单

前后端分离架构面临的挑战:

  1. 无状态性要求:RESTful API设计原则要求服务无状态
  2. 跨域问题:前后端部署在不同域名,Cookie传递受限
  3. 多端支持:需要同时支持Web、移动端、桌面应用
  4. 微服务架构:多个服务间的认证状态共享
  5. 安全性要求:防范XSS、CSRF等攻击

为什么需要Token机制?

Token机制相比传统Session具有以下优势:

  • 无状态:服务器不需要存储用户会话信息
  • 可扩展性:易于实现负载均衡和水平扩展
  • 跨域友好:不依赖Cookie,可通过HTTP Header传递
  • 移动端友好:原生应用无Cookie概念,Token更适合
  • 微服务支持:Token可在多个服务间共享和验证

2. 传统Session vs Token认证

Session认证机制

markdown 复制代码
用户登录 → 服务器创建Session → 返回SessionID → 客户端存储SessionID
                ↓
客户端请求 → 携带SessionID → 服务器验证Session → 返回响应

Session机制的特点:

  • 服务器端存储用户状态
  • 依赖Cookie或URL参数传递SessionID
  • 简单直观,但扩展性有限

Token认证机制

markdown 复制代码
用户登录 → 服务器生成Token → 返回Token → 客户端存储Token
               ↓
客户端请求 → 携带Token → 服务器验证Token → 返回响应

Token机制的特点:

  • 无状态,用户信息编码在Token中
  • 通过HTTP Header传递
  • 支持分布式部署

3. 双Token机制核心概念

Access Token(访问令牌)

作用和特点:

  • 用于访问受保护的API资源
  • 生命周期短(通常15-30分钟)
  • 包含用户身份和权限信息
  • 频繁使用,安全风险相对较高

JWT结构示例:

json 复制代码
{
  "header": {
    "alg": "HS256",
    "typ": "JWT"
  },
  "payload": {
    "sub": "user123",
    "name": "张三",
    "roles": ["user", "admin"],
    "exp": 1640995200,
    "iat": 1640991600
  }
}

Refresh Token(刷新令牌)

作用和特点:

  • 用于获取新的Access Token
  • 生命周期长(通常7-30天)
  • 使用频率低,安全风险相对较小
  • 可以被撤销,提供更好的安全控制

设计考虑:

  • 存储方式:数据库记录,支持撤销
  • 使用限制:一次性使用或限制使用次数
  • 安全性:加密存储,包含设备指纹等信息

双Token机制的优势

  1. 安全性平衡:短期Access Token降低泄露风险,长期Refresh Token减少用户重新登录
  2. 用户体验:无感知的Token续期,避免频繁登录
  3. 精细控制:可以撤销Refresh Token实现强制登出
  4. 性能优化:Access Token验证无需数据库查询,Refresh Token验证频率低

4. 完整的认证流程设计

1. 用户登录流程

sequenceDiagram participant U as 用户 participant F as 前端应用 participant A as 认证服务 participant D as 数据库 U->>F: 输入用户名密码 F->>A: POST /api/auth/login A->>D: 验证用户凭据 D-->>A: 返回用户信息 A->>A: 生成Access Token A->>A: 生成Refresh Token A->>D: 存储Refresh Token A-->>F: 返回双Token F->>F: 存储Token F-->>U: 登录成功

2. API访问流程

sequenceDiagram participant F as 前端应用 participant A as API服务 participant Auth as 认证服务 F->>A: API请求 + Access Token A->>Auth: 验证Access Token Auth-->>A: Token有效 A-->>F: 返回API数据

3. Token刷新流程

sequenceDiagram participant F as 前端应用 participant A as API服务 participant Auth as 认证服务 participant D as 数据库 F->>A: API请求 + 过期Access Token A->>Auth: 验证Access Token Auth-->>A: Token已过期 A-->>F: 401 Unauthorized F->>Auth: POST /api/auth/refresh + Refresh Token Auth->>D: 验证Refresh Token D-->>Auth: Token有效 Auth->>Auth: 生成新Access Token Auth->>D: 更新/轮换Refresh Token Auth-->>F: 返回新双Token F->>A: 重试API请求 + 新Access Token A-->>F: 返回API数据

5.技术实现方案

技术栈

  • 前端: Vue3 + TypeScript + Pinia + Axios
  • 后端: Spring Boot 3 + Spring Security 6 + JWT
  • 架构模式: 前后端分离 + RESTful API

5.1 后端实现

1. 依赖配置

xml 复制代码
<!-- pom.xml -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-api</artifactId>
    <version>0.11.5</version>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-impl</artifactId>
    <version>0.11.5</version>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-jackson</artifactId>
    <version>0.11.5</version>
</dependency>

2. JWT工具类

java 复制代码
/**
 * JWT Token提供者工具类
 * 负责生成、验证和解析JWT Token
 * 支持Access Token和Refresh Token的双Token机制
 */
@Component
public class JwtTokenProvider {
    
    /**
     * JWT签名密钥,从配置文件读取
     * 建议使用256位以上的强密钥
     */
    @Value("${jwt.secret}")
    private String jwtSecret;
    
    /**
     * Access Token有效期(毫秒)
     * 推荐设置为15-30分钟,平衡安全性和用户体验
     */
    @Value("${jwt.access-token-validity}")
    private long accessTokenValidity;
    
    /**
     * Refresh Token有效期(毫秒)
     * 推荐设置为7-30天,用于长期免登录
     */
    @Value("${jwt.refresh-token-validity}")
    private long refreshTokenValidity;
    
    /**
     * 生成Access Token
     * Access Token用于API访问,包含用户名和角色信息,有效期较短
     * 
     * @param username 用户名
     * @param roles 用户角色列表
     * @return 生成的Access Token字符串
     */
    public String generateAccessToken(String username, List<String> roles) {
        return Jwts.builder()
                .setSubject(username)                    // 设置主题(用户名)
                .claim("roles", roles)                   // 添加角色信息到载荷
                .setIssuedAt(new Date())                 // 设置签发时间
                .setExpiration(new Date(System.currentTimeMillis() + accessTokenValidity)) // 设置过期时间
                .signWith(getSigningKey(), SignatureAlgorithm.HS256)  // 使用HS256算法签名
                .compact();                              // 生成最终的JWT字符串
    }
    
    /**
     * 生成Refresh Token
     * Refresh Token用于刷新Access Token,不包含敏感信息,有效期较长
     * 
     * @param username 用户名
     * @return 生成的Refresh Token字符串
     */
    public String generateRefreshToken(String username) {
        return Jwts.builder()
                .setSubject(username)                    // 设置主题(用户名)
                .setIssuedAt(new Date())                 // 设置签发时间
                .setExpiration(new Date(System.currentTimeMillis() + refreshTokenValidity)) // 设置过期时间
                .signWith(getSigningKey(), SignatureAlgorithm.HS256)  // 使用HS256算法签名
                .compact();                              // 生成最终的JWT字符串
    }
    
    /**
     * 验证Token有效性
     * 检查Token的签名、格式和过期时间
     * 
     * @param token 待验证的JWT Token
     * @return true表示Token有效,false表示无效
     */
    public boolean validateToken(String token) {
        try {
            Jwts.parserBuilder()
                .setSigningKey(getSigningKey())          // 设置验证密钥
                .build()
                .parseClaimsJws(token);                  // 解析并验证Token
            return true;
        } catch (JwtException | IllegalArgumentException e) {
            // Token格式错误、签名不匹配或已过期
            return false;
        }
    }
    
    /**
     * 从Token中提取用户名
     * 
     * @param token JWT Token
     * @return 用户名
     */
    public String getUsernameFromToken(String token) {
        return Jwts.parserBuilder()
                .setSigningKey(getSigningKey())          // 设置验证密钥
                .build()
                .parseClaimsJws(token)                   // 解析Token
                .getBody()                               // 获取载荷部分
                .getSubject();                           // 获取主题(用户名)
    }
    
    /**
     * 获取签名密钥
     * 将Base64编码的密钥转换为HMAC密钥对象
     * 
     * @return HMAC签名密钥
     */
    private Key getSigningKey() {
        byte[] keyBytes = Decoders.BASE64.decode(jwtSecret);  // 解码Base64密钥
        return Keys.hmacShaKeyFor(keyBytes);                  // 创建HMAC密钥
    }
}

3. 认证过滤器

java 复制代码
/**
 * JWT认证过滤器
 * 继承OncePerRequestFilter确保每个请求只执行一次
 * 负责从请求中提取JWT Token并验证用户身份
 */
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
    
    /**
     * JWT Token工具类,用于Token的验证和解析
     */
    @Autowired
    private JwtTokenProvider jwtTokenProvider;
    
    /**
     * 用户详情服务,用于加载用户信息和权限
     */
    @Autowired
    private UserDetailsService userDetailsService;
    
    /**
     * 过滤器核心方法
     * 在每个HTTP请求处理前执行,验证JWT Token并设置认证上下文
     * 
     * @param request HTTP请求对象
     * @param response HTTP响应对象
     * @param filterChain 过滤器链,用于传递请求到下一个过滤器
     */
    @Override
    protected void doFilterInternal(HttpServletRequest request, 
                                  HttpServletResponse response, 
                                  FilterChain filterChain) throws ServletException, IOException {
        
        // 从请求头中提取Access Token
        String accessToken = getAccessTokenFromRequest(request);
        
        // 验证Token是否存在且有效
        if (StringUtils.hasText(accessToken) && jwtTokenProvider.validateToken(accessToken)) {
            // 从Token中解析用户名
            String username = jwtTokenProvider.getUsernameFromToken(accessToken);
            
            // 根据用户名加载用户详情(包括权限信息)
            UserDetails userDetails = userDetailsService.loadUserByUsername(username);
            
            // 创建认证对象,包含用户信息和权限
            UsernamePasswordAuthenticationToken authentication = 
                new UsernamePasswordAuthenticationToken(
                    userDetails,                    // 认证主体(用户信息)
                    null,                          // 凭据(密码),JWT认证不需要
                    userDetails.getAuthorities()   // 用户权限列表
                );
            
            // 将认证信息设置到Spring Security上下文中
            // 后续的请求处理过程中可以通过SecurityContextHolder获取当前用户信息
            SecurityContextHolder.getContext().setAuthentication(authentication);
        }
        
        // 继续执行过滤器链,处理请求
        filterChain.doFilter(request, response);
    }
    
    /**
     * 从HTTP请求头中提取Access Token
     * 标准的JWT Token通过Authorization头传递,格式为"Bearer <token>"
     * 
     * @param request HTTP请求对象
     * @return 提取的Token字符串,如果不存在则返回null
     */
    private String getAccessTokenFromRequest(HttpServletRequest request) {
        // 获取Authorization请求头
        String bearerToken = request.getHeader("Authorization");
        
        // 检查请求头是否存在且以"Bearer "开头
        if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
            // 提取Token部分(去掉"Bearer "前缀)
            return bearerToken.substring(7);
        }
        
        // 请求头格式不正确或不存在
        return null;
    }
}

4. 认证控制器

java 复制代码
/**
 * 认证控制器
 * 处理用户登录、Token刷新、登出等认证相关的API请求
 * 实现双Token认证机制的核心业务逻辑
 */
@RestController
@RequestMapping("/api/auth")
@CrossOrigin(origins = "${app.cors.allowed-origins}")  // 配置跨域访问
public class AuthController {
    
    /**
     * Spring Security认证管理器
     * 用于验证用户凭据(用户名和密码)
     */
    @Autowired
    private AuthenticationManager authenticationManager;
    
    /**
     * JWT Token工具类
     * 用于生成和验证Access Token和Refresh Token
     */
    @Autowired
    private JwtTokenProvider jwtTokenProvider;
    
    /**
     * Refresh Token服务
     * 管理Refresh Token的存储、验证和撤销
     */
    @Autowired
    private RefreshTokenService refreshTokenService;
    
    /**
     * 用户登录接口
     * 验证用户凭据并返回双Token
     * 
     * @param loginRequest 登录请求对象,包含用户名和密码
     * @return 包含Access Token和Refresh Token的响应
     */
    @PostMapping("/login")
    public ResponseEntity<AuthResponse> login(@RequestBody LoginRequest loginRequest) {
        try {
            // 使用Spring Security的认证管理器验证用户凭据
            Authentication authentication = authenticationManager.authenticate(
                new UsernamePasswordAuthenticationToken(
                    loginRequest.getUsername(),     // 用户名
                    loginRequest.getPassword()      // 密码
                )
            );
            
            // 将认证信息设置到安全上下文中
            SecurityContextHolder.getContext().setAuthentication(authentication);
            
            // 生成Access Token,包含用户名和角色信息
            String accessToken = jwtTokenProvider.generateAccessToken(
                loginRequest.getUsername(), 
                getRoles(authentication)            // 从认证对象中提取用户角色
            );
            
            // 生成Refresh Token,仅包含用户名
            String refreshToken = jwtTokenProvider.generateRefreshToken(loginRequest.getUsername());
            
            // 将Refresh Token存储到数据库或Redis中,用于后续验证
            // 这样可以实现Refresh Token的撤销功能
            refreshTokenService.saveRefreshToken(loginRequest.getUsername(), refreshToken);
            
            // 返回成功响应,包含双Token
            return ResponseEntity.ok(new AuthResponse(accessToken, refreshToken));
            
        } catch (AuthenticationException e) {
            // 认证失败,返回401未授权状态
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
                .body(new AuthResponse("Invalid credentials"));
        }
    }
    
    /**
     * Token刷新接口
     * 使用Refresh Token获取新的Access Token
     * 
     * @param request 刷新请求对象,包含Refresh Token
     * @return 包含新Access Token的响应
     */
    @PostMapping("/refresh")
    public ResponseEntity<AuthResponse> refresh(@RequestBody RefreshTokenRequest request) {
        try {
            // 验证Refresh Token的格式和有效期
            if (jwtTokenProvider.validateToken(request.getRefreshToken())) {
                // 从Refresh Token中提取用户名
                String username = jwtTokenProvider.getUsernameFromToken(request.getRefreshToken());
                
                // 验证Refresh Token是否在服务端存储中(防止被撤销的Token被重复使用)
                if (refreshTokenService.validateRefreshToken(username, request.getRefreshToken())) {
                    // 生成新的Access Token
                    String newAccessToken = jwtTokenProvider.generateAccessToken(
                        username, 
                        getRoles(username)          // 重新获取用户最新的角色信息
                    );
                    
                    // 返回新的Access Token,不返回新的Refresh Token
                    return ResponseEntity.ok(new AuthResponse(newAccessToken, null));
                }
            }
            
            // Refresh Token无效,返回401未授权状态
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
                .body(new AuthResponse("Invalid refresh token"));
            
        } catch (Exception e) {
            // Token刷新过程中发生异常,返回401未授权状态
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
                .body(new AuthResponse("Token refresh failed"));
        }
    }
    
    /**
     * 用户登出接口
     * 撤销用户的Refresh Token,实现安全登出
     * 
     * @param request 登出请求对象,包含用户名
     * @return 空响应体,状态码200表示成功
     */
    @PostMapping("/logout")
    public ResponseEntity<Void> logout(@RequestBody LogoutRequest request) {
        // 从服务端存储中撤销Refresh Token
        // 这样即使客户端仍然持有Token,也无法再次使用
        refreshTokenService.revokeRefreshToken(request.getUsername());
        
        // 返回成功状态
        return ResponseEntity.ok().build();
    }
    
    /**
     * 从认证对象中提取用户角色列表
     * 
     * @param authentication Spring Security认证对象
     * @return 角色名称列表
     */
    private List<String> getRoles(Authentication authentication) {
        return authentication.getAuthorities().stream()
                .map(GrantedAuthority::getAuthority)    // 获取权限名称
                .collect(Collectors.toList());          // 收集到列表中
    }
    
    /**
     * 根据用户名获取用户角色列表
     * 用于Token刷新时重新获取最新的用户权限
     * 
     * @param username 用户名
     * @return 角色名称列表
     */
    private List<String> getRoles(String username) {
        // 从用户服务获取用户角色
        // 这里可以从数据库、缓存或其他用户服务中获取
        return userService.getUserRoles(username);
    }
}

5. Spring Security配置

java 复制代码
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {
    
    @Autowired
    private JwtAuthenticationFilter jwtAuthFilter;
    
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .csrf(csrf -> csrf.disable())
            .cors(cors -> cors.configurationSource(corsConfigurationSource()))
            .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/auth/**").permitAll()
                .requestMatchers("/api/public/**").permitAll()
                .anyRequest().authenticated()
            )
            .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);
        
        return http.build();
    }
    
    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration configuration = new CorsConfiguration();
        configuration.setAllowedOrigins(Arrays.asList("http://localhost:3000", "http://localhost:8080"));
        configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
        configuration.setAllowedHeaders(Arrays.asList("*"));
        configuration.setAllowCredentials(true);
        
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);
        return source;
    }
    
    @Bean
    public AuthenticationManager authenticationManager(
            UserDetailsService userDetailsService, 
            PasswordEncoder passwordEncoder) {
        
        DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
        provider.setUserDetailsService(userDetailsService);
        provider.setPasswordEncoder(passwordEncoder);
        return new ProviderManager(provider);
    }
    
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

5.2 前端实现

1. 依赖安装

bash 复制代码
npm install axios pinia @vueuse/core

2. 认证状态管理

typescript 复制代码
// stores/auth.ts
/**
 * 认证状态管理 Store
 * 使用 Pinia 管理用户认证状态和双Token机制
 * 提供登录、登出、Token刷新等核心功能
 */
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import { authApi } from '@/api/auth'
import { useRouter } from 'vue-router'

/**
 * 用户信息接口定义
 */
export interface User {
  id: string          // 用户ID
  username: string    // 用户名
  email: string       // 邮箱
  roles: string[]     // 用户角色列表
}

/**
 * 认证Token接口定义
 */
export interface AuthTokens {
  accessToken: string   // 访问令牌,用于API请求认证
  refreshToken: string  // 刷新令牌,用于获取新的访问令牌
}

/**
 * 认证状态管理Store
 * 使用 Composition API 风格的 Pinia Store
 */
export const useAuthStore = defineStore('auth', () => {
  // ==================== 响应式状态 ====================
  
  /** 当前登录用户信息 */
  const user = ref<User | null>(null)
  
  /** Access Token,存储在内存中确保安全性 */
  const accessToken = ref<string | null>(null)
  
  /** Refresh Token,可选择存储在localStorage或HttpOnly Cookie */
  const refreshToken = ref<string | null>(null)
  
  /** 计算属性:用户是否已认证 */
  const isAuthenticated = computed(() => !!accessToken.value)
  
  // ==================== 依赖注入 ====================
  
  /** Vue Router实例,用于页面跳转 */
  const router = useRouter()
  
  // ==================== 认证状态管理方法 ====================
  
  /**
   * 初始化认证状态
   * 应用启动时调用,从本地存储恢复认证状态
   * 自动验证Token有效性并在必要时刷新
   */
  const initAuth = () => {
    // 从本地存储读取Token
    const storedAccessToken = localStorage.getItem('accessToken')
    const storedRefreshToken = localStorage.getItem('refreshToken')
    
    // 如果存在双Token,恢复认证状态
    if (storedAccessToken && storedRefreshToken) {
      accessToken.value = storedAccessToken
      refreshToken.value = storedRefreshToken
      
      // 验证Token有效性,如果即将过期则自动刷新
      validateAndRefreshToken()
    }
  }
  
  /**
   * 用户登录方法
   * 发送用户凭据到服务端,获取双Token并保存认证状态
   * 
   * @param username 用户名
   * @param password 密码
   * @returns 登录结果,包含成功状态和错误信息
   */
  const login = async (username: string, password: string) => {
    try {
      // 调用登录API
      const response = await authApi.login(username, password)
      const { accessToken: newAccessToken, refreshToken: newRefreshToken } = response.data
      
      // 更新内存中的Token状态
      accessToken.value = newAccessToken
      refreshToken.value = newRefreshToken
      
      // 持久化存储Token(注意:生产环境建议Refresh Token存储在HttpOnly Cookie中)
      localStorage.setItem('accessToken', newAccessToken)
      localStorage.setItem('refreshToken', newRefreshToken)
      
      // 获取当前用户详细信息
      await fetchUserInfo()
      
      // 登录成功后跳转到仪表板
      router.push('/dashboard')
      return { success: true }
      
    } catch (error: any) {
      // 登录失败,返回错误信息
      return { 
        success: false, 
        error: error.response?.data?.message || '登录失败' 
      }
    }
  }
  
  /**
   * 用户登出方法
   * 撤销服务端的Refresh Token并清除本地认证状态
   */
  const logout = async () => {
    try {
      // 如果存在Refresh Token,通知服务端撤销
      if (refreshToken.value) {
        await authApi.logout(user.value?.username || '')
      }
    } catch (error) {
      // 登出API调用失败不影响本地清理
      console.error('Logout error:', error)
    } finally {
      // 无论服务端调用是否成功,都要清除本地认证状态
      clearAuth()
      // 跳转到登录页面
      router.push('/login')
    }
  }
  
  /**
   * 清除本地认证信息
   * 重置所有认证相关的状态和本地存储
   */
  const clearAuth = () => {
    // 清除内存中的用户和Token信息
    user.value = null
    accessToken.value = null
    refreshToken.value = null
    
    // 清除本地存储中的Token
    localStorage.removeItem('accessToken')
    localStorage.removeItem('refreshToken')
  }
  
  /**
   * 刷新Access Token
   * 使用Refresh Token获取新的Access Token,实现无感知的Token续期
   * 
   * @returns 新的Access Token
   * @throws 如果刷新失败则抛出异常并清除认证状态
   */
  const refreshAccessToken = async () => {
    try {
      // 检查是否存在Refresh Token
      if (!refreshToken.value) {
        throw new Error('No refresh token available')
      }
      
      // 调用Token刷新API
      const response = await authApi.refresh(refreshToken.value)
      const { accessToken: newAccessToken } = response.data
      
      // 更新内存和本地存储中的Access Token
      accessToken.value = newAccessToken
      localStorage.setItem('accessToken', newAccessToken)
      
      return newAccessToken
      
    } catch (error) {
      // Token刷新失败,清除认证状态并跳转登录页
      clearAuth()
      router.push('/login')
      throw error
    }
  }
  
  /**
   * 验证Token有效性并在必要时刷新
   * 检查Access Token是否即将过期,如果是则自动刷新
   * 实现用户无感知的Token续期机制
   * 
   * @returns 验证结果,true表示Token有效或刷新成功
   */
  const validateAndRefreshToken = async () => {
    // 如果没有Access Token,直接返回false
    if (!accessToken.value) return false
    
    try {
      // 解析JWT Token的载荷部分(Base64解码)
      const tokenData = JSON.parse(atob(accessToken.value.split('.')[1]))
      const expirationTime = tokenData.exp * 1000  // JWT中的exp是秒,转换为毫秒
      const currentTime = Date.now()
      
      // 如果Token将在5分钟内过期,提前刷新
      // 这样可以避免在用户操作过程中突然失效
      if (expirationTime - currentTime < 5 * 60 * 1000) {
        await refreshAccessToken()
      }
      
      return true
      
    } catch (error) {
      // Token解析失败或刷新失败,清除认证状态
      clearAuth()
      return false
    }
  }
  
  /**
   * 获取当前用户详细信息
   * 登录成功后调用,从服务端获取用户的完整信息
   */
  const fetchUserInfo = async () => {
    try {
      // 调用获取用户信息API
      const response = await authApi.getUserInfo()
      user.value = response.data
      
    } catch (error) {
      // 获取用户信息失败,记录错误但不影响认证状态
      console.error('Failed to fetch user info:', error)
    }
  }
  
  return {
    user,
    accessToken,
    refreshToken,
    isAuthenticated,
    login,
    logout,
    initAuth,
    refreshAccessToken,
    validateAndRefreshToken
  }
})

3. API配置

typescript 复制代码
// api/auth.ts
/**
 * 认证相关API配置
 * 配置axios实例,实现自动Token注入和刷新机制
 * 提供完整的双Token认证支持
 */
import axios from 'axios'
import { useAuthStore } from '@/stores/auth'

/**
 * 创建专用的axios实例
 * 避免与其他API请求产生冲突,提供独立的配置
 */
const api = axios.create({
  // API基础URL,优先使用环境变量配置
  baseURL: import.meta.env.VITE_API_BASE_URL || 'http://localhost:8080/api',
  
  // 请求超时时间(10秒)
  timeout: 10000,
  
  // 默认请求头
  headers: {
    'Content-Type': 'application/json'
  }
})

/**
 * 请求拦截器
 * 在每个请求发送前自动添加Authorization头
 * 实现Token的自动注入机制
 */
api.interceptors.request.use(
  (config) => {
    // 获取认证Store实例
    const authStore = useAuthStore()
    
    // 如果存在Access Token,自动添加到请求头
    if (authStore.accessToken) {
      config.headers.Authorization = `Bearer ${authStore.accessToken}`
    }
    
    return config
  },
  (error) => {
    // 请求配置错误,直接拒绝
    return Promise.reject(error)
  }
)

/**
 * 响应拦截器
 * 处理401未授权响应,实现自动Token刷新机制
 * 确保用户在Token过期时能够无感知地继续使用应用
 */
api.interceptors.response.use(
  // 响应成功,直接返回
  (response) => response,
  
  // 响应错误处理
  async (error) => {
    const originalRequest = error.config
    
    // 检查是否是401未授权错误,且该请求未曾重试过
    if (error.response?.status === 401 && !originalRequest._retry) {
      // 标记该请求已重试,避免无限循环
      originalRequest._retry = true
      
      const authStore = useAuthStore()
      
      try {
        // 尝试刷新Access Token
        await authStore.refreshAccessToken()
        
        // Token刷新成功,重新发送原始请求
        // 新的Access Token会通过请求拦截器自动添加
        return api(originalRequest)
        
      } catch (refreshError) {
        // Token刷新失败,说明Refresh Token也已过期
        // 清除认证状态并跳转到登录页面
        authStore.clearAuth()
        window.location.href = '/login'
        return Promise.reject(refreshError)
      }
    }
    
    // 其他错误或已重试过的401错误,直接拒绝
    return Promise.reject(error)
  }
)

/**
 * 认证相关API方法集合
 * 提供登录、Token刷新、登出等认证功能的API调用
 */
export const authApi = {
  /**
   * 用户登录API
   * @param username 用户名
   * @param password 密码
   * @returns Promise<AxiosResponse> 包含双Token的响应
   */
  login: (username: string, password: string) =>
    api.post('/auth/login', { username, password }),
  
  /**
   * 刷新Token API
   * @param refreshToken 有效的Refresh Token
   * @returns Promise<AxiosResponse> 包含新Access Token的响应
   */
  refresh: (refreshToken: string) =>
    api.post('/auth/refresh', { refreshToken }),
  
  /**
   * 用户登出API
   * @param username 用户名
   * @returns Promise<AxiosResponse> 登出确认响应
   */
  logout: (username: string) =>
    api.post('/auth/logout', { username }),
  
  /**
   * 获取用户信息API
   * @returns Promise<AxiosResponse> 包含用户详细信息的响应
   */
  getUserInfo: () => api.get('/auth/user-info')
}

// 导出axios实例,供其他模块使用
export default api

4. 路由守卫

typescript 复制代码
// router/index.ts
/**
 * Vue Router 配置
 * 实现基于认证状态的路由守卫机制
 * 自动处理Token验证和页面跳转逻辑
 */
import { createRouter, createWebHistory } from 'vue-router'
import { useAuthStore } from '@/stores/auth'

/**
 * 路由配置
 * 定义应用的页面路由和访问权限
 */
const routes = [
  {
    path: '/login',
    name: 'Login',
    component: () => import('@/views/Login.vue'),
    meta: { requiresAuth: false }  // 登录页不需要认证
  },
  {
    path: '/dashboard',
    name: 'Dashboard',
    component: () => import('@/views/Dashboard.vue'),
    meta: { requiresAuth: true }   // 仪表板需要认证
  },
  {
    path: '/',
    redirect: '/dashboard'         // 根路径重定向到仪表板
  }
]

/**
 * 创建路由实例
 * 使用HTML5 History模式,提供干净的URL
 */
const router = createRouter({
  history: createWebHistory(),
  routes
})

/**
 * 全局前置守卫
 * 在每次路由跳转前执行,实现认证检查和自动跳转
 * 
 * @param to 目标路由对象
 * @param from 当前路由对象  
 * @param next 继续执行的回调函数
 */
router.beforeEach(async (to, from, next) => {
  // 获取认证状态管理实例
  const authStore = useAuthStore()
  
  // 检查目标页面是否需要认证
  if (to.meta.requiresAuth) {
    // 如果用户未登录,跳转到登录页
    if (!authStore.isAuthenticated) {
      next('/login')
      return
    }
    
    // 验证Token有效性,如果即将过期则自动刷新
    const isValid = await authStore.validateAndRefreshToken()
    if (!isValid) {
      // Token无效或刷新失败,跳转到登录页
      next('/login')
      return
    }
  }
  
  // 如果用户已登录但访问登录页,重定向到仪表板
  if (to.path === '/login' && authStore.isAuthenticated) {
    next('/dashboard')
    return
  }
  
  // 通过所有检查,继续导航
  next()
})

export default router

5. 登录组件

vue 复制代码
<!-- views/Login.vue -->
<template>
  <div class="login-container">
    <div class="login-card">
      <h2>用户登录</h2>
      <form @submit.prevent="handleLogin" class="login-form">
        <div class="form-group">
          <label for="username">用户名</label>
          <input
            id="username"
            v-model="form.username"
            type="text"
            required
            placeholder="请输入用户名"
          />
        </div>
        
        <div class="form-group">
          <label for="password">密码</label>
          <input
            id="password"
            v-model="form.password"
            type="password"
            required
            placeholder="请输入密码"
          />
        </div>
        
        <button type="submit" :disabled="loading" class="login-btn">
          {{ loading ? '登录中...' : '登录' }}
        </button>
        
        <div v-if="error" class="error-message">
          {{ error }}
        </div>
      </form>
    </div>
  </div>
</template>

<script setup lang="ts">
/**
 * 登录组件逻辑
 * 实现用户登录功能,集成双Token认证机制
 * 提供用户友好的登录界面和错误处理
 */
import { ref, reactive } from 'vue'
import { useAuthStore } from '@/stores/auth'

// 获取认证状态管理实例
const authStore = useAuthStore()

/**
 * 登录表单数据
 * 使用reactive创建响应式对象,自动跟踪表单变化
 */
const form = reactive({
  username: '',  // 用户名输入
  password: ''   // 密码输入
})

/** 登录加载状态,用于显示加载指示器 */
const loading = ref(false)

/** 错误信息,用于显示登录失败的原因 */
const error = ref('')

/**
 * 处理登录提交
 * 调用认证Store的登录方法,处理成功和失败情况
 */
const handleLogin = async () => {
  // 设置加载状态,禁用提交按钮
  loading.value = true
  // 清除之前的错误信息
  error.value = ''
  
  try {
    // 调用登录方法,传入用户名和密码
    const result = await authStore.login(form.username, form.password)
    
    // 检查登录结果
    if (!result.success) {
      // 登录失败,显示错误信息
      error.value = result.error || '登录失败'
    }
    // 登录成功的情况由authStore内部处理(自动跳转)
    
  } catch (err) {
    // 捕获未预期的错误
    error.value = '登录失败,请重试'
    console.error('Login error:', err)
    
  } finally {
    // 无论成功失败,都要取消加载状态
    loading.value = false
  }
}
</script>

<style scoped>
.login-container {
  display: flex;
  justify-content: center;
  align-items: center;
  min-height: 100vh;
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}

.login-card {
  background: white;
  padding: 2rem;
  border-radius: 8px;
  box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
  width: 100%;
  max-width: 400px;
}

.login-form {
  display: flex;
  flex-direction: column;
  gap: 1rem;
}

.form-group {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}

.form-group label {
  font-weight: 500;
  color: #374151;
}

.form-group input {
  padding: 0.75rem;
  border: 1px solid #d1d5db;
  border-radius: 4px;
  font-size: 1rem;
}

.login-btn {
  background: #3b82f6;
  color: white;
  padding: 0.75rem;
  border: none;
  border-radius: 4px;
  font-size: 1rem;
  cursor: pointer;
  transition: background-color 0.2s;
}

.login-btn:hover:not(:disabled) {
  background: #2563eb;
}

.login-btn:disabled {
  background: #9ca3af;
  cursor: not-allowed;
}

.error-message {
  color: #dc2626;
  text-align: center;
  font-size: 0.875rem;
}
</style>

5.3 安全机制

1. Token安全存储

  • Access Token: 存储在内存中,避免XSS攻击
  • Refresh Token: 存储在HttpOnly Cookie中,防止JavaScript访问
  • Token轮换: 每次刷新都生成新的Refresh Token

2. 攻击防护

  • XSS防护: 使用HttpOnly Cookie存储Refresh Token
  • CSRF防护: 验证Origin和Referer头
  • Token劫持: 实现Token黑名单机制
  • 重放攻击: 使用JWT的iat和exp字段

3. 安全配置

java 复制代码
@Configuration
public class SecurityConfig {
    
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .csrf(csrf -> csrf.disable()) // 前后端分离,禁用CSRF
            .cors(cors -> cors.configurationSource(corsConfigurationSource()))
            .sessionManagement(session -> 
                session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            )
            .headers(headers -> headers
                .frameOptions().deny() // 防止点击劫持
                .contentTypeOptions().and() // 防止MIME类型嗅探
                .httpStrictTransportSecurity(hsts -> hsts
                    .maxAgeInSeconds(31536000)
                    .includeSubdomains(true)
                )
            );
        
        return http.build();
    }
}

5.4 最佳实践

1. Token生命周期管理

  • Access Token: 15-30分钟
  • Refresh Token: 7-30天
  • 实现Token自动刷新机制
  • 支持多设备登录管理

2. 错误处理

  • 统一的错误响应格式
  • 详细的错误日志记录
  • 用户友好的错误提示

3. 性能优化

  • 使用Redis缓存Refresh Token
  • 实现Token预刷新机制
  • 减少不必要的Token验证

4. 监控和日志

  • Token使用统计
  • 异常登录检测
  • 安全事件记录

5.5 完整示例

项目结构

css 复制代码
src/
├── main/
│   ├── java/
│   │   └── com/example/auth/
│   │       ├── config/
│   │       ├── controller/
│   │       ├── filter/
│   │       ├── service/
│   │       └── util/
│   └── resources/
│       └── application.yml
├── frontend/
│   ├── src/
│   │   ├── api/
│   │   ├── components/
│   │   ├── router/
│   │   ├── stores/
│   │   └── views/
│   ├── package.json
│   └── vite.config.ts
└── README.md

配置文件

yaml 复制代码
# application.yml
jwt:
  secret: your-256-bit-secret-key-here
  access-token-validity: 900000  # 15分钟
  refresh-token-validity: 604800000  # 7天

app:
  cors:
    allowed-origins: http://localhost:3000,http://localhost:8080

spring:
  security:
    user:
      name: admin
      password: admin

启动说明

  1. 后端启动: ./mvnw spring-boot:run
  2. 前端启动: npm run dev
  3. 访问: http://localhost:3000

6.总结

通过本文的深入分析,我们全面了解了前后端分离架构下双Token认证机制的设计和实现。这套方案不仅解决了传统Session机制在分布式环境下的局限性,还通过精心设计的安全策略和最佳实践,确保了用户体验和系统安全的平衡。

关键要点回顾:

  1. 双Token机制的核心价值:短期Access Token保证安全性,长期Refresh Token保证用户体验
  2. 完整的实现方案:从后端JWT生成验证到前端自动刷新机制
  3. 安全性保障:Token轮换、设备指纹、异常监控等多层防护
  4. 性能优化:预刷新、离线处理、多端同步等用户体验优化

未来发展方向:

  • 零信任安全架构:结合设备信任度和行为分析
  • 无密码认证:WebAuthn、生物识别等新技术
  • 联邦身份认证:SSO和OAuth 2.0扩展
  • 边缘计算支持:CDN级别的Token验证

双Token认证机制作为现代Web应用的基础设施,将继续在安全性、可扩展性和用户体验之间寻找最佳平衡点。随着技术的发展,我们也需要持续关注新的安全威胁和解决方案,确保认证系统的持续演进。

相关推荐
Cyanto40 分钟前
Vue浅学
前端·javascript·vue.js
三年呀1 小时前
**超融合架构中的发散创新:探索现代编程语言的挑战与机遇**一、引言随着数字化时代的快速发展,超融合架构已成为IT领域的一种重要趋势
python·架构
Q_Q19632884751 小时前
python基于Hadoop的超市数据分析系统
开发语言·hadoop·spring boot·python·django·flask·node.js
小乌龟不会飞1 小时前
【SpringBoot】统一功能处理
java·spring boot·后端
考虑考虑2 小时前
JPA中的EntityGraph
spring boot·后端·spring
天生我材必有用_吴用2 小时前
一文搞懂 useDark:Vue 项目中实现深色模式的正确姿势
前端·vue.js
Bug生产工厂3 小时前
智能客服对接支付系统:用 AI 对话生成查询代码的全链路实现
架构
ruokkk3 小时前
一个困扰我多年的Session超时Bug,被我的新AI搭档半天搞定了
javascript·后端·架构
复苏季风3 小时前
v-for什么时候使用index,什么是时候用uuid当key
前端·vue.js