Spring Security 集成 JWT
2025/9/17大约 7 分钟
Spring Security 集成 JWT 完整指南
前置要求
在开始本教程之前,请确保您已经:
- 掌握了基本的 Spring Boot 知识
- 了解了 JWT 的基本概念
- 熟悉 Spring Security 的核心概念
概述
Spring Security 是 Spring 生态系统中强大的安全框架,结合 JWT 可以实现无状态的认证和授权系统。本教程将详细介绍如何将两者完美集成。
项目依赖配置
Maven 依赖
<dependencies>
<!-- Spring Boot Starter Security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- Spring Boot Starter Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</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>
<!-- Lombok 简化代码 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</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>
核心实现
1. 用户实体类
package com.example.security.entity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import javax.persistence.*;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
@Entity
@Table(name = "users")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User implements UserDetails {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true, nullable = false)
private String username;
@Column(nullable = false)
private String password;
@Column(unique = true, nullable = false)
private String email;
@Column(nullable = false)
private boolean enabled = true;
@ElementCollection(fetch = FetchType.EAGER)
@CollectionTable(name = "user_roles", joinColumns = @JoinColumn(name = "user_id"))
@Column(name = "role")
private List<String> roles;
// UserDetails 接口实现方法
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
// 将角色列表转换为 Spring Security 的权限对象
return roles.stream()
.map(role -> new SimpleGrantedAuthority("ROLE_" + role))
.collect(Collectors.toList());
}
@Override
public boolean isAccountNonExpired() {
return true; // 账户未过期
}
@Override
public boolean isAccountNonLocked() {
return true; // 账户未锁定
}
@Override
public boolean isCredentialsNonExpired() {
return true; // 凭证未过期
}
@Override
public boolean isEnabled() {
return enabled; // 账户是否启用
}
}
2. JWT 工具类
package com.example.security.util;
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import javax.crypto.SecretKey;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
@Component
@Slf4j
public class JwtTokenUtil {
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.expiration}")
private Long expiration;
// 生成签名密钥
private SecretKey getSigningKey() {
return Keys.hmacShaKeyFor(secret.getBytes());
}
/**
* 从令牌中提取用户名
*/
public String extractUsername(String token) {
return extractClaim(token, Claims::getSubject);
}
/**
* 从令牌中提取过期时间
*/
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);
}
/**
* 从令牌中提取所有声明
*/
private Claims extractAllClaims(String token) {
return Jwts.parserBuilder()
.setSigningKey(getSigningKey())
.build()
.parseClaimsJws(token)
.getBody();
}
/**
* 检查令牌是否过期
*/
private Boolean isTokenExpired(String token) {
return extractExpiration(token).before(new Date());
}
/**
* 为用户生成令牌
*/
public String generateToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>();
return createToken(claims, userDetails.getUsername());
}
/**
* 创建令牌
*/
private String createToken(Map<String, Object> claims, String subject) {
return Jwts.builder()
.setClaims(claims)
.setSubject(subject)
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + expiration))
.signWith(getSigningKey(), SignatureAlgorithm.HS256)
.compact();
}
/**
* 验证令牌
*/
public Boolean validateToken(String token, UserDetails userDetails) {
final String username = extractUsername(token);
return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
}
}
3. 自定义用户详情服务
package com.example.security.service;
import com.example.security.entity.User;
import com.example.security.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {
private final UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 从数据库查找用户
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("用户不存在: " + username));
return user; // User 类已经实现了 UserDetails 接口
}
}
4. JWT 认证过滤器
package com.example.security.filter;
import com.example.security.service.CustomUserDetailsService;
import com.example.security.util.JwtTokenUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
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 javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component
@RequiredArgsConstructor
@Slf4j
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtTokenUtil jwtTokenUtil;
private final CustomUserDetailsService userDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain)
throws ServletException, IOException {
// 从请求头中获取 Authorization 字段
final String authorizationHeader = request.getHeader("Authorization");
String username = null;
String jwt = null;
// 检查 Authorization 头是否以 "Bearer " 开头
if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
jwt = authorizationHeader.substring(7); // 去掉 "Bearer " 前缀
try {
username = jwtTokenUtil.extractUsername(jwt);
} catch (Exception e) {
log.error("JWT 令牌解析失败", e);
}
}
// 如果成功提取到用户名且当前没有认证信息
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
// 验证令牌
if (jwtTokenUtil.validateToken(jwt, userDetails)) {
// 创建认证令牌
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
// 设置认证详情
authenticationToken.setDetails(
new WebAuthenticationDetailsSource().buildDetails(request));
// 设置认证信息到安全上下文
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
}
// 继续过滤器链
filterChain.doFilter(request, response);
}
}
5. Spring Security 配置
package com.example.security.config;
import com.example.security.filter.JwtAuthenticationFilter;
import com.example.security.service.CustomUserDetailsService;
import lombok.RequiredArgsConstructor;
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.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
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.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true) // 启用方法级安全注解
@RequiredArgsConstructor
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final CustomUserDetailsService userDetailsService;
private final JwtAuthenticationFilter jwtAuthenticationFilter;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 配置用户详情服务
auth.userDetailsService(userDetailsService)
.passwordEncoder(passwordEncoder());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable() // 禁用 CSRF,因为我们使用 JWT
.authorizeRequests()
.antMatchers("/api/auth/**").permitAll() // 认证相关接口允许匿名访问
.antMatchers("/api/public/**").permitAll() // 公开接口
.antMatchers("/h2-console/**").permitAll() // H2 控制台(开发环境)
.anyRequest().authenticated() // 其他请求需要认证
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 无状态会话
.and()
.headers().frameOptions().disable(); // 允许 H2 控制台的 iframe
// 添加 JWT 过滤器
http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
6. 认证控制器
package com.example.security.controller;
import com.example.security.dto.AuthRequest;
import com.example.security.dto.AuthResponse;
import com.example.security.service.AuthService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/auth")
@RequiredArgsConstructor
public class AuthController {
private final AuthService authService;
/**
* 用户登录
*/
@PostMapping("/login")
public ResponseEntity<AuthResponse> login(@RequestBody AuthRequest authRequest) {
String token = authService.authenticate(authRequest.getUsername(), authRequest.getPassword());
return ResponseEntity.ok(new AuthResponse(token, "登录成功"));
}
/**
* 用户注册
*/
@PostMapping("/register")
public ResponseEntity<AuthResponse> register(@RequestBody AuthRequest authRequest) {
String token = authService.register(authRequest.getUsername(), authRequest.getPassword());
return ResponseEntity.ok(new AuthResponse(token, "注册成功"));
}
/**
* 获取当前用户信息
*/
@GetMapping("/me")
public ResponseEntity<?> getCurrentUser() {
return ResponseEntity.ok(authService.getCurrentUser());
}
}
7. 认证服务
package com.example.security.service;
import com.example.security.entity.User;
import com.example.security.repository.UserRepository;
import com.example.security.util.JwtTokenUtil;
import lombok.RequiredArgsConstructor;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import java.util.Arrays;
@Service
@RequiredArgsConstructor
public class AuthService {
private final AuthenticationManager authenticationManager;
private final JwtTokenUtil jwtTokenUtil;
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
/**
* 用户认证
*/
public String authenticate(String username, String password) {
// 使用 Spring Security 的认证管理器进行认证
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(username, password)
);
// 设置认证信息到安全上下文
SecurityContextHolder.getContext().setAuthentication(authentication);
// 生成 JWT 令牌
return jwtTokenUtil.generateToken(authentication.getPrincipal());
}
/**
* 用户注册
*/
public String register(String username, String password) {
// 检查用户是否已存在
if (userRepository.findByUsername(username).isPresent()) {
throw new RuntimeException("用户名已存在");
}
// 创建新用户
User user = new User();
user.setUsername(username);
user.setPassword(passwordEncoder.encode(password)); // 加密密码
user.setEmail(username + "@example.com"); // 示例邮箱
user.setRoles(Arrays.asList("USER")); // 默认角色
user.setEnabled(true);
// 保存用户
userRepository.save(user);
// 生成令牌
return jwtTokenUtil.generateToken(user);
}
/**
* 获取当前用户
*/
public User getCurrentUser() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
String username = authentication.getName();
return userRepository.findByUsername(username)
.orElseThrow(() -> new RuntimeException("用户不存在"));
}
}
8. DTO 类
package com.example.security.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class AuthRequest {
private String username;
private String password;
}
@Data
@NoArgsConstructor
@AllArgsConstructor
public class AuthResponse {
private String token;
private String message;
}
配置文件
application.properties
# JWT 配置
jwt.secret=your-super-secret-key-here-make-it-long-and-random-at-least-256-bits
jwt.expiration=86400000
# 数据库配置(H2 内存数据库)
spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
# H2 控制台
spring.h2.console.enabled=true
spring.h2.console.path=/h2-console
# JPA 配置
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
spring.jpa.hibernate.ddl-auto=create-drop
spring.jpa.show-sql=true
使用示例
1. 方法级安全注解
@RestController
@RequestMapping("/api/admin")
public class AdminController {
@PreAuthorize("hasRole('ADMIN')") // 只有 ADMIN 角色可以访问
@GetMapping("/dashboard")
public ResponseEntity<String> getDashboard() {
return ResponseEntity.ok("管理员仪表板");
}
@PreAuthorize("hasRole('USER')") // 只有 USER 角色可以访问
@GetMapping("/profile")
public ResponseEntity<String> getProfile() {
return ResponseEntity.ok("用户资料");
}
@PreAuthorize("hasAnyRole('ADMIN', 'MODERATOR')") // ADMIN 或 MODERATOR 角色
@GetMapping("/moderate")
public ResponseEntity<String> moderate() {
return ResponseEntity.ok("内容审核");
}
}
2. 测试 API
# 注册用户
curl -X POST http://localhost:8080/api/auth/register \
-H "Content-Type: application/json" \
-d '{"username":"testuser","password":"password"}'
# 用户登录
curl -X POST http://localhost:8080/api/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"testuser","password":"password"}'
# 访问受保护的资源
curl -X GET http://localhost:8080/api/admin/dashboard \
-H "Authorization: Bearer YOUR_JWT_TOKEN"
最佳实践
安全注意事项
- 密钥管理:使用强密钥并妥善保管
- 令牌过期:设置合理的过期时间
- HTTPS:生产环境必须使用 HTTPS
- 角色设计:合理设计角色和权限体系
性能优化
- 缓存用户信息:使用 Redis 缓存用户详情
- 令牌刷新:实现刷新令牌机制
- 数据库优化:合理设计数据库索引
总结
本教程详细介绍了 Spring Security 与 JWT 的集成,包括:
- ✅ 基础配置:依赖配置和安全配置
- ✅ 核心组件:JWT 工具类、过滤器、用户服务
- ✅ 认证流程:完整的登录注册流程
- ✅ 权限控制:基于角色的访问控制
- ✅ 最佳实践:安全和性能优化建议
下一步学习
- 了解微服务架构中的 JWT 使用
- 探索 JWT 的高级特性(如嵌套令牌)
- 学习 Spring Security 的更多高级功能