Spring Security 怎么支持多种登录方式(手机验证码、扫码等)?自定义 AuthenticationProvider 怎么做?
简化版
**Spring Security 默认支持「用户名 + 密码」登录,但实际系统常需要多种登录方式(手机验证码、扫码、第三方 OAuth 等)——扩展点就是 AuthenticationProvider(认证提供者),你可以为每种登录方式实现一个自定义的 AuthenticationProvider。Spring Security 的认证核心是:AuthenticationManager(认证管理器)持有多个 AuthenticationProvider,认证时依次问每个 Provider「你能处理这种认证吗」,能处理的就由它校验。**默认的 DaoAuthenticationProvider 处理「用户名密码」(查用户、比对密码)。要加「手机验证码登录」:① 定义一个自己的 Authentication 类(如 SmsAuthenticationToken,装手机号和验证码);② 实现一个自己的 AuthenticationProvider(supports() 声明「我处理 SmsAuthenticationToken」、authenticate() 里校验验证码是否正确、查用户、返回已认证的 Token);③ 注册这个 Provider 到 AuthenticationManager + 加一个过滤器接收手机登录请求。核心:Spring Security 的认证是可插拔的——每种登录方式 = 一个 Authentication 类型 + 一个 AuthenticationProvider,AuthenticationManager 按类型分派给对应的 Provider。
详细版
认证核心组件:
| 组件 | 作用 |
|---|---|
| AuthenticationManager | 认证入口,管理多个 Provider |
| ProviderManager | AuthenticationManager 的实现,遍历 Provider |
| AuthenticationProvider | 具体的认证逻辑(一种登录方式一个) |
| Authentication | 认证凭证/结果(如 UsernamePasswordAuthenticationToken) |
| UserDetailsService | 加载用户信息(loadUserByUsername) |
// ① 自定义 Authentication(装手机号 + 验证码)
public class SmsAuthenticationToken extends AbstractAuthenticationToken {
private final String mobile;
private final String code;
// 构造器、getters...
}
// ② 自定义 AuthenticationProvider
@Component
public class SmsAuthenticationProvider implements AuthenticationProvider {
@Autowired private UserDetailsService userDetailsService;
@Autowired private SmsCodeService smsCodeService;
@Override
public Authentication authenticate(Authentication auth) {
SmsAuthenticationToken token = (SmsAuthenticationToken) auth;
// 校验验证码
if (!smsCodeService.verify(token.getMobile(), token.getCode())) {
throw new BadCredentialsException("验证码错误");
}
// 加载用户(按手机号)
UserDetails user = userDetailsService.loadUserByUsername(token.getMobile());
// 返回已认证的 Token
return new SmsAuthenticationToken(user, user.getAuthorities());
}
@Override
public boolean supports(Class<?> authentication) {
return SmsAuthenticationToken.class.isAssignableFrom(authentication); // 我处理这种
}
}
// ③ 注册 Provider + 加过滤器接收 /login/sms 请求(略)
⚠️ 理解 Spring Security 认证扩展的关键,是抓住「
AuthenticationManager+ 多个AuthenticationProvider」这个「分派」结构:AuthenticationManager(实现类ProviderManager)本身不做具体认证,它持有一个AuthenticationProvider列表,认证时依次问每个 Provider「你supports这种Authentication类型吗」,找到能处理的就交给它authenticate。所以加一种新登录方式,本质就是「加一个 Authentication 类型 + 加一个能处理它的 AuthenticationProvider」——这是典型的「策略模式 + 责任链」。默认的DaoAuthenticationProvider处理UsernamePasswordAuthenticationToken(查UserDetailsService拿用户、用PasswordEncoder比对密码);你的SmsAuthenticationProvider处理SmsAuthenticationToken(校验验证码、拿用户)。别把所有登录逻辑塞进一个 Provider——每种登录方式一个 Provider,职责清晰、可插拔。
完整版教学
一、认证的核心结构
先理解 Spring Security 认证的核心组件和流程:
认证的核心组件(从上到下):
AuthenticationManager(认证管理器,认证入口)
↓ 实现类
ProviderManager(持有多个 AuthenticationProvider)
↓ 遍历
AuthenticationProvider(具体认证逻辑,一种方式一个)
↓ 用到
UserDetailsService(加载用户)+ PasswordEncoder(校验密码)
认证流程:
1. 过滤器收到登录请求,封装成一个 Authentication(未认证)
如 UsernamePasswordAuthenticationToken(username, password)
2. 交给 AuthenticationManager.authenticate(authentication)
3. ProviderManager 遍历它持有的 AuthenticationProvider:
对每个 Provider:provider.supports(authentication类型)?
→ 找到能处理的 Provider
4. 该 Provider.authenticate(authentication):
执行具体认证(校验密码/验证码、加载用户)
→ 成功:返回"已认证"的 Authentication(含用户权限)
→ 失败:抛 AuthenticationException
5. 认证成功 → 放入 SecurityContext(后续请求认得)
关键:AuthenticationManager 不做认证,它"分派"给 Provider
多个 Provider 支持多种认证方式
Spring Security 认证的核心组件:AuthenticationManager(入口)→ ProviderManager(实现,持有多个 Provider)→ AuthenticationProvider(具体认证逻辑)→ UserDetailsService(加载用户)+ PasswordEncoder(校验密码)。流程:过滤器封装 Authentication(未认证)→ AuthenticationManager.authenticate → ProviderManager 遍历 Provider 找 supports 的 → 该 Provider authenticate 执行认证(成功返回已认证 Authentication、失败抛异常)→ 放入 SecurityContext。关键:AuthenticationManager 不做认证,分派给 Provider(多个 Provider 支持多种方式)。理解「认证核心:AuthenticationManager→ProviderManager(持多个 Provider)→AuthenticationProvider→UserDetailsService+PasswordEncoder;流程:封装 Authentication→ProviderManager 遍历找 supports 的 Provider→authenticate;AuthenticationManager 分派给 Provider」,就理解了认证的核心结构。
二、AuthenticationProvider:认证的策略
AuthenticationProvider 是「一种认证方式的策略」——两个方法:
AuthenticationProvider 接口(两个方法):
① boolean supports(Class<?> authentication):
声明"我能处理哪种 Authentication 类型"
→ SmsAuthenticationProvider supports SmsAuthenticationToken
→ DaoAuthenticationProvider supports UsernamePasswordAuthenticationToken
② Authentication authenticate(Authentication auth):
执行具体的认证逻辑
- 校验凭证(密码 / 验证码 / token)
- 加载用户信息
- 成功 → 返回"已认证"的 Authentication(含用户、权限)
- 失败 → 抛 AuthenticationException(BadCredentialsException 等)
工作方式(策略模式 + 责任链):
ProviderManager 遍历所有 Provider
对每个:if (provider.supports(authClass)) → provider.authenticate()
→ 谁 supports 这种认证类型,谁处理
→ 加新登录方式 = 加新 Provider(supports 新类型)
默认的 DaoAuthenticationProvider:
supports UsernamePasswordAuthenticationToken
authenticate:
- userDetailsService.loadUserByUsername(username) 拿用户
- passwordEncoder.matches(输入密码, 存储的密码) 比对
- 一致 → 返回已认证 Token
AuthenticationProvider 是「一种认证方式的策略」,两个方法:① supports(Class)(声明处理哪种 Authentication 类型)、② authenticate(Authentication)(执行认证:校验凭证 + 加载用户 → 成功返回已认证 Authentication、失败抛 AuthenticationException)。工作方式(策略模式+责任链):ProviderManager 遍历,谁 supports 这种类型谁处理(加新登录方式=加新 Provider)。默认 DaoAuthenticationProvider 处理 UsernamePasswordAuthenticationToken(loadUserByUsername 拿用户 + passwordEncoder.matches 比对密码)。理解「AuthenticationProvider 是认证策略:supports 声明处理哪种类型/authenticate 执行认证;ProviderManager 遍历谁 supports 谁处理(策略模式+责任链);DaoAuthenticationProvider 处理用户名密码(loadUserByUsername+matches)」,就掌握了 AuthenticationProvider。
三、加一种登录方式:三步
加「手机验证码登录」的完整三步:
第一步:定义 Authentication 类型(装这种登录的凭证)
public class SmsAuthenticationToken extends AbstractAuthenticationToken {
private String mobile; // 手机号
private String code; // 验证码
// 未认证构造器(mobile + code)
// 已认证构造器(principal + authorities)
}
→ 用来表示"手机验证码这种认证"的凭证和结果
第二步:实现 AuthenticationProvider(这种登录的认证逻辑)
public class SmsAuthenticationProvider implements AuthenticationProvider {
public Authentication authenticate(Authentication auth) {
SmsAuthenticationToken token = (SmsAuthenticationToken) auth;
// ① 校验验证码(查 Redis 里存的验证码,比对)
if (!verifyCode(token.getMobile(), token.getCode()))
throw new BadCredentialsException("验证码错误");
// ② 加载用户(按手机号查)
UserDetails user = userDetailsService.loadUserByUsername(token.getMobile());
// ③ 返回已认证的 Token
return new SmsAuthenticationToken(user, user.getAuthorities());
}
public boolean supports(Class<?> authentication) {
return SmsAuthenticationToken.class.isAssignableFrom(authentication);
}
}
第三步:接收请求 + 注册 Provider
① 加一个过滤器(如 SmsAuthenticationFilter):
拦截 /login/sms 请求,取出 mobile、code
封装成 SmsAuthenticationToken(未认证)
交给 AuthenticationManager.authenticate
② 把 SmsAuthenticationProvider 注册到 AuthenticationManager
(配置里 authenticationProvider(smsProvider))
流程串起来:
/login/sms 请求 → SmsAuthenticationFilter 封装 SmsAuthenticationToken
→ AuthenticationManager → 遍历 Provider → SmsAuthenticationProvider(supports)
→ authenticate(校验验证码、加载用户)→ 成功 → 放入 SecurityContext
加手机验证码登录三步:① 定义 Authentication 类型(SmsAuthenticationToken 装手机号+验证码,未认证和已认证两个构造器);② 实现 AuthenticationProvider(authenticate 里校验验证码+加载用户+返回已认证 Token,supports 声明处理 SmsAuthenticationToken);③ 接收请求 + 注册 Provider(加过滤器拦截 /login/sms 封装 Token 交给 AuthenticationManager、注册 Provider)。流程:/login/sms → 过滤器封装 SmsAuthenticationToken → AuthenticationManager 遍历找 SmsAuthenticationProvider → authenticate → 放入 SecurityContext。理解「加登录方式三步:①定义 Authentication 类型(装凭证)②实现 AuthenticationProvider(authenticate 校验+加载用户,supports 声明类型)③加过滤器接收请求+注册 Provider」,就掌握了扩展登录方式的方法。
四、UserDetailsService:加载用户
AuthenticationProvider 认证时要「加载用户」,靠 UserDetailsService:
UserDetailsService(加载用户信息):
UserDetails loadUserByUsername(String username)
→ 根据标识(用户名/手机号)从数据库加载用户
→ 返回 UserDetails(含用户名、密码、权限、账号状态)
在认证中的角色:
AuthenticationProvider 认证时:
① 校验凭证(密码/验证码)
② 用 UserDetailsService.loadUserByUsername 加载用户
③ 拿到用户的权限、状态等
自定义 UserDetailsService(从数据库加载):
@Service
public class MyUserDetailsService implements UserDetailsService {
public UserDetails loadUserByUsername(String username) {
User user = userRepository.findByUsername(username);
if (user == null) throw new UsernameNotFoundException(...);
return new org.springframework.security.core.userdetails.User(
user.getUsername(), user.getPassword(), user.getAuthorities());
}
}
多种登录共用 UserDetailsService:
用户名密码登录:loadUserByUsername(username)
手机验证码登录:loadUserByUsername(mobile) // 用手机号当"username"
→ 只要能按标识查到用户,UserDetailsService 可复用
→ 或为不同登录方式写不同的加载逻辑
关键:
UserDetailsService 负责"用户信息从哪来"(加载)
AuthenticationProvider 负责"怎么校验凭证"(认证逻辑)
→ 两者配合:Provider 校验 + UserDetailsService 加载用户
AuthenticationProvider 认证时靠 UserDetailsService(加载用户)——loadUserByUsername(标识) 从数据库加载用户返回 UserDetails(含用户名/密码/权限/状态)。自定义(从数据库加载):实现 UserDetailsService、findByUsername 查用户返回 UserDetails。多种登录共用:用户名密码用 loadUserByUsername(username)、手机验证码用 loadUserByUsername(mobile)(手机号当 username),只要能按标识查到就可复用。关键:UserDetailsService 负责用户信息从哪来(加载)、AuthenticationProvider 负责怎么校验凭证(认证逻辑),两者配合。理解「UserDetailsService 加载用户(loadUserByUsername 返回 UserDetails 含用户名密码权限)、AuthenticationProvider 认证时用它加载用户;多种登录共用(按标识查)、Provider 校验+UserDetailsService 加载」,就掌握了 UserDetailsService 的角色。
五、扫码登录、第三方登录等
理解其他登录方式怎么套这个模式:
所有登录方式都套"Authentication 类型 + Provider"的模式:
扫码登录:
① ScanAuthenticationToken(装扫码的临时凭证/二维码 id)
② ScanAuthenticationProvider(校验扫码状态、关联用户)
③ 过滤器接收扫码确认请求
→ 用户扫码 → 手机确认 → 服务端标记该二维码已确认某用户
→ PC 轮询/长连接拿到确认 → 用 ScanAuthenticationToken 认证
第三方 OAuth 登录(微信、GitHub):
Spring Security 有 spring-security-oauth2-client
→ OAuth2 登录(换取 code → token → 用户信息)
→ 也可自定义 Provider 处理第三方回调
一次性 Token / 魔法链接登录:
① Token 类型 + Provider(校验一次性 token)
多因子认证(MFA):
先密码认证,再要求第二因子(短信/TOTP)
→ 可以用多个 Provider 分步、或自定义流程
共同点(都是同一套模式):
每种登录方式 = 一个 Authentication 类型 + 一个 AuthenticationProvider
AuthenticationManager 按类型分派
→ 认证是可插拔的,加方式就加 Provider
所以理解了"Authentication + Provider"模式,
任何登录方式都能套这个模式实现
其他登录方式都套「Authentication 类型 + Provider」模式:扫码登录(ScanAuthenticationToken + Provider 校验扫码状态)、第三方 OAuth(spring-security-oauth2-client 或自定义 Provider 处理回调)、一次性 Token/魔法链接(Token 类型 + Provider 校验)、多因子认证(多 Provider 分步)。共同点:每种登录方式 = 一个 Authentication 类型 + 一个 AuthenticationProvider,AuthenticationManager 按类型分派(认证可插拔、加方式就加 Provider)。理解「其他登录方式都套 Authentication+Provider 模式:扫码/OAuth/一次性 Token/MFA;每种=一个 Authentication 类型+一个 Provider、AuthenticationManager 按类型分派、可插拔」,就理解了这套模式的通用性。
六、实践与注意
总结扩展认证的实践和注意点:
实践步骤(加一种登录方式):
① 定义 Authentication 类型(装这种登录的凭证/结果)
② 实现 AuthenticationProvider(supports + authenticate)
③ 加过滤器接收请求、封装成 Authentication
④ 注册 Provider 到 AuthenticationManager
注意点:
① 一种登录方式一个 Provider(别把所有逻辑塞一个 Provider)
→ 职责清晰、可插拔
② supports 要准确声明处理的类型(否则分派错乱)
③ authenticate 失败要抛 AuthenticationException 的子类
(BadCredentialsException、UsernameNotFoundException 等)
④ 已认证的 Authentication 要 setAuthenticated(true) + 带上权限
⑤ 敏感凭证(密码、验证码)认证后要擦除(eraseCredentials)
⑥ 验证码要防刷(限流、过期、一次性)
Spring Security 版本注意:
新版本(Spring Security 5.7+/6)用 SecurityFilterChain Bean 配置
(WebSecurityConfigurerAdapter 已废弃)
AuthenticationManager 的获取方式也变了
核心总结:
Spring Security 认证可插拔:AuthenticationManager + 多 Provider
加登录方式 = Authentication 类型 + AuthenticationProvider
UserDetailsService 加载用户、PasswordEncoder 校验密码
每种方式一个 Provider,按类型分派
扩展认证的实践步骤:① 定义 Authentication 类型 → ② 实现 AuthenticationProvider(supports+authenticate)→ ③ 加过滤器接收请求 → ④ 注册 Provider。注意点:① 一种方式一个 Provider(职责清晰)、② supports 准确声明类型、③ 失败抛 AuthenticationException 子类、④ 已认证要 setAuthenticated(true)+带权限、⑤ 敏感凭证认证后擦除、⑥ 验证码防刷。版本注意:新版用 SecurityFilterChain Bean(WebSecurityConfigurerAdapter 废弃)。理解「实践:定义类型→实现 Provider→加过滤器→注册;注意:一方式一 Provider/supports 准确/失败抛异常/setAuthenticated+权限/擦除凭证/验证码防刷;新版用 SecurityFilterChain」,就掌握了扩展认证的实践。
记忆钩子:「Spring Security 认证可插拔:AuthenticationManager(实现 ProviderManager)持有多个 AuthenticationProvider、认证时遍历问每个 Provider supports 这种 Authentication 类型吗、谁 supports 谁 authenticate(策略模式+责任链);默认 DaoAuthenticationProvider 处理 UsernamePasswordAuthenticationToken(loadUserByUsername+passwordEncoder.matches);加一种登录方式(如手机验证码)三步:①定义 Authentication 类型(SmsAuthenticationToken 装手机号+验证码)②实现 AuthenticationProvider(authenticate 校验验证码+加载用户,supports 声明类型)③加过滤器接收请求封装 Token+注册 Provider;UserDetailsService 加载用户、Provider 校验凭证;每种登录方式=一个 Authentication 类型+一个 Provider、按类型分派」。
七、常见误区与追问
- 误区:Spring Security 只支持用户名密码登录。 认证是可插拔的——通过自定义 AuthenticationProvider 可以支持任意登录方式(手机验证码、扫码、第三方 OAuth、一次性 Token 等);每种登录方式 = 一个 Authentication 类型 + 一个 AuthenticationProvider,AuthenticationManager 按类型分派。
- 误区:AuthenticationManager 自己做认证。 AuthenticationManager(实现类 ProviderManager)本身不做具体认证,它持有一个 AuthenticationProvider 列表,认证时遍历问每个 Provider「supports 这种类型吗」、找到能处理的交给它 authenticate;它是「分派器」,具体认证在 Provider 里。
- 误区:所有登录逻辑塞进一个 AuthenticationProvider。 应该一种登录方式一个 Provider——每个 Provider 用 supports 声明自己处理哪种 Authentication 类型,职责清晰、可插拔;把所有登录逻辑塞一个 Provider 会难维护、违背策略模式的设计。
- 误区:AuthenticationProvider 和 UserDetailsService 是一回事。 分工不同——AuthenticationProvider 负责「怎么校验凭证」(认证逻辑,如比对密码、校验验证码);UserDetailsService 负责「用户信息从哪来」(loadUserByUsername 从数据库加载用户);Provider 认证时会用 UserDetailsService 加载用户,两者配合。
- 追问:怎么给 Spring Security 加一种手机验证码登录? 三步:① 定义一个 Authentication 类型(如 SmsAuthenticationToken,装手机号和验证码);② 实现一个 AuthenticationProvider(authenticate 里校验验证码是否正确、用 UserDetailsService 按手机号加载用户、返回已认证的 Token;supports 声明处理 SmsAuthenticationToken);③ 加一个过滤器接收 /login/sms 请求、把手机号和验证码封装成 SmsAuthenticationToken 交给 AuthenticationManager,并把这个 Provider 注册到 AuthenticationManager。
- 追问:AuthenticationManager 是怎么把认证分派给正确的 Provider 的? ProviderManager(AuthenticationManager 的实现)持有一个 AuthenticationProvider 列表,authenticate 时遍历这个列表,对每个 Provider 调用 supports(authentication.getClass()) 判断它能否处理这种 Authentication 类型,找到第一个 supports 返回 true 的 Provider,就调用它的 authenticate 执行认证;这是策略模式 + 责任链——按 Authentication 类型分派给对应的 Provider。
- 追问:UserDetailsService 在多种登录方式里怎么复用? UserDetailsService 的 loadUserByUsername 接收一个「标识」加载用户——用户名密码登录传 username、手机验证码登录传 mobile(把手机号当作标识)、第三方登录传第三方的唯一 id;只要能按这个标识从数据库查到用户、返回 UserDetails,同一个 UserDetailsService 就能被多种登录方式复用;也可以为不同登录方式写不同的加载逻辑(如按手机号查 vs 按用户名查)。
八、加强记忆
Spring Security 的认证是可插拔的——核心是「AuthenticationManager + 多个 AuthenticationProvider」的分派结构,支持多种登录方式(用户名密码、手机验证码、扫码、第三方)。AuthenticationManager(实现类 ProviderManager)不做具体认证,它持有 AuthenticationProvider 列表,认证时遍历问每个 Provider「你 supports 这种 Authentication 类型吗」,谁 supports 谁 authenticate(策略模式 + 责任链)。默认 DaoAuthenticationProvider 处理 UsernamePasswordAuthenticationToken(用 UserDetailsService.loadUserByUsername 加载用户 + PasswordEncoder.matches 比对密码)。加一种登录方式(如手机验证码)三步:① 定义 Authentication 类型(SmsAuthenticationToken 装手机号+验证码);② 实现 AuthenticationProvider(authenticate 里校验验证码 + 加载用户 + 返回已认证 Token,supports 声明处理该类型);③ 加过滤器接收请求封装 Token + 注册 Provider。分工:AuthenticationProvider 负责「怎么校验凭证」、UserDetailsService 负责「用户信息从哪来(加载)」。每种登录方式 = 一个 Authentication 类型 + 一个 AuthenticationProvider,AuthenticationManager 按类型分派——扫码、OAuth、MFA 都套这个模式。一句话「Spring Security 认证可插拔:AuthenticationManager(ProviderManager)持多个 AuthenticationProvider、遍历问 supports 谁处理(策略模式+责任链);加登录方式三步:定义 Authentication 类型+实现 Provider(authenticate 校验+supports 声明)+加过滤器注册;Provider 校验凭证、UserDetailsService 加载用户;每种方式一个 Provider 按类型分派」。