springSecurity流程
Spring Security 核心流程全解析:认证与权限控制的底层逻辑
Spring Security 的核心能力分为认证(Authentication) 和权限(Authorization) 两部分,分别对应 “确认用户身份” 和 “判断用户是否有权访问资源”。从 “请求拦截→认证处理→权限校验” 三个维度,逐行拆解流程中的关键组件与数据流转。
认证流程:从登录请求到身份确认
认证流程的核心是 “获取用户凭证→校验凭证→存储认证信息”,入口是 UsernamePasswordAuthenticationFilter(处理表单登录请求),最终通过 AuthenticationManager 和 UserDetailsService 完成校验。
1. 流程入口:UsernamePasswordAuthenticationFilter 拦截登录请求
UsernamePasswordAuthenticationFilter 是处理 POST /login(或自定义登录路径)请求的专用过滤器,继承自 AbstractAuthenticationProcessingFilter,其核心逻辑在父类的 doFilter() 方法中触发。
步骤 1:AbstractAuthenticationProcessingFilter#doFilter—— 过滤器核心调度
所有登录请求首先进入该方法,判断是否需要认证并触发后续逻辑:
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
// 1. 判断请求是否需要认证(是否是登录请求、是否已认证)
if (!requiresAuthentication(request, response)) {
chain.doFilter(request, response);
return;
}
Authentication authResult = null;
AuthenticationException failed = null;
try {
// 2. 核心:尝试认证(委托给子类实现,即 UsernamePasswordAuthenticationFilter#attemptAuthentication)
authResult = attemptAuthentication(request, response);
} catch (AuthenticationException e) {
failed = e;
}
// 3. 认证成功:处理成功逻辑(存储认证信息、跳转)
if (authResult != null) {
successfulAuthentication(request, response, chain, authResult);
}
// 4. 认证失败:处理失败逻辑(清理上下文、跳转错误页)
else if (failed != null) {
unsuccessfulAuthentication(request, response, failed);
}
}
- 关键判断:
requiresAuthentication()会检查请求路径(如是否是/login)和请求方法(是否是 POST),只有符合条件的请求才会进入认证流程。
步骤 2:UsernamePasswordAuthenticationFilter#attemptAuthentication—— 封装用户凭证
该方法是认证的 “数据准备阶段”,核心是从请求中提取用户名 / 密码,封装成未认证的 Authentication 对象:
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
// 1. 仅允许 POST 方法(防止 GET 请求暴露密码)
if (postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("不支持的认证方法:" + request.getMethod());
}
// 2. 从请求中提取用户名和密码(默认从 request 参数中获取,可自定义)
String username = obtainUsername(request); // 默认参数名:username
String password = obtainPassword(request); // 默认参数名:password
username = (username != null) ? username.trim() : "";
password = (password != null) ? password : "";
// 3. 封装成 UsernamePasswordAuthenticationToken(未认证状态)
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
// 4. 设置额外信息(如 IP 地址、会话 ID,供后续使用)
setDetails(request, authRequest);
// 5. 委托给 AuthenticationManager 执行实际认证
return this.getAuthenticationManager().authenticate(authRequest);
}
UsernamePasswordAuthenticationToken状态:此时构造的对象setAuthenticated(false)(未认证),仅包含用户名和密码,无权限信息。
步骤 3:AuthenticationManager#authenticate—— 委托认证逻辑
AuthenticationManager 是认证的 “调度中心”,默认实现 ProviderManager 不直接处理认证,而是委托给多个 AuthenticationProvider(如处理用户名密码的 DaoAuthenticationProvider、处理 OAuth2 的 OAuth2AuthenticationProvider)。
(1)ProviderManager#authenticate—— 筛选合适的 AuthenticationProvider
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Class<? extends Authentication> toTest = authentication.getClass();
AuthenticationException lastException = null;
Authentication result = null;
// 遍历所有 AuthenticationProvider,找到支持当前 Authentication 类型的实现
for (AuthenticationProvider provider : getProviders()) {
if (!provider.supports(toTest)) {
continue;
}
try {
// 委托给 DaoAuthenticationProvider 执行认证
result = provider.authenticate(authentication);
break; // 认证成功,跳出循环
} catch (AuthenticationException e) {
lastException = e;
}
}
if (result == null && lastException != null) {
throw lastException; // 所有 Provider 认证失败,抛出异常
}
return result;
}
supports()判断:DaoAuthenticationProvider仅支持UsernamePasswordAuthenticationToken类型,因此会被选中。
(2)DaoAuthenticationProvider#authenticate—— 实际认证逻辑
DaoAuthenticationProvider 是处理用户名密码认证的核心实现,继承自 AbstractUserDetailsAuthenticationProvider,其 authenticate 方法分为两步:加载用户信息→校验凭证:
// AbstractUserDetailsAuthenticationProvider#authenticate(DaoAuthenticationProvider 继承此方法)
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String username = determineUsername(authentication); // 提取用户名
boolean cacheWasUsed = true;
// 1. 加载用户信息(委托给 DaoAuthenticationProvider#retrieveUser)
UserDetails user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
// 2. 校验用户状态(是否锁定、过期等)
preAuthenticationChecks.check(user);
// 3. 校验密码(核心:对比用户提交的密码与数据库存储的密码)
additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
// 4. 校验后处理(如更新密码过期时间)
postAuthenticationChecks.check(user);
// 5. 封装成已认证的 Authentication 对象(包含权限信息)
return createSuccessAuthentication(user.getUsername(), authentication, user);
}
关键子步骤:
retrieveUser():DaoAuthenticationProvider 的核心方法,调用UserDetailsService从数据库加载用户:protected UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) { try { // 调用自定义 UserDetailsService 加载用户(生产环境需自己实现) UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username); if (loadedUser == null) { throw new UsernameNotFoundException("用户不存在:" + username); } return loadedUser; } catch (UsernameNotFoundException e) { throw e; } catch (Exception e) { throw new AuthenticationServiceException(e.getMessage(), e); } }additionalAuthenticationChecks():校验密码,使用PasswordEncoder对比提交的密码(明文)与数据库存储的加密密码:protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) { if (authentication.getCredentials() == null) { throw new BadCredentialsException("密码不能为空"); } // 1. 获取用户提交的密码(明文) String presentedPassword = authentication.getCredentials().toString(); // 2. 对比加密后的密码(数据库存储的密码 vs 提交密码加密后的值) if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) { throw new BadCredentialsException("密码错误"); } }createSuccessAuthentication():生成已认证的UsernamePasswordAuthenticationToken,此时setAuthenticated(true),并包含用户权限:protected Authentication createSuccessAuthentication(Object principal, Authentication authentication, UserDetails user) { // 生成已认证的 Token,包含用户权限(user.getAuthorities()) UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken( principal, authentication.getCredentials(), user.getAuthorities()); result.setDetails(authentication.getDetails()); return result; }
步骤 4:认证结果处理 —— 成功与失败
(1)successfulAuthentication()—— 认证成功
认证成功后,核心是将已认证的 Authentication 存入 SecurityContext,确保后续请求能获取用户身份:
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response,
FilterChain chain, Authentication authResult) throws IOException, ServletException {
// 1. 将认证信息存入 SecurityContext(ThreadLocal 存储,仅当前线程可见)
SecurityContextHolder.getContext().setAuthentication(authResult);
// 2. 处理“记住我”功能(若配置)
rememberMeServices.loginSuccess(request, response, authResult);
// 3. 触发认证成功事件(供监听器使用)
if (this.eventPublisher != null) {
this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass()));
}
// 4. 跳转成功页面(如 /index,或之前请求的 URL)
successHandler.onAuthenticationSuccess(request, response, authResult);
}
(2)unsuccessfulAuthentication()—— 认证失败
认证失败后,核心是清理 SecurityContext,避免无效信息残留:
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
AuthenticationException failed) throws IOException, ServletException {
// 1. 清除 SecurityContext(防止残留未认证信息)
SecurityContextHolder.clearContext();
// 2. 处理“记住我”失败(若配置)
rememberMeServices.loginFail(request, response);
// 3. 跳转失败页面(如 /login?error)
failureHandler.onAuthenticationFailure(request, response, failed);
}
权限流程:从请求拦截到权限判断
权限流程的核心是 “检查当前用户是否有权访问目标资源”,入口是 FilterSecurityInterceptor(Spring Security 过滤器链的最后一个过滤器),在请求到达 Controller 前完成权限校验。
1. 核心组件:FilterSecurityInterceptor 的作用
FilterSecurityInterceptor 是 “权限拦截器”,继承自 AbstractSecurityInterceptor,其核心逻辑在 doFilter() 方法中,分为 “权限校验→请求放行→后续处理” 三步:
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
// 1. 封装请求为 FilterInvocation(包含请求、响应、过滤器链)
FilterInvocation fi = new FilterInvocation(request, response, chain);
// 2. 核心:权限校验(委托给父类 AbstractSecurityInterceptor#beforeInvocation)
InterceptorStatusToken token = super.beforeInvocation(fi);
try {
// 3. 权限校验通过:放行请求,执行后续过滤器链(最终到达 Controller)
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
} finally {
// 4. 清理权限校验状态(如恢复 SecurityContext)
super.finallyInvocation(token);
}
// 5. 权限校验后的处理(如触发事件)
super.afterInvocation(token, null);
}
2. 核心步骤:AbstractSecurityInterceptor#beforeInvocation—— 权限判断
beforeInvocation 是权限校验的核心方法,逻辑可拆解为 “获取资源权限→匹配用户权限→决策是否放行”:
protected InterceptorStatusToken beforeInvocation(Object object) {
FilterInvocation fi = (FilterInvocation) object;
Authentication authenticated = authenticateIfRequired(); // 获取当前认证信息
// 1. 获取目标资源对应的权限(如 /admin/** 需要 ROLE_ADMIN 权限)
Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource().getAttributes(object);
if (attributes == null || attributes.isEmpty()) {
return null; // 无权限配置,直接放行
}
// 2. 核心:调用 AccessDecisionManager 判断用户是否有权限
this.accessDecisionManager.decide(authenticated, object, attributes);
// 3. 处理“安全上下文Holder”(如切换上下文)
if (this.publishAuthorizationSuccess) {
publishAuthorizationSuccessEvent(authenticated, object, attributes);
}
// 4. 返回权限校验状态(供后续清理)
return new InterceptorStatusToken(SecurityContextHolder.getContext(), false, attributes, object);
}
关键子步骤解析:
(1)obtainSecurityMetadataSource()—— 获取资源权限
SecurityMetadataSource 是 “资源权限数据源”,负责根据请求路径(如 /admin/user)返回对应的权限规则(如 ROLE_ADMIN)。Spring Security 提供默认实现 FilterInvocationSecurityMetadataSource,支持基于 URL 模式的权限配置(如你之前配置的 .antMatchers("/admin/**").hasRole("admin"))。
(2)accessDecisionManager.decide()—— 权限决策
AccessDecisionManager 是 “权限决策器”,核心是对比用户的权限与资源所需权限,默认实现 AffirmativeBased(只要有一个权限匹配就放行):
// AffirmativeBased#decide
public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) {
int denyCount = 0;
// 遍历所有决策投票器(默认包含 RoleVoter,基于角色投票)
for (AccessDecisionVoter voter : getDecisionVoters()) {
// 投票:支持(ACCESS_GRANTED)、拒绝(ACCESS_DENIED)、弃权(ACCESS_ABSTAIN)
int result = voter.vote(authentication, object, configAttributes);
switch (result) {
case AccessDecisionVoter.ACCESS_GRANTED:
return; // 有一个投票支持,直接放行
case AccessDecisionVoter.ACCESS_DENIED:
denyCount++; // 记录拒绝票数
break;
default:
break;
}
}
// 若有拒绝票,抛出 AccessDeniedException
if (denyCount > 0) {
throw new AccessDeniedException("没有访问权限");
}
// 无拒绝票但也无支持票(如无匹配的权限规则),根据配置决定是否放行
checkAllowIfAllAbstainDecisions();
}
RoleVoter投票逻辑:检查用户的GrantedAuthority(如ROLE_ADMIN)是否包含资源所需的权限(如ROLE_ADMIN),包含则投支持票。
3. 权限校验失败处理
若 accessDecisionManager.decide() 抛出 AccessDeniedException,会被 ExceptionTranslationFilter(过滤器链中的前一个过滤器)捕获,处理逻辑如下:
- 若用户未认证:重定向到登录页;
- 若用户已认证但无权限:重定向到 403 无权限页面(如你配置的
.accessDeniedPage("/unauth.html"))。
认证与权限的衔接:SecurityContext 的作用
SecurityContext 是认证与权限流程的 “数据桥梁”,核心作用是存储当前用户的认证信息(Authentication),由 SecurityContextHolder 管理(默认基于 ThreadLocal)。
- 认证流程:成功后将
Authentication存入SecurityContext; - 权限流程:从
SecurityContext中获取Authentication,提取用户权限进行校验。
数据流转总结:
graph LR
A[用户提交用户名密码] --> B[UsernamePasswordAuthenticationFilter封装Token]
B --> C[DaoAuthenticationProvider调用UserDetailsService加载用户]
C --> D[校验密码生成已认证Token]
D --> E[存入SecurityContext]
E --> F[FilterSecurityInterceptor从SecurityContext获取Token]
F --> G[AccessDecisionManager对比权限]
G --> H[有权限->放行到Controller]
G --> I[无权限->抛出AccessDeniedException]
总结:核心流程与组件
| 流程阶段 | 核心组件 | 核心作用 |
|---|---|---|
| 认证入口 | UsernamePasswordAuthenticationFilter | 拦截登录请求,提取凭证 |
| 认证调度 | AuthenticationManager(ProviderManager) | 委托 AuthenticationProvider 执行认证 |
| 认证实现 | DaoAuthenticationProvider | 调用 UserDetailsService,校验密码 |
| 用户数据 | UserDetailsService | 从数据库加载用户信息 |
| 权限拦截 | FilterSecurityInterceptor | 请求到达前校验权限 |
| 权限决策 | AccessDecisionManager | 对比用户权限与资源权限 |
| 数据存储 | SecurityContextHolder | 管理 SecurityContext,存储认证信息 |
理解这套流程,就能清晰掌握 Spring Security 如何从 “确认用户身份” 到 “控制资源访问”,也能轻松进行自定义扩展(如自定义登录过滤器、自定义权限决策器)
springSecurity流程
认证流程
登录请求进入UsernamePasswordAuthenticationFilter,父类是AbstractAuthenticationProcessingFilter,执行AbstractAuthenticationProcessingFilter的doFilter方法
authResult = attemptAuthentication(request, response);确认该请求是否需要认证,如果需要认证且此时还没有进行认证,则尝试进行认证,调用UsernamePasswordAuthenticationFilter的attemptAuthentication方法,将从请求中获取的用户名和密码封装成UsernamePasswordAuthenticationToken对象(认证状态为未认证)
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken( username, password); public UsernamePasswordAuthenticationToken(Object principal, Object credentials) { super(null); this.principal = principal; this.credentials = credentials; // 设置为未认证 setAuthenticated(false); }通过AuthenticationManager(实现类是ProviderManager)委托AuthenticationProvider(实现类是DaoAuthenticationProvider)关联UserDetailsService进行认证过程
// UsernamePasswordAuthenticationFilter中通过AuthenticationManager进行认证 return this.getAuthenticationManager().authenticate(authRequest); // ProviderManager中通过AuthenticationProvider进行认证,使用的是DaoAuthenticationProvider对象,DaoAuthenticationProvider继承了AbstractUserDetailsAuthenticationProvider类,实际走的是AbstractUserDetailsAuthenticationProvider的authenticate方法 result = provider.authenticate(authentication); // AbstractUserDetailsAuthenticationProvider的authenticate方法中检索用户,实际执行的是DaoAuthenticationProvider的retrieveUser方法 user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication); // DaoAuthenticationProvider的retrieveUser方法中通过UserDetailsService就查询用户进行认证,可以自己实现用户查找以及认证过程 loadedUser = this.getUserDetailsService().loadUserByUsername(username);UserDetailsService返回的UserDetails被封装成Authentication
如果认证成功则执行successfulAuthentication
successfulAuthentication(request, response, chain, authResult);如果认证失败则执行unsuccessfulAuthentication
unsuccessfulAuthentication(request, response, failed);
权限流程
FilterSecurityInterceptor
判断请求是否有权限
InterceptorStatusToken token = super.beforeInvocation(fi);进行springmvc处理
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());