1. JWT

JWT(JSON Web Token)是一种开放标准(RFC 7519),用于在网络应用环境间安全地将信息作为 JSON 对象传输。JWT 是目前最流行的跨域认证解决方案,特别适合前后端分离的架构。

1.1 JWT 的结构

JWT 由三部分组成,用点号(.)分隔:

xxxxx.yyyyy.zzzzz
│      │      │
│      │      └── Signature(签名)
│      └───────── Payload(负载)
└──────────────── Header(头部)

Header:包含令牌类型和签名算法

{
  "alg": "HS256",
  "typ": "JWT"
}

Payload:包含声明(claims),如用户信息、过期时间等

{
  "username": "zhangsan",
  "iat": 1516239022,
  "exp": 1516242622
}

Signature:使用密钥对前两部分进行签名,确保令牌不被篡改

1.2 JWT 的优势

优势 说明
无状态 服务器不需要保存会话信息,令牌自包含用户信息
跨域支持 天然支持 CORS,适合前后端分离架构
扩展性强 令牌中可以携带自定义信息
安全性高 使用签名防止篡改,支持过期时间

2. 项目架构

┌─────────────────────────────────────────────────────────────┐
│                         客户端 (Vue 3)                       │
│  ┌──────────────┐  ┌──────────────┐  ┌────────────────────┐ │
│  │   登录请求   │  │  存储 Token  │  │  请求携带 Token    │ │
│  └──────────────┘  └──────────────┘  └────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
                              │
                              │ HTTP/REST
                              ▼
┌─────────────────────────────────────────────────────────────┐
│                      服务端 (Spring Boot)                    │
│  ┌──────────────┐  ┌──────────────┐  ┌────────────────────┐ │
│  │ 生成 Token   │  │  验证 Token  │  │  授权访问资源      │ │
│  │ (JwtUtil)    │  │  (JwtFilter) │  │  (SecurityConfig)  │ │
│  └──────────────┘  └──────────────┘  └────────────────────┘ │
└─────────────────────────────────────────────────────────────┘

3. 核心组件实现

3.1 JwtUtil - JWT 工具类

负责生成、解析和验证 JWT 令牌。

@Component
public class JwtUtil {

    // 使用 HS256 算法生成密钥
    private final Key key = Keys.secretKeyFor(SignatureAlgorithm.HS256);
    
    @Autowired
    private AppConfig appConfig;

    /**
     * 生成 JWT 令牌
     * @param username 用户名
     * @return JWT 令牌字符串
     */
    public String generateToken(String username) {
        Map<String, Object> claims = new HashMap<>();
        claims.put("username", username);
        
        return Jwts.builder()
                .setClaims(claims)           // 设置自定义声明
                .setSubject(username)        // 设置主题(用户名)
                .setIssuedAt(new Date())     // 设置签发时间
                .setExpiration(new Date(System.currentTimeMillis() + getExpirationTime()))  // 设置过期时间
                .signWith(key)               // 使用密钥签名
                .compact();                  // 生成令牌
    }

    /**
     * 从令牌中提取用户名
     * @param token JWT 令牌
     * @return 用户名
     */
    public String extractUsername(String token) {
        Claims claims = Jwts.parserBuilder()
                .setSigningKey(key)          // 设置验证密钥
                .build()
                .parseClaimsJws(token)       // 解析令牌
                .getBody();                   // 获取声明体
        return claims.getSubject();
    }

    /**
     * 验证令牌是否有效
     * @param token JWT 令牌
     * @return 是否有效
     */
    public boolean validateToken(String token) {
        try {
            Jwts.parserBuilder()
                    .setSigningKey(key)
                    .build()
                    .parseClaimsJws(token);
            return true;  // 解析成功,令牌有效
        } catch (Exception e) {
            return false; // 解析失败,令牌无效或过期
        }
    }
}

