springboot
Spring Security 安全认证
By AI-Writer 15 min read
Spring Security 安全认证
Spring Security 是 Spring 生态中最权威的安全框架,提供完整的认证(Authentication)与授权(Authorization)解决方案。Spring Security 6.x 进行了重大重构,移除了部分废弃 API,提供了更现代的 Lambda DSL 配置方式。本文覆盖 JWT 无状态认证和 OAuth 2.0 资源服务器两大核心场景。
核心概念
- 认证(Authentication):确认用户是谁。验证凭证(用户名+密码、JWT Token、OAuth Token 等)
- 授权(Authorization):确认用户能做什么。判断用户是否拥有访问资源的权限
- Principal:认证后的主体(通常是用户信息)
- GrantedAuthority:用户被授予的权限(如
ROLE_USER、SCOPE_read) - SecurityContext:保存当前认证信息的上下文
Spring Security 6.x 依赖与基本配置
依赖引入
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<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.12.6</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.12.6</version>
<scope>runtime</scope>
</dependency>Spring Security 6.x 默认启用所有端点保护(除 /actuator/health),添加 spring-boot-starter-security 后所有接口都需要认证。
最小化配置
java
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(AbstractHttpConfigurer::disable) // 禁用 CSRF(前后端分离 API)
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**", "/api/public/**").permitAll()
.anyRequest().authenticated()
)
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.addFilterBefore(jwtAuthenticationFilter(),
UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(); // BCrypt 密码哈希(单向加密)
}
}JWT 无状态认证实现
整体流程
plaintext
用户登录 → 验证密码 → 生成 JWT → 返回 Token
后续请求 → Header: Authorization: Bearer <token>
→ JwtAuthFilter 拦截 → 验证 Token → 放行 / 拒绝JWT 工具类
java
@Component
public class JwtUtils {
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.expiration-ms:86400000}") // 默认 24 小时
private long expirationMs;
// 签发 Token
public String generateToken(UserDetails user) {
Map<String, Object> claims = new HashMap<>();
claims.put("roles", user.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.toList());
return Jwts.builder()
.claims(claims)
.subject(user.getUsername())
.issuedAt(new Date())
.expiration(new Date(System.currentTimeMillis() + expirationMs))
.signWith(Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8)),
Jwts.SIG.HS256)
.compact();
}
// 验证并解析 Token
public Claims parseToken(String token) {
return Jwts.parser()
.verifyWith(Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8)))
.build()
.parseSignedClaims(token)
.getPayload();
}
// 从 Token 提取用户名
public String extractUsername(String token) {
return parseToken(token).getSubject();
}
// 验证 Token 是否有效(未过期且签名正确)
public boolean isTokenValid(String token, UserDetails user) {
try {
String username = extractUsername(token);
return username.equals(user.getUsername()) && !isTokenExpired(token);
} catch (JwtException e) {
return false;
}
}
private boolean isTokenExpired(String token) {
return parseToken(token).getExpiration().before(new Date());
}
}JWT 认证过滤器
java
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtUtils jwtUtils;
private final UserDetailsService userDetailsService;
public JwtAuthenticationFilter(JwtUtils jwtUtils,
UserDetailsService userDetailsService) {
this.jwtUtils = jwtUtils;
this.userDetailsService = userDetailsService;
}
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain)
throws ServletException, IOException {
String authHeader = request.getHeader("Authorization");
// 提取 Token(格式:Bearer <token>)
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
filterChain.doFilter(request, response);
return;
}
String token = authHeader.substring(7);
try {
String username = jwtUtils.extractUsername(token);
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
// 未认证且 Token 有效
if (username != null && auth == null) {
UserDetails user = userDetailsService.loadUserByUsername(username);
if (jwtUtils.isTokenValid(token, user)) {
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(
user, null, user.getAuthorities()
);
authentication.setDetails(
new WebAuthenticationDetailsSource().buildDetails(request)
);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
} catch (JwtException e) {
// Token 无效,继续请求但不设置认证上下文
// 可在此记录日志或返回 401
}
filterChain.doFilter(request, response);
}
@Override
protected boolean shouldNotFilter(HttpServletRequest request) {
// 跳过登录接口
return request.getServletPath().startsWith("/api/auth/");
}
}登录接口
java
@RestController
@RequestMapping("/api/auth")
public class AuthController {
private final AuthenticationManager authenticationManager;
private final JwtUtils jwtUtils;
private final UserRepository userRepository;
@PostMapping("/login")
public ResponseEntity<LoginResponse> login(@Valid @RequestBody LoginRequest request) {
// 1. 验证用户名密码(触发 Security 认证流程)
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
request.username(),
request.password()
)
);
// 2. 认证成功,生成 JWT
UserDetails user = (UserDetails) authentication.getPrincipal();
String token = jwtUtils.generateToken(user);
return ResponseEntity.ok(new LoginResponse(token, "Bearer", 86400));
}
// 注册接口
@PostMapping("/register")
public ResponseEntity<Void> register(@Valid @RequestBody RegisterRequest request,
@Autowired PasswordEncoder encoder) {
if (userRepository.existsByUsername(request.username())) {
throw new UserAlreadyExistsException();
}
User user = new User(
request.username(),
encoder.encode(request.password()), // BCrypt 加密存储
Set.of("ROLE_USER")
);
userRepository.save(user);
return ResponseEntity.status(HttpStatus.CREATED).build();
}
public record LoginRequest(String username, String password) {}
public record RegisterRequest(String username, String password) {}
public record LoginResponse(String token, String type, int expiresIn) {}
}java
@Configuration
public class SecurityConfig {
@Bean
public AuthenticationManager authenticationManager(
AuthenticationConfiguration config) throws Exception {
return config.getAuthenticationManager();
}
}使用 AuthenticationManagerBuilder(自定义登录逻辑时)
如果不想用默认的 AuthenticationManager,也可以自行实现认证逻辑:
java
@Service
public class CustomUserDetailsService implements UserDetailsService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
@Override
public UserDetails loadUserByUsername(String username)
throws UsernameNotFoundException {
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException(username));
return new org.springframework.security.core.userdetails.User(
user.getUsername(),
user.getPassword(),
user.getAuthorities() // Collection<? extends GrantedAuthority>
);
}
}授权:方法级安全注解
@PreAuthorize 与 @PostAuthorize
java
@RestController
@RequestMapping("/api/admin")
@PreAuthorize("hasRole('ADMIN')") // 整个控制器要求 ADMIN 角色
public class AdminController {
@GetMapping("/users")
@PreAuthorize("hasAuthority('SCOPE_read') or hasRole('ADMIN')")
public List<User> listAllUsers() {
return userService.findAll();
}
@DeleteMapping("/users/{id}")
@PreAuthorize("hasRole('ADMIN') and #id != authentication.principal.id")
// 禁止管理员删除自己
public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
userService.deleteById(id);
return ResponseEntity.noContent().build();
}
// SpEL 表达式中使用方法参数
@GetMapping("/users/{username}")
@PreAuthorize("#username == authentication.principal.username or hasRole('ADMIN')")
public User getUserProfile(@PathVariable String username) {
return userService.findByUsername(username);
}
// @PostAuthorize:在方法执行后验证,适合基于返回值决定权限
@GetMapping("/secret")
@PostAuthorize("returnObject.owner == authentication.principal.username or hasRole('ADMIN')")
public Document getSecretDocument(@PathVariable Long id) {
return documentService.findById(id);
}
}启用方法安全注解
java
@SpringBootApplication
@EnableMethodSecurity(
prePostEnabled = true, // 启用 @PreAuthorize / @PostAuthorize
securedEnabled = true, // 启用 @Secured
jsr250Enabled = true // 启用 @RolesAllowed
)
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}OAuth 2.0 资源服务器
如果你的 API 要作为资源服务器验证第三方颁发的 OAuth 2.0 Token:
java
@Configuration
@EnableWebSecurity
public class OAuth2ResourceServerConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/public/**").permitAll()
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt.jwtAuthenticationConverter(
new JwtAuthenticationConverterWithDefaults()
))
);
return http.build();
}
}yaml
# application.yml
spring:
security:
oauth2:
resourceserver:
jwt:
# 方式1:使用授权服务器的公钥端点
issuer-uri: https://auth.example.com
# 方式2:直接指定公钥(离线验证)
# jwk-set-uri: https://auth.example.com/.well-known/jwks.jsonJWT 认证过滤器会自动验证 Token 签名、有效期等,/api/me 接口可直接获取用户信息:
java
@GetMapping("/api/me")
public UserProfile getCurrentUser(@AuthenticationPrincipal Jwt jwt) {
// @AuthenticationPrincipal 自动注入解析后的 JWT
return new UserProfile(
jwt.getSubject(), // 用户 ID
jwt.getClaimAsString("name"),
jwt.getClaimAsStringList("roles"),
jwt.getClaimAsString("email")
);
}常见安全配置
CORS 跨域配置
java
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.cors(cors -> cors.configurationSource(corsConfigurationSource()));
return http.build();
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(List.of("http://localhost:3000"));
config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));
config.setAllowedHeaders(List.of("*"));
config.setAllowCredentials(true);
config.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/api/**", config);
return source;
}
}密码加密
java
@Bean
public PasswordEncoder passwordEncoder() {
// BCrypt:自带盐,每轮加密结果不同(包含盐值存储)
return new BCryptPasswordEncoder(12); // 强度因子 12(性能与安全平衡)
}
// 验证密码
if (passwordEncoder.matches(rawPassword, encodedPassword)) {
// 密码匹配
}小结
- Spring Security 6.x 使用 Lambda DSL 配置
SecurityFilterChain,语法更简洁 - JWT 无状态认证流程:登录时
AuthenticationManager验证密码 →JwtUtils.generateToken()签发 →JwtAuthFilter拦截请求验证 BCryptPasswordEncoder是密码存储的首选,绝不明文存储密码@PreAuthorize支持 SpEL 表达式,可引用方法参数和方法名实现精细权限控制- OAuth 2.0 资源服务器通过
spring.security.oauth2.resourceserver.jwt.issuer-uri一行配置即可接入
#springboot
#spring-security
#jwt
#oauth2
#authentication
评论
A
Written by
AI-Writer
Related Articles
springboot
#5 数据访问:JPA 与 MyBatis
Spring Data JPA 实体映射与 Repository 接口、MyBatis-Plus 增强用法、JdbcTemplate 原始查询、事务管理(@Transactional 传播行为与隔离级别)完整指南
Read More springboot
#10 Spring Boot 3 新特性与 GraalVM AOT 编译
Spring Boot 3 核心升级(Spring Framework 6 / Jakarta EE 9+)、GraalVM Native Image 原生编译加速启动、Spring AOT 编译流程与实测启动时间对比
Read More springboot
#1 Spring Boot 3 环境准备与项目初始化
搭建 Spring Boot 3.x 开发环境、使用 Spring Initializr 快速创建项目、理解 Maven/Gradle 构建配置、项目目录结构与第一个可运行应用
Read More