1. Spring Security 概述
Spring Security 是一个功能强大且高度可定制的身份验证和访问控制框架,是保护基于 Spring 的应用程序的标准。它专注于为 Java 应用程序提供身份验证和授权。
1.1 Spring Security 的主要特性
- 全面的身份验证和授权支持:支持多种身份验证方式,包括表单登录、HTTP Basic、OAuth2、JWT 等
- 防止常见攻击:内置对 CSRF、XSS、SQL 注入等常见攻击的防护
- 会话管理:提供会话固定攻击防护、会话超时等功能
- 与 Spring 框架无缝集成:与 Spring MVC、Spring Boot 等框架完美配合
- 可扩展性:提供丰富的扩展点,可以根据需求定制安全行为
1.2 Spring Security 的核心组件
- SecurityContextHolder:存储当前用户的安全上下文
- Authentication:表示用户的身份验证信息
- UserDetails:提供核心用户信息
- UserDetailsService:加载特定用户的用户数据
- GrantedAuthority:表示授予用户的权限
- SecurityFilterChain:定义请求处理过程中的过滤器链
Spring Security 默认采用"安全优先"的设计理念,这意味着默认情况下所有资源都是受保护的,需要明确配置才能访问。
2. 快速入门
2.1 添加依赖
在 Maven 项目中添加 Spring Security 依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
在 Gradle 项目中添加依赖:
implementation 'org.springframework.boot:spring-boot-starter-security'
2.2 基本配置
创建一个基本的 Spring Security 配置类:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests((requests) -> requests
.requestMatchers("/", "/home").permitAll()
.anyRequest().authenticated()
)
.formLogin((form) -> form
.loginPage("/login")
.permitAll()
)
.logout((logout) -> logout.permitAll());
return http.build();
}
@Bean
public UserDetailsService userDetailsService() {
UserDetails user = User.withDefaultPasswordEncoder()
.username("user")
.password("password")
.roles("USER")
.build();
return new InMemoryUserDetailsManager(user);
}
}
2.3 测试配置
创建一个简单的控制器来测试安全配置:
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class HomeController {
@GetMapping("/")
public String home() {
return "home";
}
@GetMapping("/hello")
public String hello() {
return "hello";
}
}
上面的示例使用了内存中的用户存储,这在生产环境中是不推荐的。在实际应用中,应该使用数据库或其他持久化存储来管理用户信息。
3. 认证机制
3.1 认证流程
Spring Security 的认证流程如下:
- 用户提交用户名和密码
- Spring Security 的过滤器链处理请求
- AuthenticationManager 验证用户凭据
- 如果验证成功,创建 Authentication 对象并存储在 SecurityContextHolder 中
- 如果验证失败,抛出 AuthenticationException
3.2 自定义认证
实现自定义的 UserDetailsService:
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
public class CustomUserDetailsService implements UserDetailsService {
private final UserRepository userRepository;
public CustomUserDetailsService(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("User not found: " + username));
return org.springframework.security.core.userdetails.User
.withUsername(user.getUsername())
.password(user.getPassword())
.roles(user.getRoles().toArray(new String[0]))
.build();
}
}
3.3 密码编码器
配置密码编码器:
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
3.4 自定义登录页面
配置自定义登录页面:
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests((requests) -> requests
.requestMatchers("/", "/home", "/css/**", "/js/**").permitAll()
.anyRequest().authenticated()
)
.formLogin((form) -> form
.loginPage("/login")
.loginProcessingUrl("/login")
.defaultSuccessUrl("/dashboard")
.failureUrl("/login?error=true")
.permitAll()
)
.logout((logout) -> logout
.logoutUrl("/logout")
.logoutSuccessUrl("/login?logout=true")
.permitAll()
);
return http.build();
}
在生产环境中,应该使用 HTTPS 来保护用户凭据的传输。可以通过配置 `http.requiresChannel().anyRequest().requiresSecure()` 来强制使用 HTTPS。
5. Web 安全
5.1 安全过滤器链
Spring Security 使用过滤器链来保护 Web 应用程序:
- ChannelProcessingFilter:确保请求通过正确的通道(HTTP/HTTPS)
- SecurityContextPersistenceFilter:在请求之间维护 SecurityContext
- ConcurrentSessionFilter:处理并发会话
- UsernamePasswordAuthenticationFilter:处理表单登录
- BasicAuthenticationFilter:处理 HTTP Basic 认证
- RequestCacheAwareFilter:缓存请求以便在认证后重定向
- SecurityContextHolderAwareRequestFilter:将 SecurityContext 绑定到 HttpServletRequest
- JaasApiIntegrationFilter:集成 JAAS
- RememberMeAuthenticationFilter:处理"记住我"功能
- AnonymousAuthenticationFilter:为未认证用户创建匿名 Authentication
- SessionManagementFilter:管理会话
- ExceptionTranslationFilter:处理 Spring Security 异常
- FilterSecurityInterceptor:执行授权决策
5.2 自定义过滤器
添加自定义过滤器到过滤器链:
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.addFilterBefore(new CustomFilter(), UsernamePasswordAuthenticationFilter.class);
return http.build();
}
5.3 会话管理
配置会话管理:
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.sessionManagement((session) -> session
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
.maximumSessions(1)
.maxSessionsPreventsLogin(true)
.expiredUrl("/login?expired")
);
return http.build();
}
对于无状态应用程序,可以将会话创建策略设置为 STATELESS,这样 Spring Security 不会创建或使用 HttpSession。
6. 方法级安全
6.1 启用方法级安全
在配置类上启用方法级安全:
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
@Configuration
@EnableMethodSecurity
public class MethodSecurityConfig {
// 配置内容
}
6.2 使用注解保护方法
使用 @PreAuthorize 注解保护方法:
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service;
@Service
public class UserService {
@PreAuthorize("hasRole('ADMIN')")
public void deleteUser(Long userId) {
// 删除用户的逻辑
}
@PreAuthorize("hasRole('USER') and #userId == authentication.principal.id")
public void updateUserProfile(Long userId, UserProfile profile) {
// 更新用户资料的逻辑
}
}
6.3 使用 @PostAuthorize 注解
使用 @PostAuthorize 注解在方法执行后进行授权检查:
import org.springframework.security.access.prepost.PostAuthorize;
import org.springframework.stereotype.Service;
@Service
public class DocumentService {
@PostAuthorize("returnObject.owner == authentication.principal.username")
public Document getDocument(Long documentId) {
// 获取文档的逻辑
return documentRepository.findById(documentId).orElse(null);
}
}
方法级安全注解需要启用 AOP 支持。在 Spring Boot 应用中,spring-boot-starter-aop 依赖会自动添加。
7. OAuth2 集成
7.1 OAuth2 概述
OAuth2 是一种授权框架,允许第三方应用程序代表资源所有者访问受保护的资源。Spring Security 提供了对 OAuth2 的全面支持。
7.2 添加 OAuth2 依赖
在 Maven 项目中添加 OAuth2 依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
7.3 配置 OAuth2 客户端
在 application.properties 中配置 OAuth2 客户端:
spring.security.oauth2.client.registration.google.client-id=your-client-id
spring.security.oauth2.client.registration.google.client-secret=your-client-secret
spring.security.oauth2.client.registration.google.scope=profile,email
7.4 配置 OAuth2 登录
配置 OAuth2 登录:
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests((requests) -> requests
.requestMatchers("/", "/home").permitAll()
.anyRequest().authenticated()
)
.oauth2Login((oauth2) -> oauth2
.loginPage("/login")
.defaultSuccessUrl("/dashboard")
);
return http.build();
}
Spring Security 支持多种 OAuth2 提供商,包括 Google、GitHub、Facebook 等。只需添加相应的配置即可。
8. JWT 支持
8.1 JWT 概述
JSON Web Token (JWT) 是一种紧凑的、自包含的方式,用于在各方之间安全地传输信息。Spring Security 可以与 JWT 集成,用于无状态认证。
8.2 添加 JWT 依赖
在 Maven 项目中添加 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>
8.3 创建 JWT 工具类
创建 JWT 工具类用于生成和验证令牌:
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import java.security.Key;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
@Component
public class JwtUtil {
private final Key key = Keys.secretKeyFor(SignatureAlgorithm.HS256);
private final long jwtTokenValidity = 5 * 60 * 60 * 1000; // 5 hours
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(key).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() + jwtTokenValidity))
.signWith(key)
.compact();
}
public Boolean validateToken(String token, UserDetails userDetails) {
final String username = extractUsername(token);
return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
}
}
8.4 创建 JWT 过滤器
创建 JWT 过滤器用于处理 JWT 认证:
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
@Component
public class JwtRequestFilter extends OncePerRequestFilter {
private final UserDetailsService userDetailsService;
private final JwtUtil jwtUtil;
public JwtRequestFilter(UserDetailsService userDetailsService, JwtUtil jwtUtil) {
this.userDetailsService = userDetailsService;
this.jwtUtil = jwtUtil;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
final String authorizationHeader = request.getHeader("Authorization");
String username = null;
String jwt = null;
if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
jwt = authorizationHeader.substring(7);
username = jwtUtil.extractUsername(jwt);
}
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
if (jwtUtil.validateToken(jwt, userDetails)) {
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authToken);
}
}
chain.doFilter(request, response);
}
}
JWT 令牌一旦签发就无法撤销,除非它们过期。对于需要立即撤销访问权限的场景,可以考虑使用黑名单或短令牌有效期。
9. CSRF 防护
9.1 CSRF 概述
跨站请求伪造 (CSRF) 是一种攻击,其中恶意网站诱使用户的浏览器向用户已认证的网站发送请求。Spring Security 提供了内置的 CSRF 防护机制。
9.2 启用 CSRF 防护
默认情况下,Spring Security 启用 CSRF 防护。如果需要显式配置:
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf((csrf) -> csrf
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
);
return http.build();
}
9.3 在表单中包含 CSRF 令牌
在 Thymeleaf 模板中包含 CSRF 令牌:
<form th:action="@{/login}" method="post">
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" />
<!-- 其他表单字段 -->
</form>
9.4 禁用 CSRF 防护
对于某些场景(如 REST API),可能需要禁用 CSRF 防护:
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf((csrf) -> csrf.disable());
return http.build();
}
对于使用 JWT 的无状态 API,通常可以禁用 CSRF 防护,因为每个请求都需要包含有效的 JWT 令牌。
10. 最佳实践
10.1 安全配置最佳实践
- 使用 HTTPS:在生产环境中始终使用 HTTPS
- 设置安全响应头:配置 X-Content-Type-Options、X-Frame-Options 等安全响应头
- 使用安全的密码编码器:使用 BCryptPasswordEncoder 或更强的算法
- 实施最小权限原则:只授予必要的权限
- 定期更新依赖:保持 Spring Security 和其他依赖的最新版本
10.2 认证最佳实践
- 实施强密码策略:要求用户使用强密码
- 实施账户锁定:在多次失败登录尝试后锁定账户
- 实施密码重置流程:提供安全的密码重置机制
- 实施多因素认证:对于敏感操作,要求额外的认证步骤
- 记录安全事件:记录登录尝试、权限更改等安全事件
10.3 授权最佳实践
- 使用细粒度的权限:使用细粒度的权限而不是粗粒度的角色
- 实施职责分离:确保敏感操作需要多个角色的批准
- 定期审查权限:定期审查和更新用户权限
- 实施最小权限原则:只授予必要的权限
- 使用声明式安全:优先使用注解进行方法级安全控制
10.4 测试安全配置
- 进行安全测试:使用 OWASP ZAP 等工具进行安全测试
- 进行渗透测试:定期进行渗透测试
- 进行代码审查:审查安全相关代码
- 进行依赖扫描:使用 OWASP Dependency-Check 等工具扫描依赖漏洞
- 进行安全培训:对开发团队进行安全培训
安全是一个持续的过程,而不是一次性的任务。定期审查和更新安全配置是保持应用程序安全的关键。