跳到主要内容

Spring Security 安全框架

Spring Security 是一个功能强大且高度可定制的身份验证和访问控制框架,是保护 Spring 应用的标准解决方案。

安全框架概述

什么是 Spring Security?

Spring Security 提供了完整的安全性解决方案,包括:

  • 认证(Authentication):验证用户身份
  • 授权(Authorization):验证用户权限
  • 防止攻击:CSRF、Session Fixation、Clickjacking 等
  • 集成能力:OAuth2、SAML、LDAP 等

核心架构

┌─────────────────────────────────────────────────────────────┐
│ Spring Security 架构 │
├─────────────────────────────────────────────────────────────┤
│ │
│ HTTP 请求 ──> Security Filter Chain ──> 应用程序 │
│ │ │
│ ┌───────────────┴───────────────┐ │
│ ▼ ▼ │
│ 认证过滤器 授权过滤器 │
│ (Authentication) (Authorization) │
│ │ │ │
│ ▼ ▼ │
│ AuthenticationManager AccessDecisionManager │
│ │ │ │
│ ▼ ▼ │
│ UserDetailsService 权限配置 │
│ │
└─────────────────────────────────────────────────────────────┘

核心组件

组件说明
SecurityContextHolder存储当前安全上下文
SecurityContext持有 Authentication 和安全信息
Authentication表示已认证用户的身份信息
UserDetails用户详细信息接口
AuthenticationManager认证管理器
AccessDecisionManager访问决策管理器

快速开始

添加依赖

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>

基本配置

添加依赖后,Spring Security 默认启用基本安全配置:

  • 所有请求需要认证
  • 默认用户名:user
  • 默认密码:启动时生成并打印在控制台

自定义用户和密码

# application.yml
spring:
security:
user:
name: admin
password: admin123
roles: ADMIN

基本安全配置

@Configuration
@EnableWebSecurity
public class SecurityConfig {

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// 配置请求授权
.authorizeHttpRequests(auth -> auth
.requestMatchers("/", "/home", "/register", "/login").permitAll()
.requestMatchers("/admin/**").hasRole("ADMIN")
.requestMatchers("/user/**").hasAnyRole("USER", "ADMIN")
.anyRequest().authenticated()
)
// 配置表单登录
.formLogin(form -> form
.loginPage("/login")
.defaultSuccessUrl("/dashboard")
.permitAll()
)
// 配置登出
.logout(logout -> logout
.logoutSuccessUrl("/login?logout")
.permitAll()
)
// 配置记住我
.rememberMe(remember -> remember
.key("uniqueAndSecret")
.tokenValiditySeconds(86400) // 1 天
);

return http.build();
}

@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}

认证机制

用户详情服务

实现 UserDetailsService 接口加载用户信息:

@Service
public class CustomUserDetailsService implements UserDetailsService {

@Autowired
private UserRepository userRepository;

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("用户不存在: " + username));

return org.springframework.security.core.userdetails.User
.withUsername(user.getUsername())
.password(user.getPassword())
.roles(user.getRoles().toArray(new String[0]))
.accountExpired(!user.isAccountNonExpired())
.accountLocked(!user.isAccountNonLocked())
.credentialsExpired(!user.isCredentialsNonExpired())
.disabled(!user.isEnabled())
.build();
}
}

用户实体

@Entity
@Table(name = "t_user")
@Data
public class User {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column(unique = true, nullable = false)
private String username;

@Column(nullable = false)
private String password;

@Column(nullable = false)
private String email;

@ElementCollection(fetch = FetchType.EAGER)
@CollectionTable(name = "t_user_roles", joinColumns = @JoinColumn(name = "user_id"))
@Column(name = "role")
private Set<String> roles = new HashSet<>();

private boolean enabled = true;
private boolean accountNonExpired = true;
private boolean accountNonLocked = true;
private boolean credentialsNonExpired = true;
}

密码编码