关键点说明

  • 使用 Keys.secretKeyFor(SignatureAlgorithm.HS256) 生成安全的密钥
  • setExpiration() 设置令牌过期时间,增强安全性
  • validateToken() 捕获所有异常,确保令牌验证的健壮性

3.2 JwtFilter - JWT 过滤器

拦截请求,验证 JWT 令牌的有效性。

@Component
public class JwtFilter extends OncePerRequestFilter {

    private static final Logger logger = LoggerFactory.getLogger(JwtFilter.class);

    @Autowired
    private JwtUtil jwtUtil;

    @Override
    protected void doFilterInternal(HttpServletRequest request, 
                                     HttpServletResponse response, 
                                     FilterChain chain) throws ServletException, IOException {
        // 从请求头中获取令牌
        String authorizationHeader = request.getHeader("Authorization");
        String token = null;
        String username = null;

        // 检查授权头,提取 Bearer Token
        if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
            token = authorizationHeader.substring(7);  // 去掉 "Bearer " 前缀
            try {
                username = jwtUtil.extractUsername(token);
            } catch (Exception e) {
                logger.error("Error extracting username from token: {}", e.getMessage());
            }
        }

        // 验证令牌并设置认证上下文
        if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
            if (jwtUtil.validateToken(token)) {
                // 创建认证令牌
                UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
                        username, null, null);
                authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                // 设置安全上下文
                SecurityContextHolder.getContext().setAuthentication(authToken);
            }
        }

        chain.doFilter(request, response);
    }
}

关键点说明

  • 继承 OncePerRequestFilter 确保每个请求只过滤一次
  • Authorization 头中提取 Bearer Token
  • 使用 SecurityContextHolder 设置认证信息,供后续业务使用

3.3 SecurityConfig - 安全配置

配置 Spring Security,定义安全规则和认证方式。

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Autowired
    private JwtFilter jwtFilter;

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            // 配置 CORS
            .cors(cors -> cors.configurationSource(corsConfigurationSource()))
            // 禁用 CSRF(前后端分离不需要)
            .csrf(csrf -> csrf.disable())
            // 配置请求授权
            .authorizeHttpRequests(authorize -> authorize
                // 允许匿名访问的路径
                .requestMatchers("/api/v1/users/register", 
                                "/api/v1/users/login", 
                                "/api/v1/users/security-question", 
                                "/api/v1/users/reset-password", 
                                "/api/v1/captcha/**").permitAll()
                // AI 相关接口允许匿名访问
                .requestMatchers("/api/v1/ai-analysis/**", 
                                "/api/v1/ai-assistant/**").permitAll()
                // Swagger 文档允许访问
                .requestMatchers("/swagger-ui.html", 
                                "/swagger-ui/**", 
                                "/v3/api-docs/**").permitAll()
                // 其他请求需要认证
                .anyRequest().authenticated()
            )
            // 添加 JWT 过滤器
            .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
        return http.build();
    }

    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration configuration = new CorsConfiguration();
        configuration.setAllowedOrigins(Arrays.asList("http://localhost:5173"));
        configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
        configuration.setAllowedHeaders(Arrays.asList("Content-Type", "Authorization"));
        configuration.setAllowCredentials(true);

        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);
        return source;
    }
}

关键点说明

  • 禁用 CSRF 保护(前后端分离架构不需要)
  • 配置白名单路径,允许匿名访问登录、注册等接口
  • 使用 addFilterBefore() 将 JWT 过滤器添加到 Spring Security 过滤器链中

4. 认证流程

4.1 登录流程

1. 用户发送登录请求 POST /api/v1/users/login
   {
     "username": "zhangsan",
     "password": "password123",
     "captcha": "a3f8"
   }

2. UserController 接收请求,验证用户名密码

3. 验证成功,调用 JwtUtil.generateToken(username) 生成 JWT

4. 返回响应,包含 JWT 令牌
   {
     "success": true,
     "data": {
       "token": "eyJhbGciOiJIUzI1NiIs...",
       "username": "zhangsan"
     }
   }

