Spring Security 安全框架详解
1. 概述
1.1 什么是 Spring Security
Spring Security 是一个功能强大且高度可定制的身份验证和访问控制框架。它是 Spring 生态系统中的安全标准,为基于 Spring 的应用程序提供全面的安全解决方案。
主要特性:
- 🔐 身份认证(Authentication)
- 🛡️ 授权控制(Authorization)
- 🔒 会话管理(Session Management)
- 🛠️ 攻击防护(Attack Prevention)
- 🔧 高度可定制(Highly Customizable)
2. 核心概念
2.1 安全上下文(SecurityContext)
SecurityContext 是 Spring Security 的核心概念,它包含了当前用户的认证信息。
// SecurityContext结构
public interface SecurityContext {
Authentication getAuthentication();
void setAuthentication(Authentication authentication);
}
2.2 认证(Authentication)
认证是验证用户身份的过程,确认"你是谁"。
public interface Authentication {
// 用户主体信息
Object getPrincipal();
// 用户凭证
Object getCredentials();
// 用户权限
Collection<? extends GrantedAuthority> getAuthorities();
// 认证详情
Object getDetails();
// 是否已认证
boolean isAuthenticated();
// 设置认证状态
void setAuthenticated(boolean isAuthenticated);
}
2.3 授权(Authorization)
授权是控制用户访问权限的过程,确认"你能做什么"。
// 权限接口
public interface GrantedAuthority {
String getAuthority();
}
// 角色接口
public interface Role extends GrantedAuthority {
String getRoleName();
}
2.4 用户详情(UserDetails)
UserDetails 接口定义了用户的核心信息。
public interface UserDetails {
String getUsername();
String getPassword();
boolean isAccountNonExpired();
boolean isAccountNonLocked();
boolean isCredentialsNonExpired();
boolean isEnabled();
Collection<? extends GrantedAuthority> getAuthorities();
}
3. 架构原理
3.1 整体架构
Spring Security 的整体架构采用了分层设计模式,构建了一个完整的安全防护体系。该架构从客户端层开始,经过多个安全处理层,最终到达用户存储和访问决策层,形成了一个完整的安全处理链路。
客户端层是架构的入口点,包括 Web 浏览器、移动应用和 API 客户端等多种类型的客户端。这些客户端通过 HTTP 请求与应用程序进行交互,每个请求都会经过 Spring Security 的安全处理流程。
Spring Security 过滤器链是安全处理的核心组件,它由多个专门的过滤器组成。SecurityFilterChain 作为过滤器链的容器,协调各个过滤器的执行顺序。AuthenticationFilter 负责处理认证相关的逻辑,提取和验证用户的身份信息。AuthorizationFilter 负责处理授权逻辑,判断用户是否有权限访问特定资源。SessionManagementFilter 则负责会话管理,包括会话的创建、维护和销毁。
认证管理器是认证流程的控制中心,AuthenticationManager 作为认证管理器的主要接口,定义了认证的标准流程。ProviderManager 是 AuthenticationManager 的具体实现,它管理多个 AuthenticationProvider 实例。AuthenticationProvider 负责具体的认证逻辑,可以支持多种认证方式,如用户名密码认证、OAuth2 认证等。
用户存储层提供了用户信息的持久化方案。UserDetailsService 是用户信息服务的核心接口,负责加载用户的详细信息。用户信息可以存储在不同的数据源中,包括关系型数据库、LDAP 目录服务和内存存储等。这种设计使得系统可以灵活地选择用户存储方案,适应不同的部署环境。
访问决策层负责最终的权限判断。AccessDecisionManager 是访问决策管理器,它协调多个 Voter(投票器)来做出最终的访问决策。RoleVoter 是基于角色的投票器,根据用户的角色来判断是否有访问权限。这种投票机制使得权限判断更加灵活和可扩展。
整个架构通过清晰的层次划分和组件职责分离,实现了安全功能的模块化和可扩展性。每个层次都有明确的职责,通过标准化的接口进行交互,使得系统既保证了安全性,又具备了良好的可维护性和可扩展性。
3.2 过滤器链原理
Spring Security 基于 Servlet 过滤器链实现安全控制:
// 过滤器链配置示例
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authz -> authz
.requestMatchers("/public/**").permitAll()
.requestMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.formLogin(form -> form
.loginPage("/login")
.permitAll()
)
.logout(logout -> logout
.logoutSuccessUrl("/")
);
return http.build();
}
}
3.3 认证流程
Spring Security 的认证流程是一个精心设计的交互过程,它确保了用户身份验证的安全性和可靠性。整个流程从客户端发起请求开始,经过多个组件的协作处理,最终完成用户的身份认证。
客户端发起请求是认证流程的起点。当用户尝试访问受保护的资源时,客户端会向服务器发送包含认证信息的请求。这些认证信息可能是用户名和密码、JWT 令牌、OAuth2 令牌等多种形式。
SecurityFilter 提取认证信息是流程的第一个处理步骤。过滤器会从请求中提取认证相关的信息,如 HTTP 头中的 Authorization 字段、Cookie 中的会话标识符等。提取的信息会被封装成 Authentication 对象,为后续的认证处理做准备。
AuthenticationManager 创建认证对象是认证流程的核心步骤。认证管理器接收从过滤器传来的认证信息,创建一个标准的 Authentication 对象。这个对象包含了用户的身份信息、凭证信息以及相关的元数据。
AuthenticationProvider 执行认证是实际进行身份验证的环节。认证提供者接收 Authentication 对象,根据配置的认证策略进行身份验证。它可能会调用 UserDetailsService 来加载用户的详细信息,然后验证用户提供的凭证是否正确。
UserDetailsService 加载用户信息是获取用户详细信息的步骤。用户详情服务根据用户名或其他标识符从数据源中加载用户的完整信息,包括密码哈希、权限列表、账户状态等。这些信息对于后续的密码验证和权限判断至关重要。
密码验证和认证结果返回是认证流程的关键步骤。认证提供者将用户输入的密码与数据库中存储的密码哈希进行比对。如果验证成功,认证提供者会返回一个包含用户权限信息的完整 Authentication 对象。如果验证失败,则会抛出相应的异常。
SecurityContext 设置和响应返回是认证流程的最后步骤。认证成功后,用户的认证信息会被设置到 SecurityContext 中,这样后续的请求处理就可以通过 SecurityContext 获取到当前用户的身份和权限信息。最后,过滤器将处理结果返回给客户端,完成整个认证流程。
这个认证流程设计确保了认证过程的安全性和可靠性,通过多个组件的协作,实现了对用户身份的准确验证和权限的有效管理。
4. 认证机制
4.1 表单登录认证
@Configuration
@EnableWebSecurity
public class FormLoginConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.formLogin(form -> form
.loginPage("/login") // 自定义登录页
.loginProcessingUrl("/auth") // 登录处理URL
.defaultSuccessUrl("/dashboard") // 登录成功跳转
.failureUrl("/login?error") // 登录失败跳转
.usernameParameter("username") // 用户名参数
.passwordParameter("password") // 密码参数
.permitAll()
);
return http.build();
}
}
4.2 HTTP Basic 认证
@Configuration
@EnableWebSecurity
public class BasicAuthConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.httpBasic(basic -> basic
.realmName("My Application")
)
.authorizeHttpRequests(authz -> authz
.anyRequest().authenticated()
);
return http.build();
}
}
4.3 JWT 认证
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private JwtTokenProvider tokenProvider;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
try {
String jwt = getJwtFromRequest(request);
if (StringUtils.hasText(jwt) && tokenProvider.validateToken(jwt)) {
String userId = tokenProvider.getUserIdFromJWT(jwt);
UserDetails userDetails = customUserDetailsService.loadUserById(userId);
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
} catch (Exception ex) {
logger.error("Could not set user authentication in security context", ex);
}
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;
}
}
4.4 OAuth2 认证
@Configuration
@EnableWebSecurity
public class OAuth2Config {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.oauth2Login(oauth2 -> oauth2
.loginPage("/login")
.defaultSuccessUrl("/dashboard")
.failureUrl("/login?error")
.userInfoEndpoint(userInfo -> userInfo
.userService(customOAuth2UserService)
)
);
return http.build();
}
}
5. 授权机制
5.1 基于角色的访问控制(RBAC)
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class RoleBasedConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authz -> authz
.requestMatchers("/public/**").permitAll()
.requestMatchers("/user/**").hasRole("USER")
.requestMatchers("/admin/**").hasRole("ADMIN")
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
);
return http.build();
}
}
5.2 基于注解的授权
@RestController
@RequestMapping("/api")
public class UserController {
// 需要USER角色
@PreAuthorize("hasRole('USER')")
@GetMapping("/profile")
public ResponseEntity<UserProfile> getProfile() {
return ResponseEntity.ok(userService.getCurrentUserProfile());
}
// 需要ADMIN角色
@PreAuthorize("hasRole('ADMIN')")
@GetMapping("/users")
public ResponseEntity<List<User>> getAllUsers() {
return ResponseEntity.ok(userService.getAllUsers());
}
// 自定义权限表达式
@PreAuthorize("hasAuthority('USER_DELETE') and #id != authentication.principal.id")
@DeleteMapping("/users/{id}")
public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
userService.deleteUser(id);
return ResponseEntity.ok().build();
}
// 基于方法参数的权限控制
@PreAuthorize("#user.id == authentication.principal.id or hasRole('ADMIN')")
@PutMapping("/users/{id}")
public ResponseEntity<User> updateUser(@PathVariable Long id, @RequestBody User user) {
return ResponseEntity.ok(userService.updateUser(user));
}
}
5.3 自定义权限评估器
@Component
public class CustomPermissionEvaluator implements PermissionEvaluator {
@Override
public boolean hasPermission(Authentication authentication, Object targetDomainObject, Object permission) {
if (authentication == null || targetDomainObject == null || !(permission instanceof String)) {
return false;
}
String targetType = targetDomainObject.getClass().getSimpleName().toUpperCase();
return hasPrivilege(authentication, targetType, permission.toString().toUpperCase());
}
@Override
public boolean hasPermission(Authentication authentication, Serializable targetId, String targetType, Object permission) {
if (authentication == null || targetType == null || !(permission instanceof String)) {
return false;
}
return hasPrivilege(authentication, targetType.toUpperCase(), permission.toString().toUpperCase());
}
private boolean hasPrivilege(Authentication auth, String targetType, String permission) {
for (GrantedAuthority grantedAuth : auth.getAuthorities()) {
if (grantedAuth.getAuthority().startsWith(targetType) &&
grantedAuth.getAuthority().contains(permission)) {
return true;
}
}
return false;
}
}
6. 会话管理
6.1 会话配置
@Configuration
@EnableWebSecurity
public class SessionConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
.maximumSessions(1) // 最大会话数
.expiredUrl("/login?expired") // 会话过期跳转
.and()
.sessionFixation().migrateSession() // 会话固定攻击防护
.and()
.invalidSessionUrl("/login?invalid") // 无效会话跳转
);
return http.build();
}
}
6.2 并发会话控制
@Component
public class CustomSessionRegistry implements SessionRegistry {
private final Map<String, List<SessionInformation>> sessionMap = new ConcurrentHashMap<>();
@Override
public List<Object> getAllPrincipals() {
return new ArrayList<>(sessionMap.keySet());
}
@Override
public List<SessionInformation> getAllSessions(Object principal, boolean includeExpiredSessions) {
List<SessionInformation> sessions = sessionMap.get(principal.toString());
if (sessions == null) {
return new ArrayList<>();
}
if (includeExpiredSessions) {
return new ArrayList<>(sessions);
}
return sessions.stream()
.filter(session -> !session.isExpired())
.collect(Collectors.toList());
}
@Override
public SessionInformation getSessionInformation(String sessionId) {
return sessionMap.values().stream()
.flatMap(List::stream)
.filter(session -> session.getSessionId().equals(sessionId))
.findFirst()
.orElse(null);
}
@Override
public void refreshLastRequest(String sessionId) {
SessionInformation session = getSessionInformation(sessionId);
if (session != null) {
session.refreshLastRequest();
}
}
@Override
public void registerNewSession(String sessionId, Object principal) {
SessionInformation session = new SessionInformation(principal, sessionId, new Date());
sessionMap.computeIfAbsent(principal.toString(), k -> new ArrayList<>()).add(session);
}
@Override
public void removeSessionInformation(String sessionId) {
sessionMap.values().forEach(sessions ->
sessions.removeIf(session -> session.getSessionId().equals(sessionId))
);
}
}
7. 密码加密
7.1 密码编码器
@Configuration
public class PasswordEncoderConfig {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(12); // 强度为12
}
// 多种编码器支持
@Bean
public DelegatingPasswordEncoder delegatingPasswordEncoder() {
Map<String, PasswordEncoder> encoders = new HashMap<>();
encoders.put("bcrypt", new BCryptPasswordEncoder());
encoders.put("scrypt", new SCryptPasswordEncoder());
encoders.put("pbkdf2", new Pbkdf2PasswordEncoder());
return new DelegatingPasswordEncoder("bcrypt", encoders);
}
}
7.2 密码验证流程
Spring Security 的密码验证流程是一个多步骤的安全处理过程,它确保了用户密码的安全性和验证的准确性。整个流程从用户输入密码开始,经过多个安全处理环节,最终完成密码的验证。
用户输入密码是密码验证流程的起点。用户在登录界面输入用户名和密码,这些信息会被前端收集并准备发送到服务器。为了确保传输安全,前端可能会对密码进行初步的加密处理,如使用 HTTPS 传输或进行客户端加密。
前端加密处理是密码安全传输的重要环节。前端可能会对用户输入的密码进行加密处理,如使用 JavaScript 加密库对密码进行哈希处理,或者使用 HTTPS 协议确保传输过程中的安全性。这一步的目的是防止密码在传输过程中被窃取。
传输到服务器是密码信息的安全传输过程。加密后的密码信息通过安全的网络连接传输到服务器端。服务器接收这些信息后,会进行进一步的验证和处理。
Spring Security 接收处理是服务器端的安全处理环节。Spring Security 接收到包含密码信息的请求后,会通过配置的认证过滤器提取密码信息,并将其传递给相应的认证提供者进行处理。
PasswordEncoder 验证是密码验证的核心步骤。Spring Security 使用配置的密码编码器来验证用户输入的密码。密码编码器会将用户输入的原始密码与数据库中存储的加密密码进行比对,判断密码是否正确。
密码匹配判断是验证结果的决策环节。如果用户输入的密码与数据库中存储的密码匹配,则认证成功,用户可以继续访问受保护的资源。如果密码不匹配,则认证失败,系统会返回相应的错误信息。
认证结果处理是流程的最后步骤。根据密码验证的结果,系统会执行相应的后续处理。认证成功时,系统会创建用户的认证会话,并允许用户访问受保护的资源。认证失败时,系统会记录失败信息,并可能触发安全策略,如账户锁定或登录尝试限制。
这个密码验证流程设计确保了密码验证的安全性和可靠性,通过多层的安全处理,有效防止了密码泄露和未授权访问的风险。
7.3 自定义密码编码器
@Component
public class CustomPasswordEncoder implements PasswordEncoder {
private static final String SALT = "MyCustomSalt";
@Override
public String encode(CharSequence rawPassword) {
try {
MessageDigest md = MessageDigest.getInstance("SHA-256");
String saltedPassword = rawPassword + SALT;
byte[] hashedBytes = md.digest(saltedPassword.getBytes());
return Base64.getEncoder().encodeToString(hashedBytes);
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("Error encoding password", e);
}
}
@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
String encodedRawPassword = encode(rawPassword);
return encodedRawPassword.equals(encodedPassword);
}
}
8. 实战示例
8.1 完整的 Spring Security 配置
@Configuration
@EnableWebSecurity
@EnableMethodSecurity(prePostEnabled = true)
public class SecurityConfiguration {
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
@Autowired
private JwtRequestFilter jwtRequestFilter;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig) throws Exception {
return authConfig.getAuthenticationManager();
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(authz -> authz
.requestMatchers("/api/auth/**").permitAll()
.requestMatchers("/api/public/**").permitAll()
.requestMatchers("/swagger-ui/**", "/v3/api-docs/**").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.requestMatchers("/api/user/**").hasRole("USER")
.anyRequest().authenticated()
)
.exceptionHandling(exceptions -> exceptions
.authenticationEntryPoint(jwtAuthenticationEntryPoint)
)
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}
8.2 用户实体和 DTO
@Entity
@Table(name = "users")
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;
@Enumerated(EnumType.STRING)
private UserStatus status = UserStatus.ACTIVE;
@ManyToMany(fetch = FetchType.EAGER)
@JoinTable(
name = "user_roles",
joinColumns = @JoinColumn(name = "user_id"),
inverseJoinColumns = @JoinColumn(name = "role_id")
)
private Set<Role> roles = new HashSet<>();
// getters and setters
}
@Entity
@Table(name = "roles")
public class Role {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Enumerated(EnumType.STRING)
@Column(unique = true, nullable = false)
private RoleName name;
// getters and setters
}
public enum RoleName {
ROLE_USER,
ROLE_ADMIN,
ROLE_MODERATOR
}
public enum UserStatus {
ACTIVE,
INACTIVE,
LOCKED
}
8.3 认证控制器
@RestController
@RequestMapping("/api/auth")
@Validated
public class AuthController {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private UserRepository userRepository;
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private JwtTokenProvider jwtTokenProvider;
@PostMapping("/login")
public ResponseEntity<JwtAuthenticationResponse> authenticateUser(@Valid @RequestBody LoginRequest loginRequest) {
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
loginRequest.getUsername(),
loginRequest.getPassword()
)
);
SecurityContextHolder.getContext().setAuthentication(authentication);
String jwt = jwtTokenProvider.generateToken(authentication);
return ResponseEntity.ok(new JwtAuthenticationResponse(jwt));
}
@PostMapping("/register")
public ResponseEntity<ApiResponse> registerUser(@Valid @RequestBody SignUpRequest signUpRequest) {
if (userRepository.existsByUsername(signUpRequest.getUsername())) {
return ResponseEntity.badRequest()
.body(new ApiResponse(false, "用户名已存在"));
}
if (userRepository.existsByEmail(signUpRequest.getEmail())) {
return ResponseEntity.badRequest()
.body(new ApiResponse(false, "邮箱已存在"));
}
User user = new User();
user.setUsername(signUpRequest.getUsername());
user.setEmail(signUpRequest.getEmail());
user.setPassword(passwordEncoder.encode(signUpRequest.getPassword()));
Role userRole = roleRepository.findByName(RoleName.ROLE_USER)
.orElseThrow(() -> new RuntimeException("用户角色未找到"));
user.setRoles(Collections.singleton(userRole));
userRepository.save(user);
return ResponseEntity.ok(new ApiResponse(true, "用户注册成功"));
}
@GetMapping("/me")
@PreAuthorize("hasRole('USER')")
public ResponseEntity<UserSummary> getCurrentUser(@CurrentUser UserPrincipal currentUser) {
UserSummary userSummary = new UserSummary(
currentUser.getId(),
currentUser.getUsername(),
currentUser.getEmail()
);
return ResponseEntity.ok(userSummary);
}
}
8.4 JWT 工具类
@Component
public class JwtTokenProvider {
@Value("${app.jwtSecret}")
private String jwtSecret;
@Value("${app.jwtExpirationInMs}")
private int jwtExpirationInMs;
public String generateToken(Authentication authentication) {
UserPrincipal userPrincipal = (UserPrincipal) authentication.getPrincipal();
Date now = new Date();
Date expiryDate = new Date(now.getTime() + jwtExpirationInMs);
return Jwts.builder()
.setSubject(Long.toString(userPrincipal.getId()))
.setIssuedAt(new Date())
.setExpiration(expiryDate)
.signWith(SignatureAlgorithm.HS512, jwtSecret)
.compact();
}
public Long getUserIdFromJWT(String token) {
Claims claims = Jwts.parser()
.setSigningKey(jwtSecret)
.parseClaimsJws(token)
.getBody();
return Long.parseLong(claims.getSubject());
}
public boolean validateToken(String authToken) {
try {
Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(authToken);
return true;
} catch (SignatureException ex) {
logger.error("Invalid JWT signature");
} catch (MalformedJwtException ex) {
logger.error("Invalid JWT token");
} catch (ExpiredJwtException ex) {
logger.error("Expired JWT token");
} catch (UnsupportedJwtException ex) {
logger.error("Unsupported JWT token");
} catch (IllegalArgumentException ex) {
logger.error("JWT claims string is empty");
}
return false;
}
}
9. 最佳实践
9.1 安全配置最佳实践
@Configuration
@EnableWebSecurity
public class SecurityBestPractices {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
// 1. 禁用CSRF(仅用于API)
.csrf(csrf -> csrf.disable())
// 2. 配置CORS
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
// 3. 配置授权
.authorizeHttpRequests(authz -> authz
.requestMatchers("/api/public/**").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
// 4. 配置异常处理
.exceptionHandling(exceptions -> exceptions
.authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED))
.accessDeniedHandler(new HttpStatusEntryPoint(HttpStatus.FORBIDDEN))
)
// 5. 配置会话管理
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
// 6. 添加安全头
.headers(headers -> headers
.frameOptions().deny()
.contentTypeOptions().and()
.httpStrictTransportSecurity(hstsConfig -> hstsConfig
.maxAgeInSeconds(31536000)
.includeSubdomains(true)
)
);
return http.build();
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOriginPatterns(Arrays.asList("*"));
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
configuration.setAllowedHeaders(Arrays.asList("*"));
configuration.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}
9.2 密码安全最佳实践
@Component
public class PasswordSecurityService {
@Autowired
private PasswordEncoder passwordEncoder;
public boolean isPasswordStrong(String password) {
// 至少8个字符
if (password.length() < 8) {
return false;
}
// 包含大小写字母、数字和特殊字符
boolean hasUpper = password.matches(".*[A-Z].*");
boolean hasLower = password.matches(".*[a-z].*");
boolean hasNumber = password.matches(".*\\d.*");
boolean hasSpecial = password.matches(".*[!@#$%^&*()_+\\-=\\[\\]{};':\"\\\\|,.<>\\/?].*");
return hasUpper && hasLower && hasNumber && hasSpecial;
}
public String generateSecurePassword() {
String chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()_+-=[]{}|;:,.<>?";
StringBuilder password = new StringBuilder();
Random random = new SecureRandom();
// 确保包含每种字符类型
password.append(chars.charAt(random.nextInt(26))); // 大写字母
password.append(chars.charAt(26 + random.nextInt(26))); // 小写字母
password.append(chars.charAt(52 + random.nextInt(10))); // 数字
password.append(chars.charAt(62 + random.nextInt(30))); // 特殊字符
// 填充剩余字符
for (int i = 4; i < 12; i++) {
password.append(chars.charAt(random.nextInt(chars.length())));
}
// 打乱字符顺序
char[] passwordArray = password.toString().toCharArray();
for (int i = passwordArray.length - 1; i > 0; i--) {
int j = random.nextInt(i + 1);
char temp = passwordArray[i];
passwordArray[i] = passwordArray[j];
passwordArray[j] = temp;
}
return new String(passwordArray);
}
}
9.3 审计日志
@Entity
@Table(name = "audit_logs")
public class AuditLog {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String username;
@Column(nullable = false)
private String action;
@Column(nullable = false)
private String resource;
@Column
private String details;
@Column(nullable = false)
private String ipAddress;
@Column(nullable = false)
private LocalDateTime timestamp;
@Column(nullable = false)
private String userAgent;
// getters and setters
}
@Component
public class AuditService {
@Autowired
private AuditLogRepository auditLogRepository;
public void logEvent(String action, String resource, String details) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
String username = authentication != null ? authentication.getName() : "ANONYMOUS";
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
AuditLog auditLog = new AuditLog();
auditLog.setUsername(username);
auditLog.setAction(action);
auditLog.setResource(resource);
auditLog.setDetails(details);
auditLog.setIpAddress(getClientIpAddress(request));
auditLog.setTimestamp(LocalDateTime.now());
auditLog.setUserAgent(request.getHeader("User-Agent"));
auditLogRepository.save(auditLog);
}
private String getClientIpAddress(HttpServletRequest request) {
String xForwardedFor = request.getHeader("X-Forwarded-For");
if (xForwardedFor != null && !xForwardedFor.isEmpty() && !"unknown".equalsIgnoreCase(xForwardedFor)) {
return xForwardedFor.split(",")[0];
}
return request.getRemoteAddr();
}
}
10. 常见问题
10.1 常见错误及解决方案
| 错误 | 原因 | 解决方案 |
|---|---|---|
Access Denied | 用户权限不足 | 检查用户角色和权限配置 |
Invalid Credentials | 用户名或密码错误 | 验证用户凭据 |
Session Expired | 会话超时 | 重新登录或延长会话时间 |
CSRF Token Missing | CSRF 保护启用但缺少 token | 在表单中添加 CSRF token |
Maximum Sessions Exceeded | 超过最大会话数 | 配置会话管理策略 |
10.2 性能优化建议
@Configuration
public class SecurityPerformanceConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
// 1. 使用无状态会话
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
// 2. 缓存用户信息
.userDetailsService(cachedUserDetailsService())
// 3. 优化密码编码器
.passwordEncoder(passwordEncoder());
return http.build();
}
@Bean
public UserDetailsService cachedUserDetailsService() {
return new CachingUserDetailsService(userDetailsService());
}
@Bean
public PasswordEncoder passwordEncoder() {
// 使用合适的强度,避免过度加密
return new BCryptPasswordEncoder(10);
}
}
10.3 安全测试
@SpringBootTest
@AutoConfigureTestDatabase
class SecurityIntegrationTest {
@Autowired
private TestRestTemplate restTemplate;
@Test
void testPublicEndpointAccess() {
ResponseEntity<String> response = restTemplate.getForEntity("/api/public/hello", String.class);
assertEquals(HttpStatus.OK, response.getStatusCode());
}
@Test
void testProtectedEndpointWithoutAuth() {
ResponseEntity<String> response = restTemplate.getForEntity("/api/user/profile", String.class);
assertEquals(HttpStatus.UNAUTHORIZED, response.getStatusCode());
}
@Test
void testAdminEndpointWithUserRole() {
HttpHeaders headers = new HttpHeaders();
headers.setBearerAuth(createUserToken());
HttpEntity<String> entity = new HttpEntity<>(headers);
ResponseEntity<String> response = restTemplate.exchange(
"/api/admin/users",
HttpMethod.GET,
entity,
String.class
);
assertEquals(HttpStatus.FORBIDDEN, response.getStatusCode());
}
private String createUserToken() {
// 创建用户JWT token的逻辑
return "user-jwt-token";
}
}
总结
Spring Security 是一个功能强大且灵活的安全框架,通过本文档的学习,您应该能够:
- 理解核心概念:认证、授权、会话管理等
- 掌握架构原理:过滤器链、认证流程等
- 实现安全功能:表单登录、JWT 认证、OAuth2 等
- 应用最佳实践:密码安全、审计日志、性能优化等
- 解决常见问题:错误处理、调试技巧等
Spring Security 为 Spring 应用程序提供了全面的安全解决方案,通过合理配置和使用,可以有效保护应用程序免受各种安全威胁。