@Service
public class UserService {

@Autowired
private UserRepository userRepository;

@Autowired
private PasswordEncoder passwordEncoder;

public User register(UserDTO userDTO) {
// 检查用户名是否已存在
if (userRepository.existsByUsername(userDTO.getUsername())) {
throw new BusinessException("用户名已存在");
}

User user = new User();
user.setUsername(userDTO.getUsername());
user.setPassword(passwordEncoder.encode(userDTO.getPassword())); // 加密密码
user.setEmail(userDTO.getEmail());
user.setRoles(Collections.singleton("USER"));

return userRepository.save(user);
}
}

认证流程

┌─────────────────────────────────────────────────────────────┐
│ 认证流程 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. 用户提交用户名和密码 │
│ │ │
│ 2. UsernamePasswordAuthenticationFilter 捕获请求 │
│ │ │
│ 3. AuthenticationManager 调用认证 │
│ │ │
│ 4. DaoAuthenticationProvider 调用 UserDetailsService │
│ │ │
│ 5. UserDetailsService 从数据库加载用户信息 │
│ │ │
│ 6. PasswordEncoder 验证密码 │
│ │ │
│ 7. 认证成功:创建 Authentication 并存入 SecurityContext │
│ 认证失败:抛出 AuthenticationException │
│ │
└─────────────────────────────────────────────────────────────┘

授权机制

方法级安全

@Service
public class OrderService {

@PreAuthorize("hasRole('ADMIN')")
public List<Order> findAllOrders() {
// 只有 ADMIN 角色可以访问
return orderRepository.findAll();
}

@PreAuthorize("hasRole('USER') or hasRole('ADMIN')")
public Order findMyOrder(Long orderId, String username) {
return orderRepository.findByIdAndUsername(orderId, username);
}

@PreAuthorize("#username == authentication.principal.username")
public Order getMyOrder(Long orderId, String username) {
return orderRepository.findById(orderId);
}

@PostAuthorize("returnObject.username == authentication.principal.username")
public Order getOrderById(Long id) {
return orderRepository.findById(id);
}

@PreAuthorize("@orderService.isOwner(#id, authentication.principal.username)")
public void updateOrder(Long id, OrderDTO orderDTO) {
// 自定义权限检查
}

@PostFilter("filterObject.username == authentication.principal.username")
public List<Order> getMyOrders() {
return orderRepository.findAll();
}
}

启用方法级安全

@Configuration
@EnableMethodSecurity
public class SecurityConfig {
// ...
}

SpEL 表达式

表达式说明
hasRole('ROLE')是否有指定角色
hasAnyRole('ROLE1', 'ROLE2')是否有任意一个角色
hasAuthority('AUTH')是否有指定权限
hasAnyAuthority('AUTH1', 'AUTH2')是否有任意一个权限
permitAll允许所有人
denyAll拒绝所有人
isAnonymous()是否匿名用户
isAuthenticated()是否已认证
isRememberMe()是否记住我登录
isFullyAuthenticated()是否完全认证(非记住我)
principal当前用户主体
authentication当前认证对象

JWT 认证

添加依赖

<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.13.0</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.13.0</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.13.0</version>
<scope>runtime</scope>
</dependency>

JWT 工具类

JJWT 0.13.x 版本提供了更简洁的API。以下是推荐的实现方式:

import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import javax.crypto.SecretKey;

@Component
public class JwtTokenProvider {

// 密钥至少需要256位(32字节)用于HS256算法
// 生产环境应从安全配置中心或环境变量获取
private final SecretKey secretKey;

@Value("${jwt.expiration:86400000}")
private long jwtExpiration; // 默认 24 小时

public JwtTokenProvider(@Value("${jwt.secret:}") String jwtSecret) {
// 将Base64编码的密钥转换为SecretKey
// 生产环境建议使用 Keys.secretKeyFor() 生成强密钥
this.secretKey = Keys.hmacShaKeyFor(Decoders.BASE64.decode(jwtSecret));
}

/**
* 生成 JWT Token
*
* @param userDetails 用户详情
* @return JWT字符串
*/
public String generateToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>();
// 可以添加自定义声明
claims.put("roles", userDetails.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.toList()));

