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());
小结
本章我们学习了:
- 安全框架概述:认证和授权的基本概念,核心架构和组件
- 认证机制:UserDetailsService、密码编码、认证流程
- 授权机制:方法级安全、SpEL 表达式、自定义权限评估器
- JWT 认证:Token 生成和验证、认证过滤器和安全配置
- OAuth2 资源服务器:与第三方认证服务器集成、权限提取
- CSRF 防护:理解和配置CSRF保护
- CORS 配置:跨域请求处理
- 最佳实践:密码安全、JWT密钥管理、HTTPS、安全响应头
练习
- 实现一个基于数据库的用户认证系统
- 集成 JWT 实现无状态认证
- 使用方法级权限控制 API 访问
- 配置 CORS 允许前端跨域访问
- 配置OAuth2资源服务器与Keycloak集成