← 返回题目列表

Spring Security 的认证流程是怎样的?AuthenticationManager、AuthenticationProvider、UserDetailsService 各是什么?

高频 中等 第 10 / 23 题 更新于 2026/07/26
认证流程AuthenticationManagerUserDetailsServiceAuthenticationProvider

简化版

Spring Security 认证的核心是一条「责任链式」的组件协作:认证过滤器(如 UsernamePasswordAuthenticationFilter)从请求里拿到用户名密码,封装成一个未认证的 Authentication 对象,交给 AuthenticationManager(认证管理器,通常是 ProviderManager);ProviderManager 内部持有多个 AuthenticationProvider(认证提供者),挨个问「你能处理这种认证吗」,能处理的那个来验证;对于用户名密码认证,DaoAuthenticationProvider 会调 UserDetailsService 根据用户名查出用户信息(UserDetails,再用 PasswordEncoder 比对密码;验证通过就返回一个已认证的 Authentication,存进 SecurityContext。一句话:过滤器收集凭证 → Manager 分发 → Provider 验证 → UserDetailsService 查用户 → 密码比对 → 认证成功存上下文

详细版

核心组件及职责

组件职责
AuthenticationManager认证入口(接口),发起认证;默认实现是 ProviderManager
ProviderManager持有一组 AuthenticationProvider,逐个尝试认证
AuthenticationProvider真正的认证逻辑,一种认证方式一个 Provider(用户名密码、JWT、LDAP…)
DaoAuthenticationProvider处理「用户名密码」认证的 Provider,最常用
UserDetailsService根据用户名加载用户信息,返回 UserDetails
UserDetails用户信息载体(用户名、密码、权限、账号状态)
PasswordEncoder密码编码/比对(如 BCrypt)
Authentication认证对象——认证前装凭证、认证后装用户和权限
SecurityContext存放已认证的 Authentication(当前登录用户)

认证的完整流程(以用户名密码为例):

1. UsernamePasswordAuthenticationFilter 拦截登录请求
   → 从请求取 username/password → 封装成"未认证"的 UsernamePasswordAuthenticationToken
2. 交给 AuthenticationManager.authenticate(token)
   → ProviderManager 遍历它持有的 AuthenticationProvider
3. DaoAuthenticationProvider.supports() 返回 true(它能处理 UsernamePasswordAuthenticationToken)
   → 由它来认证:
     a. 调 UserDetailsService.loadUserByUsername(username) → 查数据库得到 UserDetails
     b. 用 PasswordEncoder.matches(输入密码, UserDetails里的加密密码) 比对
     c. 检查账号状态(是否锁定、过期、禁用)
4. 认证成功 → 返回"已认证"的 Authentication(含 UserDetails 和权限)
   → 存进 SecurityContextHolder(SecurityContext)
5. 后续请求从 SecurityContext 取当前用户,做授权判断

自定义认证:最常见的定制是实现 UserDetailsService(把「从数据库查用户」的逻辑换成你的),或自定义 AuthenticationProvider(换整个认证逻辑,如对接第三方)。

@Service
public class MyUserDetailsService implements UserDetailsService {
    public UserDetails loadUserByUsername(String username) {
        User user = userMapper.findByUsername(username);  // 你的查库逻辑
        if (user == null) throw new UsernameNotFoundException(username);
        return org.springframework.security.core.userdetails.User
            .withUsername(user.getUsername())
            .password(user.getPassword())         // 数据库里的加密密码
            .authorities(user.getRoles())         // 权限
            .build();
    }
}

⚠️ UserDetailsService.loadUserByUsername 只负责「根据用户名查出用户(含加密密码)」,它不做密码比对——密码比对是 DaoAuthenticationProvider 拿到 UserDetails 后用 PasswordEncoder.matches 做的。很多人误以为要在 UserDetailsService 里比对密码,其实不用,你只管把「数据库里存的那个用户」返回给框架,比对由框架完成。

完整版教学

一、认证的整体设计:责任分离

Spring Security 的认证不是一个方法搞定,而是一条组件协作链,每个组件职责单一。理解这个「分工」是关键:

认证要做几件事,Spring Security 把它们拆给不同组件:
  ① 从请求里提取凭证(用户名密码/token)  → 认证过滤器
  ② 决定用哪种方式认证(分发)            → AuthenticationManager/ProviderManager
  ③ 执行具体的认证逻辑                    → AuthenticationProvider
  ④ 加载用户信息(查库)                  → UserDetailsService
  ⑤ 比对密码                              → PasswordEncoder
  ⑥ 保存认证结果(当前登录用户)          → SecurityContext

这种「责任分离」的好处是可扩展、可替换——你想改哪一环就替换哪个组件(换查库逻辑就实现 UserDetailsService、换认证方式就加 AuthenticationProvider),不用动其他部分。理解「认证是一条分工明确的组件链」,就抓住了 Spring Security 认证的设计精髓,也就能回答「怎么自定义认证」——找到对应职责的组件替换即可。

二、Authentication 对象:认证前后的载体

Authentication 是贯穿认证全程的核心对象,它有个「认证前 / 认证后」的双重身份:

认证前(未认证):装"凭证",等待验证
  UsernamePasswordAuthenticationToken(username, password)
  - principal = username(用户名)
  - credentials = password(密码,待验证)
  - authenticated = false

认证后(已认证):装"用户和权限",验证已通过
  - principal = UserDetails(完整用户信息)
  - credentials = null(密码被清除,安全)
  - authorities = 权限列表
  - authenticated = true

关键理解:同一个 Authentication 对象,在认证前后含义不同——认证前它是「一份待验证的凭证」(有密码),认证后它变成「一个已验证的用户身份」(有权限、密码被清空)。认证过滤器创建「未认证」的,AuthenticationManager 验证后返回「已认证」的。这个转换是整个认证流程的主线。SecurityContext 里存的就是「已认证」的 Authentication,代表「当前登录的是谁、有什么权限」。

三、AuthenticationManager 与 ProviderManager:分发者

AuthenticationManager 是认证的入口接口,只有一个方法 authenticate()。它的默认实现是 ProviderManager,扮演「分发者」的角色:

ProviderManager 内部持有一组 AuthenticationProvider(认证提供者列表)
authenticate(未认证的 Authentication) 时:
  遍历每个 Provider:
    provider.supports(该 Authentication 类型)?
      支持 → 由它来 authenticate(),成功就返回、失败抛异常
      不支持 → 跳过,问下一个 Provider
  → 直到某个 Provider 认证成功,或全部失败

为什么要「一组 Provider」而不是一个?因为一个系统可能支持多种认证方式——用户名密码、短信验证码、JWT、LDAP、OAuth2…每种对应一个 AuthenticationProviderProviderManager 负责「把认证请求分发给能处理它的那个 Provider」(靠 supports() 判断类型)。这是典型的「责任链 + 策略」模式——多个策略(Provider),管理器按类型选合适的来处理。理解「ProviderManager 是分发者、Provider 是具体执行者」,就理解了 Spring Security 怎么支持多种认证方式共存。

四、AuthenticationProvider:真正的认证执行者

AuthenticationProvider真正执行认证逻辑的组件,一种认证方式一个 Provider。最常用的是处理用户名密码的 DaoAuthenticationProvider

DaoAuthenticationProvider.authenticate() 做的事:
  1. 从 Authentication 拿到 username
  2. 调 UserDetailsService.loadUserByUsername(username) → 查出 UserDetails
     (查不到抛 UsernameNotFoundException)
  3. 用 PasswordEncoder.matches(输入的密码, UserDetails.getPassword()) 比对密码
     (不匹配抛 BadCredentialsException)
  4. 检查账号状态(isAccountNonLocked、isEnabled、isAccountNonExpired...)
  5. 全通过 → 构造"已认证"的 Authentication(含 UserDetails 和权限)返回

DaoAuthenticationProvider 把「认证」拆成了「查用户(委托 UserDetailsService)+ 比对密码(委托 PasswordEncoder)+ 检查状态」。它自己不查库、不比对,而是委托给专门的组件——这样你只要替换 UserDetailsService(换查库逻辑)或 PasswordEncoder(换加密算法),就能定制认证,不用重写整个 Provider。这就是为什么「自定义认证最常见的方式是实现 UserDetailsService」——你只需提供「怎么根据用户名查出用户」,框架的 DaoAuthenticationProvider 负责剩下的比对和检查。

五、UserDetailsService 与 UserDetails:用户信息的来源

UserDetailsService 是「根据用户名加载用户信息」的接口,只有一个方法:

public interface UserDetailsService {
    UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}

它是「Spring Security 世界」和「你的用户数据」之间的桥梁——框架不知道你的用户存在哪(MySQL、Redis、LDAP、第三方),你实现这个接口告诉它「怎么根据用户名查出用户」,返回一个 UserDetails

UserDetails 是用户信息的标准载体,包含:

UserDetails 接口的核心信息:
  getUsername()      → 用户名
  getPassword()      → 加密后的密码(用于比对,不是明文)
  getAuthorities()   → 权限/角色列表
  isAccountNonLocked()   → 账号是否未锁定
  isEnabled()            → 账号是否启用
  isAccountNonExpired()  → 账号是否未过期
  isCredentialsNonExpired() → 密码是否未过期

关键点(再强调):UserDetailsService 只负责「查出用户」,不比对密码。它返回的 UserDetails 里带着「数据库里存的加密密码」,框架(DaoAuthenticationProvider)拿这个加密密码和「用户输入的密码」用 PasswordEncoder.matches 比对。所以你实现 UserDetailsService 时,只管把数据库里那条用户记录(含加密密码、权限)转成 UserDetails 返回即可,比对逻辑框架全包了。这是自定义认证时最容易搞错的地方。

六、SecurityContext:认证结果的存放

认证成功后,「已认证的 Authentication」要存起来,供后续请求使用——这就是 SecurityContext 的作用:

认证成功 → SecurityContextHolder.getContext().setAuthentication(已认证的 Authentication)
后续任何地方获取当前用户:
  SecurityContextHolder.getContext().getAuthentication()
  → 拿到当前登录用户的 Authentication(含 UserDetails、权限)

存储机制:
  SecurityContextHolder 默认用 ThreadLocal 存 SecurityContext
  → 当前线程内任何地方都能拿到当前用户(不用层层传参)
  → Session 认证时,请求结束会把 SecurityContext 存进 Session(下次请求恢复)

SecurityContext 是「当前登录用户」的存放处,默认用 ThreadLocal(当前线程内可见)。所以在 Controller、Service 任何地方都能 SecurityContextHolder.getContext().getAuthentication() 拿到当前用户,做权限判断。但正因为是 ThreadLocal,异步线程(新线程)拿不到主线程的 SecurityContext——这是「异步场景下认证信息丢失」的原因(需要 DelegatingSecurityContextRunnable 等传递上下文)。授权(@PreAuthorize、URL 权限判断)就是从 SecurityContext 取当前用户的权限来判断的。至此,认证流程闭环:过滤器收集凭证 → Manager 分发 → Provider 验证 → 查用户比对 → 存进 SecurityContext → 供授权使用。

记忆钩子:「认证流程:过滤器收集凭证→AuthenticationManager(ProviderManager)分发→AuthenticationProvider 验证→DaoAuthenticationProvider 调 UserDetailsService 查用户(UserDetails)→PasswordEncoder 比对密码→成功存 SecurityContext(ThreadLocal);UserDetailsService 只查用户不比对密码;自定义认证就实现 UserDetailsService」

七、常见误区与追问

  • 误区:UserDetailsService 要负责比对密码。 不——它只根据用户名查出用户(含加密密码);密码比对由 DaoAuthenticationProvider 用 PasswordEncoder.matches 完成,你只管返回用户。
  • 误区:AuthenticationManager 自己执行认证逻辑。 它是入口接口,默认实现 ProviderManager 是「分发者」,把认证分发给能处理的 AuthenticationProvider;真正的认证逻辑在 Provider 里。
  • 误区:Authentication 对象认证前后一样。 认证前装凭证(有密码、authenticated=false),认证后装用户和权限(密码清空、authenticated=true);同一对象前后含义不同。
  • 误区:SecurityContext 在所有线程都能拿到。 默认用 ThreadLocal,只在当前线程可见;异步/新线程拿不到主线程的认证信息,需显式传递上下文。
  • 追问:怎么自定义 Spring Security 的认证? 最常见是实现 UserDetailsService(换查库逻辑);要换整个认证方式(如对接第三方、加短信验证码)则自定义 AuthenticationProvider 并注册到 ProviderManager。
  • 追问:ProviderManager 为什么持有多个 AuthenticationProvider? 一个系统可能支持多种认证方式(用户名密码、JWT、短信、LDAP),每种一个 Provider;ProviderManager 按 supports() 判断类型,把请求分发给能处理它的 Provider。
  • 追问:认证成功后用户信息存在哪,怎么取? 存在 SecurityContext(SecurityContextHolder,默认 ThreadLocal);任何地方用 SecurityContextHolder.getContext().getAuthentication() 取当前登录用户;Session 认证时还会存进 Session 供下次请求恢复。

八、加强记忆

Spring Security 认证是一条责任分离的组件协作链认证过滤器(如 UsernamePasswordAuthenticationFilter)从请求提取凭证、封装成**「未认证」的 Authentication** → 交给 AuthenticationManager(默认 ProviderManager,扮演分发者)ProviderManager 遍历持有的一组 AuthenticationProvider(每种认证方式一个),用 supports() 找到能处理的那个 → 用户名密码由 DaoAuthenticationProvider 处理:调 UserDetailsService.loadUserByUsername 查出 UserDetails(含加密密码、权限),再用 PasswordEncoder.matches 比对密码、检查账号状态 → 成功后返回**「已认证」的 Authentication(密码清空、带权限)存进 SecurityContext(ThreadLocal),供后续授权使用。核心记忆点:UserDetailsService 只「查用户」不「比对密码」(比对是 Provider 用 PasswordEncoder 做的)、Authentication 认证前装凭证认证后装身份**、ProviderManager 是分发者 Provider 是执行者SecurityContext 用 ThreadLocal(异步会丢)。自定义认证最常见就是实现 UserDetailsService。一句话「过滤器收凭证→Manager 分发→Provider 验证→UserDetailsService 查用户→PasswordEncoder 比对→存 SecurityContext,查用户和比对是分开的」。