return Jwts.builder()
.claims(claims)
.subject(userDetails.getUsername())
.issuedAt(new Date())
.expiration(new Date(System.currentTimeMillis() + jwtExpiration))
.signWith(secretKey)
.compact();
}

/**
* 从 Token 中获取用户名
*
* @param token JWT字符串
* @return 用户名
*/
public String getUsernameFromToken(String token) {
return Jwts.parser()
.verifyWith(secretKey)
.build()
.parseSignedClaims(token)
.getPayload()
.getSubject();
}

/**
* 获取Token中的所有声明
*/
public Claims getClaimsFromToken(String token) {
return Jwts.parser()
.verifyWith(secretKey)
.build()
.parseSignedClaims(token)
.getPayload();
}

/**
* 验证 Token
*
* @param token JWT字符串
* @param userDetails 用户详情
* @return 是否有效
*/
public boolean validateToken(String token, UserDetails userDetails) {
try {
final String username = getUsernameFromToken(token);
return username.equals(userDetails.getUsername()) && !isTokenExpired(token);
} catch (JwtException e) {
// Token无效或已过期
return false;
}
}

/**
* 检查Token是否过期
*/
private boolean isTokenExpired(String token) {
try {
Date expiration = Jwts.parser()
.verifyWith(secretKey)
.build()
.parseSignedClaims(token)
.getPayload()
.getExpiration();
return expiration.before(new Date());
} catch (JwtException e) {
return true;
}
}
}

密钥生成建议

生产环境应使用强密钥。可以通过以下方式生成:

// 生成一个安全的随机密钥(只需执行一次,然后保存到配置中)
SecretKey key = Jwts.SIG.HS256.key().build();
String encodedKey = Encoders.BASE64.encode(key.getEncoded());
// 将encodedKey保存到配置文件或环境变量中

JWT 认证过滤器

@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {

@Autowired
private JwtTokenProvider jwtTokenProvider;

@Autowired
private UserDetailsService userDetailsService;

@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {

String token = getJwtFromRequest(request);

if (StringUtils.hasText(token) && jwtTokenProvider.validateToken(token)) {
String username = jwtTokenProvider.getUsernameFromToken(token);

UserDetails userDetails = userDetailsService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));

SecurityContextHolder.getContext().setAuthentication(authentication);
}

filterChain.doFilter(request, response);
}

private String getJwtFromRequest(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
}

JWT 安全配置

@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class JwtSecurityConfig {

@Autowired
private JwtAuthenticationFilter jwtAuthenticationFilter;

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**").permitAll()
.requestMatchers("/api/public/**").permitAll()
.anyRequest().authenticated()
)
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);

return http.build();
}
}

认证控制器

@RestController
@RequestMapping("/api/auth")
public class AuthController {

@Autowired
private AuthenticationManager authenticationManager;

@Autowired
private JwtTokenProvider jwtTokenProvider;

@Autowired
private UserDetailsService userDetailsService;

@Autowired
private UserService userService;

/**
* 用户登录
*/
@PostMapping("/login")
public Result<AuthResponse> login(@RequestBody LoginRequest loginRequest) {
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
loginRequest.getUsername(),
loginRequest.getPassword()
)
);

SecurityContextHolder.getContext().setAuthentication(authentication);

UserDetails userDetails = (UserDetails) authentication.getPrincipal();
String token = jwtTokenProvider.generateToken(userDetails);

return Result.success(new AuthResponse(token, userDetails.getUsername()));
}

/**
* 用户注册
*/
@PostMapping("/register")
public Result<User> register(@RequestBody @Valid RegisterRequest registerRequest) {
User user = userService.register(registerRequest);
return Result.success(user);
}