5. 前端存储 Token(localStorage 或 sessionStorage)

4.2 请求认证流程

1. 用户访问受保护资源,请求头携带 Token
   Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

2. JwtFilter 拦截请求,从 Header 提取 Token

3. 验证 Token 有效性(签名、过期时间)

4. 验证通过,设置 SecurityContext 认证信息

5. 请求到达 Controller,执行业务逻辑

6. 返回业务数据

5. 代码示例

5.1 登录接口实现

@RestController
@RequestMapping("/api/v1/users")
public class UserController {

    @Autowired
    private UserService userService;
    
    @Autowired
    private JwtUtil jwtUtil;
    
    private BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();

    @PostMapping("/login")
    public UserResponse login(@Valid @RequestBody UserLoginRequest request, 
                               HttpServletRequest httpRequest) {
        // 1. 验证验证码
        String captcha = request.getCaptcha();
        if (!validateCaptcha(captcha, httpRequest.getSession())) {
            throw new RuntimeException("验证码错误");
        }
        
        // 2. 验证用户名密码
        User user = userService.login(request.getUsername(), request.getPassword());
        
        // 3. 生成 JWT Token
        String token = jwtUtil.generateToken(user.getUsername());
        
        // 4. 构建响应
        UserResponse response = new UserResponse();
        response.setToken(token);
        response.setUsername(user.getUsername());
        response.setEmail(user.getEmail());
        // ... 其他用户信息
        
        return response;
    }
}

5.2 受保护接口示例

@RestController
@RequestMapping("/api/v1/food-records")
public class FoodRecordController {

    @GetMapping
    public List<FoodRecord> getRecords(@RequestParam String username) {
        // 由于 JwtFilter 已经验证过 Token
        // 这里可以直接获取当前认证用户
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        String currentUser = auth.getName();
        
        // 验证只能查询自己的记录
        if (!currentUser.equals(username)) {
            throw new RuntimeException("无权访问其他用户的数据");
        }
        
        return foodRecordService.findByUsername(username);
    }
}

6. 前端集成

6.1 登录后存储 Token

// 登录成功,保存 Token
const login = async (credentials) => {
  const response = await api.post('/api/v1/users/login', credentials);
  const { token, username } = response.data;
  
  // 存储到 localStorage
  localStorage.setItem('token', token);
  localStorage.setItem('username', username);
  
  return response.data;
};

6.2 请求拦截器添加 Token

// Axios 请求拦截器
api.interceptors.request.use(
  config => {
    const token = localStorage.getItem('token');
    if (token) {
      config.headers.Authorization = `Bearer ${token}`;
    }
    return config;
  },
  error => Promise.reject(error)
);

6.3 响应拦截器处理 401

// Axios 响应拦截器
api.interceptors.response.use(
  response => response.data,
  error => {
    if (error.response?.status === 401) {
      // Token 过期或无效,清除存储并跳转登录页
      localStorage.removeItem('token');
      window.location.href = '/';
    }
    return Promise.reject(error);
  }
);

7. 安全注意事项

7.1 密钥管理

  • 生产环境:密钥应存储在环境变量或配置中心,不要硬编码
  • 密钥轮换:定期更换密钥,旧令牌会自然失效
  • 密钥强度:使用足够强度的密钥(HS256 自动生成)

7.2 令牌安全

风险 防护措施
XSS 攻击 使用 httpOnly Cookie 存储令牌,或做好 XSS 防护
CSRF 攻击 前后端分离天然免疫 CSRF,但仍需验证 Origin
令牌泄露 设置合理的过期时间(通常 1-24 小时)
重放攻击 结合时间戳和随机数,或使用短期令牌

7.3 传输安全

  • 生产环境必须使用 HTTPS
  • 敏感接口增加额外的验证(如密码修改需要旧密码)
Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