Spring Security 安全框架
Spring Security 是一个强大且高度可定制的安全框架,专门用于保护 Spring 应用程序。它提供了认证(Authentication)和授权(Authorization)两大核心功能,同时还能防护常见的 Web 攻击。
安全框架概述
为什么需要安全框架?
在开发 Web 应用时,安全性是必须考虑的重要因素。常见的安全需求包括:
身份认证:确认用户是谁,验证用户的身份凭证。
权限授权:确认用户能做什么,控制用户可以访问哪些资源。
会话管理:管理用户的登录状态,防止会话劫持。
攻击防护:防御 CSRF、XSS、点击劫持等常见 Web 攻击。
如果不使用安全框架,我们需要自己实现这些功能,不仅工作量大,而且容易出现安全漏洞。Spring Security 提供了一套完整的安全解决方案,让我们能够专注于业务逻辑的实现。
Spring Security 的核心特性
认证支持:支持多种认证方式,包括表单登录、HTTP Basic、OAuth2、JWT 等。
授权支持:支持 URL 级别和方法级别的授权控制,支持角色和权限两种授权模型。
防护攻击:内置 CSRF 防护、安全头设置、点击劫持防护等安全措施。
会话管理:支持会话固定攻击防护、会话并发控制等功能。
高度可定制:几乎每个功能都可以通过配置进行定制。
核心架构
理解 Spring Security 的架构对于正确使用和定制安全功能至关重要。
过滤器链架构
Spring Security 基于 Servlet 过滤器(Filter)实现安全控制。当请求到达应用之前,会先经过一系列安全过滤器,每个过滤器负责特定的安全功能。
整个安全架构的核心是 FilterChainProxy,它是一个特殊的过滤器,负责将请求分发到不同的 SecurityFilterChain。每个 SecurityFilterChain 包含多个安全过滤器,按顺序执行。
请求处理流程大致如下:
- 用户发起请求
- 请求到达
FilterChainProxy FilterChainProxy根据请求路径选择匹配的SecurityFilterChain- 请求依次通过
SecurityFilterChain中的各个过滤器 - 认证过滤器验证用户身份
- 授权过滤器检查用户权限
- 请求最终到达业务代码
核心组件
SecurityContextHolder:存储当前安全上下文的持有者,使用 ThreadLocal 存储安全信息,确保线程安全。
SecurityContext:安全上下文接口,包含当前认证用户的 Authentication 对象。
Authentication:认证对象,包含用户的身份信息(Principal)、凭证信息(Credentials)和权限列表(Authorities)。
UserDetails:用户详情接口,定义了获取用户名、密码、权限等信息的标准方法。
UserDetailsService:用户详情服务接口,负责根据用户名加载用户信息。
PasswordEncoder:密码编码器接口,负责密码的加密和验证。
GrantedAuthority:权限接口,表示用户拥有的权限。
认证流程详解
Spring Security 的认证流程涉及多个组件的协作:
- 用户提交认证请求(如登录表单)
UsernamePasswordAuthenticationFilter拦截请求,提取用户名和密码- 创建
UsernamePasswordAuthenticationToken(未认证状态) - 调用
AuthenticationManager.authenticate()进行认证 AuthenticationManager委托给ProviderManagerProviderManager遍历配置的AuthenticationProviderDaoAuthenticationProvider调用UserDetailsService加载用户信息- 使用
PasswordEncoder验证密码 - 认证成功,创建已认证的
Authentication对象 - 将
Authentication存入SecurityContext
基础配置
添加依赖
在 Spring Boot 项目中添加 Spring Security 依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
添加依赖后,Spring Security 默认会启用安全配置,所有请求都需要认证。默认用户名为 user,密码在启动日志中生成。
基本安全配置
Spring Security 6.x 推荐使用 Lambda DSL 进行配置,代码更简洁易读:
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// 授权配置
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/", "/home", "/css/**", "/js/**").permitAll()
.requestMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
// 表单登录配置
.formLogin(form -> form
.loginPage("/login")
.defaultSuccessUrl("/dashboard")
.permitAll()
)
// 注销配置
.logout(logout -> logout
.logoutSuccessUrl("/login?logout")
.permitAll()
);
return http.build();
}
@Bean
public UserDetailsService userDetailsService() {
UserDetails user = User.withDefaultPasswordEncoder()
.username("user")
.password("password")
.roles("USER")
.build();
UserDetails admin = User.withDefaultPasswordEncoder()
.username("admin")
.password("admin")
.roles("ADMIN", "USER")
.build();
return new InMemoryUserDetailsManager(user, admin);
}
}
这个配置实现了以下安全策略:
静态资源放行:首页、CSS、JS 等资源无需认证即可访问。
角色控制:/admin/** 路径需要 ADMIN 角色。
其他请求需要认证:除明确放行的路径外,其他请求都需要用户登录。
自定义登录页:使用自定义的登录页面而不是默认页面。
注销功能:注销后重定向到登录页。
密码编码器
密码永远不应该以明文形式存储。Spring Security 提供了多种密码编码器:
@Bean
public PasswordEncoder passwordEncoder() {
// 推荐使用 BCrypt
return new BCryptPasswordEncoder();
}
// 使用示例
String rawPassword = "123456";
String encodedPassword = passwordEncoder.encode(rawPassword);
boolean matches = passwordEncoder.matches(rawPassword, encodedPassword);
常用的密码编码器对比:
| 编码器 | 特点 | 适用场景 |
|---|---|---|
| BCryptPasswordEncoder | 内置盐值,自动处理 | 通用推荐 |
| Pbkdf2PasswordEncoder | 基于 PBKDF2 算法 | 需要符合 FIPS 标准 |
| SCryptPasswordEncoder | 基于 SCrypt 算法 | 需要更高安全性 |
| Argon2PasswordEncoder | 基于 Argon2 算法 | 抗 GPU 攻击 |
认证方式
表单登录
表单登录是最常用的认证方式,用户通过登录页面提交用户名和密码:
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.formLogin(form -> form
.loginPage("/login") // 自定义登录页 URL
.loginProcessingUrl("/authenticate") // 登录处理 URL
.usernameParameter("username") // 用户名参数名
.passwordParameter("password") // 密码参数名
.defaultSuccessUrl("/dashboard") // 登录成功默认跳转
.failureUrl("/login?error=true") // 登录失败跳转
.successHandler(customSuccessHandler) // 自定义成功处理
.failureHandler(customFailureHandler) // 自定义失败处理
.permitAll()
);
return http.build();
}
自定义登录页面需要包含一个提交到登录处理 URL 的表单:
<form action="/authenticate" method="post">
<input type="text" name="username" placeholder="用户名">
<input type="password" name="password" placeholder="密码">
<button type="submit">登录</button>
</form>
HTTP Basic 认证
HTTP Basic 认证通过请求头传递凭证,适合 API 服务或内部系统:
http
.httpBasic(basic -> basic
.realmName("My Application")
);
客户端请求时需要在请求头中携带 Base64 编码的凭证:
Authorization: Basic dXNlcjpwYXNzd29yZA==
注意:HTTP Basic 认证会在每次请求中传递密码,建议配合 HTTPS 使用。
自定义用户存储
实际项目中,用户信息通常存储在数据库中。实现 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())
.authorities(user.getRoles().stream()
.map(role -> "ROLE_" + role.getName())
.toArray(String[]::new))
.accountExpired(!user.isAccountNonExpired())
.accountLocked(!user.isAccountNonLocked())
.credentialsExpired(!user.isCredentialsNonExpired())
.disabled(!user.isEnabled())
.build();
}
}
自定义认证提供者
如果需要自定义认证逻辑(如验证码、多因素认证等),可以实现 AuthenticationProvider:
@Component
public class CustomAuthenticationProvider implements AuthenticationProvider {
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String username = authentication.getName();
String password = authentication.getCredentials().toString();
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
// 验证密码
if (!passwordEncoder.matches(password, userDetails.getPassword())) {
throw new BadCredentialsException("密码错误");
}
// 检查账户状态
if (!userDetails.isAccountNonLocked()) {
throw new LockedException("账户已锁定");
}
// 返回已认证的 Authentication
return new UsernamePasswordAuthenticationToken(
userDetails,
null,
userDetails.getAuthorities()
);
}
@Override
public boolean supports(Class<?> authentication) {
return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
}
}
注册自定义认证提供者:
@Bean
public AuthenticationManager authenticationManager(
AuthenticationProvider customAuthenticationProvider) {
return new ProviderManager(customAuthenticationProvider);
}
授权控制
授权是确定已认证用户是否有权限访问特定资源的过程。Spring Security 支持多种授权方式。
URL 授权
URL 授权是最常见的授权方式,根据请求路径控制访问权限:
http
.authorizeHttpRequests(authorize -> authorize
// 完全放行
.requestMatchers("/", "/home", "/register").permitAll()
// 静态资源放行
.requestMatchers("/css/**", "/js/**", "/images/**").permitAll()
// 需要特定角色
.requestMatchers("/admin/**").hasRole("ADMIN")
.requestMatchers("/user/**").hasAnyRole("USER", "ADMIN")
// 需要特定权限
.requestMatchers("/api/users/**").hasAuthority("USER_MANAGEMENT")
// 需要认证
.requestMatchers("/dashboard/**").authenticated()
// 记住我用户
.requestMatchers("/remember-me/**").rememberMe()
// 其他所有请求都需要认证
.anyRequest().authenticated()
);
常用的授权方法:
| 方法 | 说明 |
|---|---|
| permitAll() | 允许所有人访问,无需认证 |
| authenticated() | 需要认证 |
| hasRole(String) | 需要指定角色(会自动加 ROLE_ 前缀) |
| hasAnyRole(String...) | 需要任一角色 |
| hasAuthority(String) | 需要指定权限 |
| hasAnyAuthority(String...) | 需要任一权限 |
| rememberMe() | 记住我用户可访问 |
方法级授权
方法级授权可以在 Service 层或 Controller 层的方法上进行细粒度的权限控制:
首先需要启用方法级安全注解:
@Configuration
@EnableMethodSecurity
public class MethodSecurityConfig {
}
然后在方法上使用授权注解:
@Service
public class UserService {
// 需要特定角色
@PreAuthorize("hasRole('ADMIN')")
public List<User> findAllUsers() {
return userRepository.findAll();
}
// 需要特定权限
@PreAuthorize("hasAuthority('USER_CREATE')")
public User createUser(User user) {
return userRepository.save(user);
}
// 使用 SpEL 表达式,检查用户只能访问自己的数据
@PreAuthorize("#username == authentication.name")
public User findByUsername(String username) {
return userRepository.findByUsername(username);
}
// 组合条件
@PreAuthorize("hasRole('ADMIN') or #id == authentication.principal.id")
public User findById(Long id) {
return userRepository.findById(id);
}
// 方法执行后检查
@PostAuthorize("returnObject.username == authentication.name")
public User getUser(Long id) {
return userRepository.findById(id);
}
// 过滤返回结果
@PostFilter("filterObject.owner == authentication.name")
public List<User> getOwnedUsers() {
return userRepository.findAll();
}
// 过滤输入参数
@PreFilter("filterObject.owner == authentication.name")
public void batchUpdate(List<User> users) {
userRepository.saveAll(users);
}
}
常用的方法授权注解:
| 注解 | 位置 | 说明 |
|---|---|---|
| @PreAuthorize | 方法前 | 执行前检查权限 |
| @PostAuthorize | 方法后 | 执行后检查权限 |
| @PreFilter | 方法前 | 过滤输入参数 |
| @PostFilter | 方法后 | 过滤返回结果 |
| @Secured | 方法前 | 简单角色检查(传统方式) |
自定义权限评估器
当授权逻辑比较复杂时,可以实现自定义的权限评估器:
@Component
public class CustomPermissionEvaluator implements PermissionEvaluator {
@Override
public boolean hasPermission(Authentication authentication,
Object targetDomainObject,
Object permission) {
// 检查用户对特定对象的权限
// 例如:用户是否有权限修改某篇文章
return true;
}
@Override
public boolean hasPermission(Authentication authentication,
Serializable targetId,
String targetType,
Object permission) {
// 检查用户对特定 ID 对象的权限
// 例如:用户是否有权限修改 ID 为 123 的文章
return true;
}
}
使用自定义权限评估器:
@PreAuthorize("hasPermission(#id, 'Article', 'READ')")
public Article getArticle(Long id) {
return articleRepository.findById(id);
}
CSRF 防护
什么是 CSRF?
CSRF(Cross-Site Request Forgery,跨站请求伪造)是一种常见的 Web 攻击。攻击者诱导用户在已登录的网站上执行非预期的操作。
例如,用户已登录银行网站,攻击者在第三方网站上放置了一个自动提交的表单,向银行网站发起转账请求。由于用户的浏览器会自动携带银行网站的 Cookie,请求会成功执行。
Spring Security 的 CSRF 防护
Spring Security 默认启用 CSRF 防护,它使用同步令牌模式(Synchronizer Token Pattern):
- 服务器生成一个 CSRF Token
- 将 Token 存储在 Session 中
- 在渲染表单时,将 Token 作为隐藏字段包含在表单中
- 提交表单时,验证 Token 是否匹配
在 Thymeleaf 模板中自动添加 CSRF Token:
<form action="/transfer" method="post">
<!-- Thymeleaf 会自动添加 CSRF Token -->
<input type="text" name="amount">
<button type="submit">转账</button>
</form>
在纯 HTML 中需要手动添加:
<form action="/transfer" method="post">
<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}">
<input type="text" name="amount">
<button type="submit">转账</button>
</form>
禁用 CSRF
在某些场景下(如纯 API 服务),可能需要禁用 CSRF:
http
.csrf(csrf -> csrf.disable());
注意:禁用 CSRF 会降低应用的安全性,只建议在无状态 API 服务中使用。
在 AJAX 请求中处理 CSRF
对于 AJAX 请求,可以将 CSRF Token 放在请求头中:
// 从 meta 标签获取 Token
var token = document.querySelector('meta[name="_csrf"]').content;
var header = document.querySelector('meta[name="_csrf_header"]').content;
// 发送请求时添加请求头
fetch('/api/data', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
[header]: token
},
body: JSON.stringify(data)
});
CORS 配置
CORS(Cross-Origin Resource Sharing,跨源资源共享)是浏览器的一种安全机制,限制网页向不同源发起请求。
全局 CORS 配置
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins("https://example.com")
.allowedMethods("GET", "POST", "PUT", "DELETE")
.allowedHeaders("*")
.allowCredentials(true)
.maxAge(3600);
}
}
Spring Security CORS 配置
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.cors(cors -> cors.configurationSource(corsConfigurationSource()));
return http.build();
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList("https://example.com"));
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE"));
configuration.setAllowedHeaders(Arrays.asList("*"));
configuration.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/api/**", configuration);
return source;
}
会话管理
会话配置
http
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED) // 会话创建策略
.maximumSessions(1) // 最大并发会话数
.maxSessionsPreventsLogin(false) // 不阻止新登录
.expiredUrl("/login?expired") // 会话过期跳转
);
会话创建策略:
| 策略 | 说明 |
|---|---|
| IF_REQUIRED | 需要时创建会话(默认) |
| ALWAYS | 总是创建会话 |
| NEVER | 从不创建会话,但使用已有的 |
| STATELESS | 不创建也不使用会话 |
会话固定攻击防护
会话固定攻击是指攻击者获取用户的会话 ID 后,诱导用户使用该会话 ID 登录,从而获取用户的认证状态。
Spring Security 默认启用会话固定攻击防护:
http
.sessionManagement(session -> session
.sessionFixation(sessionFixation -> sessionFixation
.newSession() // 登录后创建新会话
)
);
防护策略:
| 策略 | 说明 |
|---|---|
| none | 不防护 |
| newSession | 创建新会话,不保留旧会话属性 |
| migrateSession | 创建新会话,复制旧会话属性(默认) |
| changeSessionId | 只改变会话 ID(Servlet 3.1+) |
安全响应头
Spring Security 默认添加多个安全响应头:
http
.headers(headers -> headers
.contentTypeOptions(contentTypeOptions -> contentTypeOptions.disable())
.xssProtection(xss -> xss.disable())
.frameOptions(frame -> frame.sameOrigin())
.httpStrictTransportSecurity(hsts -> hsts
.includeSubDomains(true)
.maxAgeInSeconds(31536000)
)
);
常用的安全响应头:
| 响应头 | 说明 |
|---|---|
| X-Content-Type-Options | 防止 MIME 类型嗅探 |
| X-XSS-Protection | XSS 过滤器 |
| X-Frame-Options | 防止点击劫持 |
| Strict-Transport-Security | 强制使用 HTTPS |
| Content-Security-Policy | 内容安全策略 |
实战示例
完整的安全配置示例
下面是一个结合数据库用户存储、自定义登录页、记住我功能的完整配置:
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {
@Autowired
private CustomUserDetailsService userDetailsService;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// 授权配置
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/", "/home", "/register", "/login").permitAll()
.requestMatchers("/css/**", "/js/**", "/images/**").permitAll()
.requestMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
// 表单登录
.formLogin(form -> form
.loginPage("/login")
.defaultSuccessUrl("/dashboard", true)
.failureUrl("/login?error=true")
.permitAll()
)
// 记住我
.rememberMe(remember -> remember
.key("uniqueAndSecret")
.tokenValiditySeconds(86400)
.userDetailsService(userDetailsService)
)
// 注销
.logout(logout -> logout
.logoutUrl("/logout")
.logoutSuccessUrl("/login?logout")
.deleteCookies("JSESSIONID", "remember-me")
.permitAll()
)
// 会话管理
.sessionManagement(session -> session
.maximumSessions(1)
.expiredUrl("/login?expired")
)
// CSRF
.csrf(csrf -> csrf
.ignoringRequestMatchers("/api/**")
);
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationManager authenticationManager(
AuthenticationConfiguration config) throws Exception {
return config.getAuthenticationManager();
}
}
自定义登录成功处理器
@Component
public class CustomAuthenticationSuccessHandler
implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(
HttpServletRequest request,
HttpServletResponse response,
Authentication authentication) throws IOException {
String redirectUrl = determineTargetUrl(authentication);
response.sendRedirect(redirectUrl);
}
private String determineTargetUrl(Authentication authentication) {
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
if (authorities.stream().anyMatch(a -> a.getAuthority().equals("ROLE_ADMIN"))) {
return "/admin/dashboard";
} else if (authorities.stream().anyMatch(a -> a.getAuthority().equals("ROLE_USER"))) {
return "/user/dashboard";
}
return "/";
}
}
获取当前登录用户
// 方式1:通过 SecurityContextHolder
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
String username = authentication.getName();
// 方式2:通过方法参数注入
@GetMapping("/profile")
public String profile(Principal principal) {
return "Hello, " + principal.getName();
}
// 方式3:通过 Authentication 参数
@GetMapping("/info")
public String info(Authentication authentication) {
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
return userDetails.getUsername();
}
// 方式4:通过 @AuthenticationPrincipal 注解
@GetMapping("/me")
public String me(@AuthenticationPrincipal UserDetails userDetails) {
return userDetails.getUsername();
}
小结
本章详细介绍了 Spring Security 的核心概念和使用方法:
- 核心架构:过滤器链、SecurityContext、Authentication 等核心组件
- 认证方式:表单登录、HTTP Basic、自定义用户存储
- 授权控制:URL 授权、方法级授权、自定义权限评估
- 安全防护:CSRF 防护、CORS 配置、会话管理
- 实战配置:完整的安全配置示例
Spring Security 是一个功能强大的安全框架,通过合理配置可以为应用提供完善的安全保护。理解其核心架构有助于更好地定制安全策略。