/**
* 获取当前用户信息
*/
@GetMapping("/me")
public Result<UserInfo> getCurrentUser() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
// 返回用户信息
return Result.success(userService.getUserInfo(userDetails.getUsername()));
}
}

CSRF 防护

理解 CSRF

CSRF(Cross-Site Request Forgery,跨站请求伪造)是一种攻击方式:

┌─────────────────────────────────────────────────────────────┐
│ CSRF 攻击流程 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. 用户登录银行网站,获取 Cookie │
│ │ │
│ 2. 用户访问恶意网站 │
│ │ │
│ 3. 恶意网站发起请求到银行网站(携带用户的 Cookie) │
│ │ │
│ 4. 银行网站验证 Cookie 成功,执行转账操作 │
│ │
│ 防护方式: │
│ - CSRF Token │
│ - SameSite Cookie │
│ - 验证 Referer/Origin 头 │
│ │
└─────────────────────────────────────────────────────────────┘

配置 CSRF

@Configuration
@EnableWebSecurity
public class CsrfSecurityConfig {

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// 启用 CSRF(默认启用)
.csrf(csrf -> csrf
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
)
// 或者对特定端点禁用
.csrf(csrf -> csrf
.ignoringRequestMatchers("/api/**")
);

return http.build();
}
}

前端处理 CSRF Token

// 从 Cookie 获取 CSRF Token
function getCsrfToken() {
const cookies = document.cookie.split(';');
for (let cookie of cookies) {
const [name, value] = cookie.trim().split('=');
if (name === 'XSRF-TOKEN') {
return decodeURIComponent(value);
}
}
return null;
}

// 请求时携带 CSRF Token
fetch('/api/data', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-XSRF-TOKEN': getCsrfToken()
},
body: JSON.stringify(data)
});

CORS 配置

全局 CORS 配置

@Configuration
public class CorsConfig {

@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList("http://localhost:3000", "https://example.com"));
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
configuration.setAllowedHeaders(Arrays.asList("*"));
configuration.setAllowCredentials(true);
configuration.setMaxAge(3600L);

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

在 Security 中配置 CORS

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
// ...其他配置
;
return http.build();
}

方法级权限

自定义权限评估器

@Component("orderService")
public class OrderService {

/**
* 检查是否是订单所有者
*/
public boolean isOwner(Long orderId, String username) {
Order order = orderRepository.findById(orderId).orElse(null);
return order != null && order.getUsername().equals(username);
}
}

使用自定义权限

@RestController
@RequestMapping("/api/orders")
public class OrderController {

@PreAuthorize("@orderService.isOwner(#id, authentication.principal.username)")
@PutMapping("/{id}")
public Result<Order> updateOrder(@PathVariable Long id, @RequestBody OrderDTO orderDTO) {
// 只有订单所有者可以更新
return Result.success(orderService.update(id, orderDTO));
}
}

会话管理

配置会话

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED) // 按需创建
.maximumSessions(1) // 每个用户最多 1 个会话
.maxSessionsPreventsLogin(true) // 阻止新登录
.expiredUrl("/login?expired") // 会话过期跳转
);

return http.build();
}

会话创建策略

策略说明
IF_REQUIRED按需创建(默认)
ALWAYS总是创建会话
NEVER从不创建会话
STATELESS无状态(JWT 场景)

安全最佳实践

1. 密码安全

// 使用 BCrypt 加密
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(); // 默认强度 10
}

// 或使用更强的加密
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(12); // 强度 12
}

2. HTTPS 强制

@Configuration
public class SslConfig {

@Bean
public ServletWebServerFactory servletContainer() {
TomcatServletWebServerFactory tomcat = new TomcatServletWebServerFactory() {
@Override
protected void postProcessContext(Context context) {
SecurityConstraint securityConstraint = new SecurityConstraint();
securityConstraint.setUserConstraint("CONFIDENTIAL");
SecurityCollection collection = new SecurityCollection();
collection.addPattern("/*");
securityConstraint.addCollection(collection);
context.addConstraint(securityConstraint);
}
};
tomcat.addAdditionalTomcatConnectors(redirectConnector());
return tomcat;
}

private Connector redirectConnector() {
Connector connector = new Connector("org.apache.coyote.http11.Http11NioProtocol");
connector.setScheme("http");
connector.setPort(8080);
connector.setSecure(false);
connector.setRedirectPort(8443);
return connector;
}
}

