跳到主要内容

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 包含多个安全过滤器,按顺序执行。

请求处理流程大致如下:

  1. 用户发起请求
  2. 请求到达 FilterChainProxy
  3. FilterChainProxy 根据请求路径选择匹配的 SecurityFilterChain
  4. 请求依次通过 SecurityFilterChain 中的各个过滤器
  5. 认证过滤器验证用户身份
  6. 授权过滤器检查用户权限
  7. 请求最终到达业务代码

核心组件

SecurityContextHolder:存储当前安全上下文的持有者,使用 ThreadLocal 存储安全信息,确保线程安全。

SecurityContext:安全上下文接口,包含当前认证用户的 Authentication 对象。

Authentication:认证对象,包含用户的身份信息(Principal)、凭证信息(Credentials)和权限列表(Authorities)。

UserDetails:用户详情接口,定义了获取用户名、密码、权限等信息的标准方法。

UserDetailsService:用户详情服务接口,负责根据用户名加载用户信息。

PasswordEncoder:密码编码器接口,负责密码的加密和验证。

GrantedAuthority:权限接口,表示用户拥有的权限。

认证流程详解

Spring Security 的认证流程涉及多个组件的协作:

  1. 用户提交认证请求(如登录表单)
  2. UsernamePasswordAuthenticationFilter 拦截请求,提取用户名和密码
  3. 创建 UsernamePasswordAuthenticationToken(未认证状态)
  4. 调用 AuthenticationManager.authenticate() 进行认证
  5. AuthenticationManager 委托给 ProviderManager
  6. ProviderManager 遍历配置的 AuthenticationProvider
  7. DaoAuthenticationProvider 调用 UserDetailsService 加载用户信息
  8. 使用 PasswordEncoder 验证密码
  9. 认证成功,创建已认证的 Authentication 对象
  10. 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):

  1. 服务器生成一个 CSRF Token
  2. 将 Token 存储在 Session 中
  3. 在渲染表单时,将 Token 作为隐藏字段包含在表单中
  4. 提交表单时,验证 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-ProtectionXSS 过滤器
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 的核心概念和使用方法:

  1. 核心架构:过滤器链、SecurityContext、Authentication 等核心组件
  2. 认证方式:表单登录、HTTP Basic、自定义用户存储
  3. 授权控制:URL 授权、方法级授权、自定义权限评估
  4. 安全防护:CSRF 防护、CORS 配置、会话管理
  5. 实战配置:完整的安全配置示例

Spring Security 是一个功能强大的安全框架,通过合理配置可以为应用提供完善的安全保护。理解其核心架构有助于更好地定制安全策略。