一、核心概念
1.1 什么是 JWT?
JWT(JSON Web Token)是一种开放标准(RFC 7519),用于在各方之间安全地传输信息作为 JSON 对象。它通常用于身份验证和信息交换。
1.2 JWT 结构
JWT 由三部分组成,用点(.
)分隔:
xxxxx.yyyyy.zzzzz
- Header(头部):包含令牌类型和使用的签名算法(如
HS256
)。 - Payload(负载):包含声明(Claims),如用户信息、过期时间等。
- Signature(签名):对前两部分的签名,确保令牌未被篡改。
1.3 无状态认证(Stateless Authentication)
- 有状态:服务器需要维护 Session 状态(如存储在内存、Redis)。
- 无状态:服务器不存储会话信息,每次请求都携带 JWT,服务器通过验证 JWT 的签名来确认用户身份。
1.4 核心优势
- 可扩展性:适合分布式系统和微服务架构。
- 跨域支持:易于在不同域之间传递。
- 自包含:Token 包含所有必要信息。
二、详细操作步骤(适合快速实践)
步骤 1:创建 Spring Boot 项目并添加依赖
使用 start.spring.io 创建项目,添加以下依赖:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- JWT 支持 -->
<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>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<!-- 数据库支持(可选) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>
注意:JJWT 0.11+ 版本将 API、实现和 Jackson 支持分离。
步骤 2:创建 JWT 工具类 JwtUtil.java
package com.example.demo.util;
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import java.security.Key;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
@Component
public class JwtUtil {
// 密钥(生产环境应使用更安全的密钥,如从配置中心获取)
@Value("${jwt.secret:mySecretKeyThatIsAtLeast32CharactersLong}")
private String secret;
// Token 有效期(毫秒)
@Value("${jwt.expiration:86400000}") // 24小时
private long expiration;
// 获取签名密钥
private Key getSignKey() {
return Keys.hmacShaKeyFor(secret.getBytes());
}
// 从 Token 中提取用户名
public String extractUsername(String token) {
return extractClaim(token, Claims::getSubject);
}
// 从 Token 中提取过期时间
public Date extractExpiration(String token) {
return extractClaim(token, Claims::getExpiration);
}
// 提取单个声明
public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
final Claims claims = extractAllClaims(token);
return claimsResolver.apply(claims);
}
// 解析 Token 获取所有声明
private Claims extractAllClaims(String token) {
try {
return Jwts.parser()
.verifyWith(getSignKey())
.build()
.parseSignedClaims(token)
.getPayload();
} catch (JwtException e) {
throw new IllegalArgumentException("Invalid JWT token", e);
}
}
// 检查 Token 是否过期
private Boolean isTokenExpired(String token) {
return extractExpiration(token).before(new Date());
}
// 生成 Token
public String generateToken(String username) {
Map<String, Object> claims = new HashMap<>();
return createToken(claims, username);
}
// 生成 Token(带自定义声明)
public String generateToken(Map<String, Object> claims, String username) {
return createToken(claims, username);
}
// 创建 Token
private String createToken(Map<String, Object> claims, String subject) {
return Jwts.builder()
.claims(claims)
.subject(subject)
.issuedAt(new Date(System.currentTimeMillis()))
.expiration(new Date(System.currentTimeMillis() + expiration))
.signWith(getSignKey(), SignatureAlgorithm.HS256)
.compact();
}
// 验证 Token 是否有效
public Boolean validateToken(String token, UserDetails userDetails) {
final String username = extractUsername(token);
return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
}
}
步骤 3:创建用户实体和仓库(可选,用于数据库用户)
// UserEntity.java
@Entity
@Table(name = "users")
public class UserEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String username;
private String password;
private String role; // 如 "ROLE_USER"
// getters and setters
}
// UserRepository.java
public interface UserRepository extends JpaRepository<UserEntity, Long> {
Optional<UserEntity> findByUsername(String username);
}
步骤 4:实现 UserDetailsService
@Service
public class CustomUserDetailsService implements UserDetailsService {
@Autowired
private UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
UserEntity user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
return org.springframework.security.core.userdetails.User.builder()
.username(user.getUsername())
.password(user.getPassword())
.roles(user.getRole().replace("ROLE_", ""))
.build();
}
}
步骤 5:创建 JWT 认证过滤器 JwtRequestFilter.java
package com.example.demo.filter;
import com.example.demo.util.JwtUtil;
import com.example.demo.service.CustomUserDetailsService;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
@Component
public class JwtRequestFilter extends OncePerRequestFilter {
@Autowired
private CustomUserDetailsService userDetailsService;
@Autowired
private JwtUtil jwtUtil;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
final String requestTokenHeader = request.getHeader("Authorization");
String username = null;
String jwtToken = null;
// 检查 Authorization 头是否存在且以 "Bearer " 开头
if (requestTokenHeader != null && requestTokenHeader.startsWith("Bearer ")) {
jwtToken = requestTokenHeader.substring(7); // 去除 "Bearer " 前缀
try {
username = jwtUtil.extractUsername(jwtToken);
} catch (IllegalArgumentException e) {
System.out.println("Unable to get JWT Token");
} catch (ExpiredJwtException e) {
System.out.println("JWT Token has expired");
}
} else {
logger.warn("JWT Token does not begin with Bearer String");
}
// 如果用户名存在且当前未认证
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
// 验证 Token
if (jwtUtil.validateToken(jwtToken, userDetails)) {
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
// 将认证信息放入 SecurityContext
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
}
chain.doFilter(request, response);
}
}
步骤 6:配置安全配置类 SecurityConfig.java
package com.example.demo.config;
import com.example.demo.filter.JwtRequestFilter;
import com.example.demo.service.CustomUserDetailsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {
@Autowired
private CustomUserDetailsService userDetailsService;
@Autowired
private JwtRequestFilter jwtRequestFilter;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable()) // JWT 通常与 CSRF 结合使用较复杂,无状态可禁用
.authorizeHttpRequests(authz -> authz
.requestMatchers("/authenticate", "/public/**").permitAll() // 登录接口放行
.anyRequest().authenticated() // 其他请求需要认证
)
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 无状态会话
)
// 在 UsernamePasswordAuthenticationFilter 之前添加 JWT 过滤器
.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
return config.getAuthenticationManager();
}
}
步骤 7:创建认证控制器 AuthController.java
package com.example.demo.controller;
import com.example.demo.util.JwtUtil;
import com.example.demo.service.CustomUserDetailsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
@RestController
public class AuthController {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private CustomUserDetailsService userDetailsService;
@Autowired
private JwtUtil jwtUtil;
// 登录接口,返回 JWT
@PostMapping("/authenticate")
public ResponseEntity<?> createAuthenticationToken(@RequestBody AuthenticationRequest authenticationRequest) throws Exception {
try {
authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(authenticationRequest.getUsername(), authenticationRequest.getPassword())
);
} catch (Exception e) {
throw new Exception("Incorrect username or password", e);
}
final UserDetails userDetails = userDetailsService.loadUserByUsername(authenticationRequest.getUsername());
final String jwt = jwtUtil.generateToken(userDetails.getUsername());
Map<String, String> response = new HashMap<>();
response.put("token", jwt);
return ResponseEntity.ok(response);
}
// 请求体类
public static class AuthenticationRequest {
private String username;
private String password;
// getters and setters
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
public String getPassword() { return password; }
public void setPassword(String password) { this.password = password; }
}
}
步骤 8:创建测试控制器
@RestController
public class TestController {
@GetMapping("/public/hello")
public String hello() {
return "Hello, Public!";
}
@GetMapping("/secure/data")
@PreAuthorize("hasRole('USER')")
public String secureData() {
return "This is secure data!";
}
@GetMapping("/admin/stats")
@PreAuthorize("hasRole('ADMIN')")
public String adminStats() {
return "Admin only!";
}
}
步骤 9:配置文件 application.yml
server:
port: 8080
jwt:
secret: mySuperSecretKeyThatIsAtLeast32CharactersLong
expiration: 86400000 # 24 hours in milliseconds
spring:
datasource:
url: jdbc:h2:mem:testdb
driver-class-name: org.h2.Driver
jpa:
hibernate:
ddl-auto: create-drop
show-sql: true
步骤 10:运行与测试
- 启动应用。
- 获取 Token:
- 发送 POST 请求到
http://localhost:8080/authenticate
- Body:
{ "username": "user", "password": "password" }
- 响应:
{ "token": "eyJhbGciOiJIUzI1NiJ9.xxxxx" }
- 发送 POST 请求到
- 访问受保护资源:
- 发送 GET 请求到
http://localhost:8080/secure/data
- Header:
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.xxxxx
- 成功返回数据。
- 发送 GET 请求到
- 访问未授权资源:
- 用
user
的 Token 访问/admin/stats
→ 403 Forbidden。
- 用
三、常见错误与解决方案
错误现象 | 原因分析 | 解决方案 |
---|---|---|
Invalid JWT token |
签名密钥不匹配、Token 格式错误 | 检查 jwt.secret 配置;确认 Token 以 Bearer 开头 |
JWT Token has expired |
Token 已过期 | 重新登录获取新 Token;调整 jwt.expiration |
401 Unauthorized |
未提供 Token 或 Token 无效 | 确保请求头包含 Authorization: Bearer <token> |
No 'Access-Control-Allow-Origin' |
CORS 问题 | 配置 CORS 过滤器或使用 @CrossOrigin |
Unable to inject AuthenticationManager |
未正确暴露 Bean | 确保在 SecurityConfig 中定义 @Bean AuthenticationManager |
四、注意事项
- 密钥安全:
- 生产环境密钥必须足够长(至少 32 字符)且保密。
- 避免硬编码,使用环境变量或配置中心。
- Token 过期:
- 设置合理的过期时间(如 15-30 分钟)。
- 实现 Refresh Token 机制(本文未涉及)。
- CSRF 与 JWT:
- 无状态 JWT 通常不需要传统 CSRF 保护,但需防范 XSS。
- 敏感信息:
- 不要在 JWT Payload 中存储敏感信息(如密码)。
- 算法选择:
- HS256 适用于简单场景,RS256 更安全(非对称加密)。
五、使用技巧
5.1 在 Payload 中添加自定义声明
Map<String, Object> claims = new HashMap<>();
claims.put("userId", user.getId());
claims.put("email", user.getEmail());
String token = jwtUtil.generateToken(claims, username);
5.2 提取 Token 中的自定义声明
String userId = (String) jwtUtil.extractClaim(token, claims -> claims.get("userId"));
5.3 使用 Refresh Token(进阶)
- 实现一个
/refresh-token
接口,使用长期有效的 Refresh Token 换取新的 Access Token。 - Refresh Token 应存储在安全位置(如 HTTP Only Cookie)。
六、最佳实践
- HTTPS 强制:所有 API 必须通过 HTTPS 访问。
- 最小权限:Token 中只包含必要权限。
- 审计日志:记录 Token 生成、使用和失效事件。
- Token 黑名单:实现短期黑名单(如 Redis)应对 Token 泄露。
- 版本控制:在 JWT Payload 中加入版本号,便于未来升级。
七、性能优化
- 签名算法:
- HS256 计算快,适合高并发。
- 避免使用计算密集型算法(如 PBKDF2)。
- Token 大小:
- 减少 Payload 中的数据量,避免影响网络传输。
- 缓存:
- 虽然无状态,但可缓存用户信息(如 Redis)减少数据库查询。
- 异步验证:
- 对于极高并发,可考虑异步验证 Token(需谨慎处理线程安全)。
总结
通过以上步骤,你已成功在 Spring Boot 中集成了 JWT 无状态认证。该方案适合现代 Web 应用和微服务架构。
下一步建议:
- 实现 Refresh Token 机制。
- 集成 Spring Security OAuth2 Resource Server。
- 学习 JWT 的 JWE(加密)和 JWS(签名)高级用法。
- 考虑使用
spring-security-oauth2-resource-server
简化 JWT 验证。
本文基于 Spring Boot 3.x + Spring Security 6.x + JJWT 0.11+ 编写。