3. 安全响应头

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.headers(headers -> headers
.contentTypeOptions(contentTypeOptions -> contentTypeOptions.disable())
.xssProtection(xss -> xss.disable())
.frameOptions(frame -> frame.sameOrigin())
.contentSecurityPolicy(csp -> csp
.policyDirectives("default-src 'self'; script-src 'self' 'unsafe-inline'")
)
);

return http.build();
}

4. 输入验证

@Data
public class UserDTO {

@NotBlank(message = "用户名不能为空")
@Size(min = 3, max = 20, message = "用户名长度必须在3-20个字符之间")
@Pattern(regexp = "^[a-zA-Z0-9_]+$", message = "用户名只能包含字母、数字和下划线")
private String username;

@NotBlank(message = "密码不能为空")
@Size(min = 8, max = 100, message = "密码长度必须在8-100个字符之间")
private String password;

@NotBlank(message = "邮箱不能为空")
@Email(message = "邮箱格式不正确")
private String email;
}

OAuth2 资源服务器

当需要与第三方OAuth2认证服务器(如Keycloak、Auth0、Azure AD)集成时,可以使用Spring Security的OAuth2资源服务器支持。

添加依赖

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>

配置资源服务器

spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: https://your-auth-server.com
# 或直接指定公钥
# jwk-set-uri: https://your-auth-server.com/.well-known/jwks.json

资源服务器配置类

@Configuration
@EnableWebSecurity
@EnableMethodSecurity
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(Customizer.withDefaults())
);

return http.build();
}
}

获取用户信息

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

@GetMapping("/me")
public Map<String, Object> getCurrentUser(@AuthenticationPrincipal Jwt jwt) {
Map<String, Object> result = new HashMap<>();
result.put("subject", jwt.getSubject());
result.put("claims", jwt.getClaims());
return result;
}

@GetMapping("/roles")
public Collection<String> getRoles(@AuthenticationPrincipal Jwt jwt) {
// 从JWT中提取角色信息(假设角色存储在"roles"声明中)
return jwt.getClaimAsStringList("roles");
}
}

JWT中的权限提取

@Configuration
public class JwtConverterConfig {

@Bean
public JwtAuthenticationConverter jwtAuthenticationConverter() {
JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
// 设置JWT中权限的声明名称
converter.setJwtGrantedAuthoritiesConverter(jwt -> {
Collection<GrantedAuthority> authorities = new ArrayList<>();

// 从"roles"声明中提取角色
List<String> roles = jwt.getClaimAsStringList("roles");
if (roles != null) {
roles.forEach(role ->
authorities.add(new SimpleGrantedAuthority("ROLE_" + role))
);
}

// 从"scope"声明中提取权限
String scope = jwt.getClaim("scope");
if (scope != null) {
Arrays.stream(scope.split(" "))
.forEach(s -> authorities.add(new SimpleGrantedAuthority(s)));
}

return authorities;
});
return converter;
}
}

自定义JWT解码器

@Configuration
public class JwtDecoderConfig {

@Value("${jwt.secret}")
private String jwtSecret;

@Bean
public JwtDecoder jwtDecoder() {
// 使用对称密钥解码JWT
SecretKey key = Keys.hmacShaKeyFor(Decoders.BASE64.decode(jwtSecret));
return NimbusJwtDecoder.withSecretKey(key).build();
}
}

资源服务器与JWT认证组合

可以同时支持OAuth2资源服务器和自定义JWT认证:

