0%

springSecurity流程

Spring Security 核心流程全解析:认证与权限控制的底层逻辑

Spring Security 的核心能力分为认证(Authentication)权限(Authorization) 两部分,分别对应 “确认用户身份” 和 “判断用户是否有权访问资源”。从 “请求拦截→认证处理→权限校验” 三个维度,逐行拆解流程中的关键组件与数据流转。

认证流程:从登录请求到身份确认

认证流程的核心是 “获取用户凭证→校验凭证→存储认证信息”,入口是 UsernamePasswordAuthenticationFilter(处理表单登录请求),最终通过 AuthenticationManagerUserDetailsService 完成校验。

1. 流程入口:UsernamePasswordAuthenticationFilter 拦截登录请求

UsernamePasswordAuthenticationFilter 是处理 POST /login(或自定义登录路径)请求的专用过滤器,继承自 AbstractAuthenticationProcessingFilter,其核心逻辑在父类的 doFilter() 方法中触发。

步骤 1:AbstractAuthenticationProcessingFilter#doFilter—— 过滤器核心调度

所有登录请求首先进入该方法,判断是否需要认证并触发后续逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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 对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Override
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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Override
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 方法分为两步:加载用户信息→校验凭证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// AbstractUserDetailsAuthenticationProvider#authenticate(DaoAuthenticationProvider 继承此方法)
@Override
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从数据库加载用户:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    @Override
    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对比提交的密码(明文)与数据库存储的加密密码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    @Override
    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),并包含用户权限:

    1
    2
    3
    4
    5
    6
    7
    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,确保后续请求能获取用户身份:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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,避免无效信息残留

1
2
3
4
5
6
7
8
9
10
11
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() 方法中,分为 “权限校验→请求放行→后续处理” 三步:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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 是权限校验的核心方法,逻辑可拆解为 “获取资源权限→匹配用户权限→决策是否放行”:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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(只要有一个权限匹配就放行):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// AffirmativeBased#decide
@Override
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(过滤器链中的前一个过滤器)捕获,处理逻辑如下:

  1. 若用户未认证:重定向到登录页;
  2. 若用户已认证但无权限:重定向到 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方法

    1
    authResult = attemptAuthentication(request, response);
  • 确认该请求是否需要认证,如果需要认证且此时还没有进行认证,则尝试进行认证,调用UsernamePasswordAuthenticationFilter的attemptAuthentication方法,将从请求中获取的用户名和密码封装成UsernamePasswordAuthenticationToken对象(认证状态为未认证)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    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进行认证过程

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // 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

    1
    successfulAuthentication(request, response, chain, authResult);
  • 如果认证失败则执行unsuccessfulAuthentication

    1
    unsuccessfulAuthentication(request, response, failed);

权限流程

FilterSecurityInterceptor

  • 判断请求是否有权限

    1
    InterceptorStatusToken token = super.beforeInvocation(fi);
  • 进行springmvc处理

    1
    fi.getChain().doFilter(fi.getRequest(), fi.getResponse());

欢迎关注我的其它发布渠道

表情 | 预览
快来做第一个评论的人吧~
Powered By Valine
v1.3.10