概述
Spring Security 有三大功能可以概括为:
- 认证(Authentication):确认用户身份,确保访问系统的主体是真实可信的。常用方式包括用户名/密码、Token、OAuth2 等。
- 授权(Authorization):控制用户对资源或操作的访问权限,可以基于 URL、方法或角色进行精细化控制,例如使用注解
@PreAuthorize或配置访问规则。 - 防护(Protection):针对常见安全威胁提供防御措施,包括 CSRF 防护、防止 Session 固定攻击、点击劫持(Clickjacking)以及密码加密存储等,保障应用的整体安全性。
整个请求在进入应用之前都会沿着过滤器链依次经过各类 Filter,认证、授权和防护都在链内按顺序处理;只有当请求通过所有检查,才会到达应用的核心 Servlet。
核心逻辑
- 认证产生认证对象
- 授权访问认证对象进行权限检查
过滤器链运行流程
进入链
所有请求先到达 FilterChainProxy,这是 Spring Security 在 Servlet 容器里注册的唯一安全入口。它根据请求路径,选择合适的 SecurityFilterChain。
链的结构
SecurityFilterChain 的内部其实就是一组按照顺序排列的 Filter。这些 Filter 都实现了 javax.servlet.Filter 接口,并遵循统一的调用规范:doFilter(ServletRequest request, ServletResponse response, FilterChain chain)。在执行过程中,请求会依次经过链上的各个 Filter,每一个 Filter 都可能对请求进行处理,例如:
- 处理请求(比如认证、鉴权、上下文维护)
- 决定是否继续往下走(调用
chain.doFilter) - 或者直接中断(返回响应,比如认证失败 401)。
链中节点特性
- 拦截点:每个 Filter 都是一个检查点。
- 条件执行:有的 Filter 只处理特定请求(比如
/login时才进入认证 Filter)。 - 职责单一:每个 Filter 做一件事(认证、CSRF、异常翻译、权限判断等)。
- 可短路:一旦某个 Filter 决定返回响应,请求就不会继续向下传。
出链
如果没有被拦截、认证/授权成功,请求会一路穿过所有 Filter,最终进入应用的 Servlet(如 DispatcherServlet → Controller)。响应返回时,也会逆向经过这些 Filter(有些 Filter 会在响应阶段做处理,比如写回 SecurityContext)。
认证
AbstractAuthenticationProcessingFilter
由于都是在链内完成的,涉及认证的主要 Fliter 就是 AbstractAuthenticationProcessingFilter 接口的实现类了。他的实现类有很多,例如:
UsernamePasswordAuthenticationFilter:最常见的实现类,用于表单登录OAuth2LoginAuthenticationFilter:处理基于 OAuth2 登录的认证请求
接口
实现类内部的操作包含了认证的全部流程,其主要涉及的组件接口如下:
数据封装:
Authentication:承载了认证请求和认证结果的数据。UserDetails:用户的核心信息载体(用户名、密码、权限、账户是否过期/锁定等)。
工具服务组件:
AuthenticationManager:认证调度器,接收Authentication请求并分派给合适的AuthenticationProvider。AuthenticationProvider:执行具体认证逻辑(如用户名密码校验)。常见实现是DaoAuthenticationProvider。UserDetailsService:按用户名加载用户信息,返回UserDetails。PasswordEncoder:用于密码的加密与匹配校验。AuthenticationSuccessHandler/AuthenticationFailureHandler:登录成功或失败后的处理器(比如跳转页面、返回 JSON)。AuthenticationEntryPoint:未登录时访问受保护资源的入口处理(比如返回 401)。
典型流程
请求进入 Spring Security FilterChain
|
└── AbstractAuthenticationProcessingFilter.doFilter()
|
├── attemptAuthentication(request, response)
| |
| └── 调用 AuthenticationManager.authenticate(authenticationToken)
| |
| ├── 遍历所有 AuthenticationProvider
| | |
| | ├── 如果 provider.supports(token)
| | | └── 调用 provider.authenticate(token)
| | | |
| | | ├── AbstractUserDetailsAuthenticationProvider
| | | | |
| | | | ├── retrieveUser(username, token)
| | | | | └── 调用 UserDetailsService.loadUserByUsername()
| | | | | └── 返回 UserDetails(包含用户名、密码、权限等)
| | | | |
| | | | ├── additionalAuthenticationChecks(UserDetails, token)
| | | | | └── 比对密码(PasswordEncoder.matches())
| | | | |
| | | | └── 返回 Authentication(认证成功的对象)
| | |
| | └── 如果认证失败,抛出 AuthenticationException
| |
| └── 如果所有 provider 都不能认证,抛出 ProviderNotFoundException
|
├── 如果认证成功
| ├── 将 Authentication 放入 SecurityContextHolder
| └── 调用 successfulAuthentication()
| └── 继续执行过滤器链
|
└── 如果认证失败
└── 调用 unsuccessfulAuthentication()
└── 返回错误响应 (如 401 Unauthorized)
(以 UsernamePasswordAuthenticationFilter 为例)
-
接收请求
- 从
HttpServletRequest里拿到用户名、密码。 - 封装成
UsernamePasswordAuthenticationToken(一个实现了Authentication的对象,代表“待认证的凭证”)。
- 从
-
调用认证管理器
Authentication authResult = this.getAuthenticationManager() .authenticate(authRequest);- 这里会进入
AuthenticationManager,通常是ProviderManager。
- 这里会进入
-
Manager → Provider
ProviderManager遍历AuthenticationProvider,找到能处理UsernamePasswordAuthenticationToken的(一般是DaoAuthenticationProvider)。
-
Provider → Service + Encoder
DaoAuthenticationProvider调用UserDetailsService.loadUserByUsername()查询用户信息。- 拿到
UserDetails,再用PasswordEncoder.matches()校验密码。
-
结果回传
- 成功 → 返回一个带有用户信息和权限的
Authentication(authenticated=true)。 - 失败 → 抛异常,被 Filter 捕获后交给
AuthenticationFailureHandler。
- 成功 → 返回一个带有用户信息和权限的
-
Filter 收尾
- 成功 → 调用
successfulAuthentication(),把认证信息放入SecurityContextHolder。 - 失败 → 调用
unsuccessfulAuthentication(),返回错误信息。
- 成功 → 调用
JWT处理示例
jwt由于其认证等复杂过程都基于jwt的工具函数,通常已经包装完整,因此,如果采用jwt的工具函数,就很难再对整个认证过程进行像以上 Provider 等等的划分,因此,建议直接继承 OncePerRequestFilter ,构建一个基于jwt的过滤器,在过滤器中完成 jwt 的验证,处理,解析等完整流程,并将验证之后的数据存入上下文,达到和原本运行完 UsernamePasswordAuthenticationFilter 之后的一致结果。
示例:
public class JwtAuthFilter extends OncePerRequestFilter {
private final JwtCoreUtil jwtCoreUtil; // 你封装的 JWT 工具类
private final LocalTokenBlacklistService tokenBlacklistService; // 可选黑名单
public JwtAuthFilter(JwtCoreUtil jwtCoreUtil, LocalTokenBlacklistService tokenBlacklistService) {
this.jwtCoreUtil = jwtCoreUtil;
this.tokenBlacklistService = tokenBlacklistService;
}
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain)
throws IOException, ServletException {
String token = request.getHeader("Authorization");
if (!StringUtils.hasText(token) || !token.startsWith("Bearer ")) {
chain.doFilter(request, response); // 没有 token 就直接放行,让后续访问控制处理
return;
}
token = token.substring(7); // 去掉 "Bearer " 前缀
try {
// 黑名单检查
if (tokenBlacklistService != null && tokenBlacklistService.isBlacklisted(token)) {
sendError(response, "Token revoked");
return;
}
Claims claims = jwtCoreUtil.parseToken(token); // 解析 JWT
validateClaims(claims);
// 创建 Authentication 并放入 SecurityContext
Authentication auth = createAuthentication(claims);
SecurityContextHolder.getContext().setAuthentication(auth);
chain.doFilter(request, response);
} catch (ExpiredJwtException ex) {
sendError(response, "Token expired");
} catch (JwtException | IllegalArgumentException ex) {
sendError(response, "Invalid token");
} catch (Exception ex) {
sendError(response, ex.getMessage());
}
}
// 验证 JWT 必要字段
protected void validateClaims(Claims claims) {
if (claims.getSubject() == null) {
throw new IllegalArgumentException("Missing subject");
}
// 可按需校验 roles / tenantId / email 等
}
// 构造 Authentication
protected Authentication createAuthentication(Claims claims) {
String username = claims.getSubject();
List<String> roles = claims.get("roles", List.class);
List<GrantedAuthority> authorities = Optional.ofNullable(roles)
.orElse(Collections.emptyList())
.stream()
.filter(Objects::nonNull)
.map(r -> new SimpleGrantedAuthority("ROLE_" + r))
.collect(Collectors.toList());
// 也可以在 principal 中封装 email / tenantId / userId
return new UsernamePasswordAuthenticationToken(username, null, authorities);
}
// 统一返回错误
private void sendError(HttpServletResponse response, String message) throws IOException {
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getWriter().write("{\\"code\\":401,\\"message\\":\\"" + message + "\\"}");
}
@Override
protected boolean shouldNotFilter(HttpServletRequest request) {
String path = request.getRequestURI();
// 例:公共路径不验证
return path.startsWith("/public/") || path.startsWith("/login");
}
}
数据的存储与加载
SecurityContextHolder
SecurityContextHolder
└── SecurityContext (接口)
└── Authentication (接口)
├── Principal (Object) // 当前用户对象,一般是 UserDetails 或自定义用户
├── Credentials (Object) // 认证凭证,如密码或 token
├── Authorities (Collection<GrantedAuthority>) // 权限列表
└── isAuthenticated (boolean) // 是否认证通过
SecurityContextHolder 是 Spring Security 的全局上下文容器,用于在认证链中传递用户和权限信息,保证数据线程安全。
- 认证链开始时:从请求中提取必要的凭证(如 JWT token、用户名密码)。验证凭证有效性,将基本的用户信息载入
Principal,并设置isAuthenticated=false。 - 认证链结束时:完成完整验证(如查数据库加载用户详情、权限列表)。更新
Authentication对象,设置isAuthenticated=true,权限信息(Authorities)也完整载入。
载入后的访问:
- 业务逻辑访问时:Controller 或 Service 层可通过
SecurityContextHolder.getContext().getAuthentication()获取当前用户。可直接读取Principal、权限列表以及认证状态。
@AuthenticationPrincipal
直接将当前认证用户对象注入到 Controller 方法参数中,避免手动从 SecurityContextHolder 获取。
@GetMapping("/me")
public String me(@AuthenticationPrincipal MyUser user) {
return user.getEmail();
}
权限管理
权限管理的依据依旧在 Authentication 接口实现类的存储中,他有一个 Authorities 的成员数据,是一个 Collection<GrantedAuthority> 的权限列表,只需要在构建的时候载入角色该有的权限即可。
作为用户而言,通常可能有多个权限级别,例如读权限,写权限。因此这是一个序列而并不是一个类似角色的单个值。
授权概述
在 Spring Security 中,授权(Authorization)主要有 三类方式,根据粒度和应用场景划分:
| 授权方式 | 粒度 | 实现方式 | 核心组件 |
|---|---|---|---|
| URL / 请求级 | 粗 | HttpSecurity + Filter |
FilterSecurityInterceptor |
| 方法级 | 细 | 注解 + AOP | MethodSecurityInterceptor |
| 表达式 / 策略级 | 动态 | SpEL + 自定义策略 | PermissionEvaluator、AccessDecisionManager |
授权的核心接口
AccessDecisionManager
决策者,决定某个请求是否允许访问。
常见实现:
AffirmativeBased(一个允许即可通过)ConsensusBased(多数投票)UnanimousBased(全票通过)
AccessDecisionVoter
投票者,单独判断是否允许访问。
int vote(Authentication authentication, Object object, Collection<ConfigAttribute> attributes)
返回 ACCESS_GRANTED / ACCESS_DENIED / ACCESS_ABSTAIN
SecurityMetadataSource
提供安全元数据(URL / 方法 → 需要权限)。
例如:FilterInvocationSecurityMetadataSource(URL 授权)
FilterSecurityInterceptor
Spring Security Filter 链中的最后一个 Filter,用于做最终的授权判断。
流程:
- 获取
SecurityMetadataSource配置的资源权限 - 调用
AccessDecisionManager做决策 - 决策成功 → 放行
- 决策失败 → 抛出
AccessDeniedException
URL级别控制
请求进入 SecurityFilterChain
|
└── FilterSecurityInterceptor.doFilter()
|
├── 调用 SecurityMetadataSource.getAttributes(request)
| └── 获取该 URL 需要的权限列表
|
├── 调用 AccessDecisionManager.decide(authentication, request, attributes)
| |
| ├── 遍历 AccessDecisionVoter
| ├── 投票决定是否允许访问
| └── 返回允许或抛异常
|
├── 如果允许
| └── chain.doFilter(request, response) 继续执行
|
└── 如果拒绝
└── AccessDeniedHandler 处理(返回 403 或自定义页面)
实现
使用 HttpSecurity 进行静态 URL 控制
@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// URL 权限配置
.authorizeHttpRequests(auth -> auth
.requestMatchers("/admin/**").hasRole("ADMIN")
.requestMatchers("/user/**").hasAnyRole("USER", "ADMIN")
.anyRequest().authenticated()
)
// 登录配置
.formLogin()
.and()
// 注销配置
.logout();
return http.build();
}
}
方法级别控制
请求调用方法
|
└── MethodSecurityInterceptor.invoke()
|
├── 调用 SecurityMetadataSource.getAttributes(method)
| └── 获取该方法需要的权限元数据(角色 / 表达式)
|
├── 获取 Authentication(当前用户身份 + 角色/权限)
|
├── 调用 AccessDecisionManager.decide(authentication, method, attributes)
| |
| ├── 遍历 AccessDecisionVoter
| ├── 投票决定是否允许访问
| └── 返回允许或抛 AccessDeniedException
|
├── 如果允许
| └── 执行目标方法
|
└── 如果拒绝
└── 抛 AccessDeniedException
实现
@PreAuthorize
功能:方法调用前进行权限检查,可以使用表达式,例如 hasRole('ROLE')、hasAuthority('AUTH')、SpEL 表达式等。
@PreAuthorize("hasRole('ADMIN')")
public void deleteUser(Long id) {
// 只有 ADMIN 角色能执行
}
@PostAuthorize
功能:方法调用后进行权限检查,可以访问方法返回值 (returnObject)。适合根据返回内容控制权限。
@PostAuthorize("returnObject.owner == authentication.name")
public User getUser(Long id) {
// 方法执行后,会检查返回的 User 是否属于当前用户
return userService.findById(id);
}
@PreFilter
功能:方法调用前过滤集合或数组参数,只保留满足条件的元素。
@PreFilter("filterObject.owner == authentication.name")
public void processUsers(List<User> users) {
// 只处理属于当前用户的 User 对象
}
@PostFilter
功能:方法调用后过滤集合或数组返回值,只保留满足条件的元素。
@PostFilter("filterObject.owner == authentication.name")
public List<User> listUsers() {
// 返回列表时,只保留属于当前用户的对象
return userService.findAll();
}
原理
以 @PreAuthorize("hasRole('ADMIN')") 为例:
切面拦截:
PreInvocationAuthorizationAdviceVoter或MethodSecurityInterceptor拦截方法调用- 从方法上的注解解析出
ConfigAttribute(Spring 内部会生成一个RoleConfigAttribute)
AccessDecisionManager 执行:
- 默认是
AffirmativeBased:只要有一个 voter 投ACCESS_GRANTED就允许 - 将当前
Authentication(JWT 解析后放在 SecurityContextHolder 的对象)和方法上解析出的角色ConfigAttribute一起传入
投票逻辑(RoleVoter):
RoleVoter会把ConfigAttribute的角色(如"ROLE_ADMIN") 和Authentication.getAuthorities()里的权限比对- 投票规则:如果
authentication.getAuthorities()中包含ConfigAttribute,投ACCESS_GRANTED否则投ACCESS_DENIED或ABSTAIN(视Voter类型而定) - 默认
RoleVoter会自动加"ROLE_"前缀,所以你的 JWT 权限要加ROLE_前缀才能生效
表达式 / 策略的权限控制
权限判断请求
|
└── AccessDecisionManager.decide(authentication, resource, attributes)
|
├── 遍历 AccessDecisionVoter 列表
| |
| ├── ExpressionVoter
| | └── 使用表达式解析器解析权限表达式(如 @PreAuthorize、hasRole、hasAuthority)
| | ├─ 解析表达式
| | ├─ 对比当前用户 Authentication 的权限 / 角色
| | └─ 投票:允许 / 拒绝 / 弃权
| |
| ├── RoleVoter
| | └── 判断用户角色是否匹配资源角色要求
| |
| └── CustomVoter
| └── 自定义策略逻辑判断(如业务条件、用户属性、时间约束等)
|
├── 汇总投票结果
| ├─ 多数投票或一致通过 → 允许访问
| └─ 否则 → 拒绝访问
|
└── 返回决策结果
├─ 允许 → 放行请求 / 执行方法
└─ 拒绝 → 抛 AccessDeniedException 或交给 AccessDeniedHandler 处理
实现
例子:
@Service
public class UserService {
@PreAuthorize("hasRole('ADMIN') or #userId == principal.id")
public void updateUser(Long userId) {
// 只有管理员或操作自己信息的用户可以调用
}
@PreAuthorize("isAuthenticated()")
public void viewProfile() {
// 只要登录用户就可以访问
}
}
原理
表达式权限控制:用 SpEL 表达式 判断用户是否有访问权限,灵活且可组合。
- 典型场景:方法级或 URL 级权限控制
- 核心接口:
AccessDecisionManager+Voter模型
策略级权限控制:底层用 AccessDecisionManager 聚合多个 Voter 的投票结果来决策。
Voter 类型举例:
- RoleVoter:检查用户角色(
hasRole('ADMIN')) - AuthenticatedVoter:检查用户是否已认证(
isAuthenticated()) - PreInvocationAuthorizationAdviceVoter:处理表达式注解(
@PreAuthorize/@PostAuthorize)