@Configuration
@EnableWebSecurity
public class CombinedSecurityConfig {

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/public/**").permitAll()
.anyRequest().authenticated()
)
// OAuth2资源服务器
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(Customizer.withDefaults())
)
// 同时支持表单登录
.formLogin(form -> form
.loginPage("/login")
.permitAll()
);

return http.build();
}
}

安全最佳实践

1. 密码安全

// 使用 BCrypt 加密
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(); // 默认强度 10
}

// 或使用更强的加密
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(12); // 强度 12
}

密码存储建议

策略说明
BCrypt默认推荐,自适应哈希
SCrypt更安全但更耗资源
PBKDF2标准算法,可配置迭代次数
Argon2最安全,但需要额外依赖

2. JWT密钥管理

// 推荐:使用强密钥生成
SecretKey key = Jwts.SIG.HS256.key().build(); // 自动生成256位密钥

// 或从安全配置中心获取
@Value("${jwt.secret}")
private String jwtSecret; // 必须是Base64编码的至少256位密钥

3. HTTPS 强制

@Configuration
public class SslConfig {

@Bean
public ServletWebServerFactory servletContainer() {
TomcatServletWebServerFactory tomcat = new TomcatServletWebServerFactory() {
@Override
protected void postProcessContext(Context context) {
SecurityConstraint securityConstraint = new SecurityConstraint();
securityConstraint.setUserConstraint("CONFIDENTIAL");
SecurityCollection collection = new SecurityCollection();
collection.addPattern("/*");
securityConstraint.addCollection(collection);
context.addConstraint(securityConstraint);
}
};
tomcat.addAdditionalTomcatConnectors(redirectConnector());
return tomcat;
}

private Connector redirectConnector() {
Connector connector = new Connector("org.apache.coyote.http11.Http11NioProtocol");
connector.setScheme("http");
connector.setPort(8080);
connector.setSecure(false);
connector.setRedirectPort(8443);
return connector;
}
}

4. 安全响应头

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.headers(headers -> headers
.contentTypeOptions(contentTypeOptions -> contentTypeOptions.disable())
.xssProtection(xss -> xss.disable())
.frameOptions(frame -> frame.sameOrigin())
.contentSecurityPolicy(csp -> csp
.policyDirectives("default-src 'self'; script-src 'self' 'unsafe-inline'")
)
);

return http.build();
}

5. 输入验证

@Data
public class UserDTO {

@NotBlank(message = "用户名不能为空")
@Size(min = 3, max = 20, message = "用户名长度必须在3-20个字符之间")
@Pattern(regexp = "^[a-zA-Z0-9_]+$", message = "用户名只能包含字母、数字和下划线")
private String username;

@NotBlank(message = "密码不能为空")
@Size(min = 8, max = 100, message = "密码长度必须在8-100个字符之间")
private String password;

@NotBlank(message = "邮箱不能为空")
@Email(message = "邮箱格式不正确")
private String email;
}

6. 敏感信息保护

// 不要在JWT中存储敏感信息
// 错误
claims.put("password", user.getPassword());

// 正确:只存储必要的标识信息
claims.put("userId", user.getId());
claims.put("roles", user.getRoles());

小结

本章我们学习了:

  1. 安全框架概述:认证和授权的基本概念,核心架构和组件
  2. 认证机制:UserDetailsService、密码编码、认证流程
  3. 授权机制:方法级安全、SpEL 表达式、自定义权限评估器
  4. JWT 认证:Token 生成和验证、认证过滤器和安全配置
  5. OAuth2 资源服务器:与第三方认证服务器集成、权限提取
  6. CSRF 防护:理解和配置CSRF保护
  7. CORS 配置:跨域请求处理
  8. 最佳实践:密码安全、JWT密钥管理、HTTPS、安全响应头

练习

  1. 实现一个基于数据库的用户认证系统
  2. 集成 JWT 实现无状态认证
  3. 使用方法级权限控制 API 访问
  4. 配置 CORS 允许前端跨域访问
  5. 配置OAuth2资源服务器与Keycloak集成

参考资源