作者:旷野说
前情提要:上回我们搞定了静默 403,这次产品又来了个新需求……
🧩 一、那个让人夜不能寐的需求
上周五下班前,产品拉住我:
“我们要支持短信验证码登录,但账号密码登录也要保留。
而且,两种方式登录后,权限、Token、审计日志……都要一样。”
我表面点头,心里打鼓。
“这不就是两个登录接口吗?”
可转念一想:如果我手动校验、手动生成 Token,那 Spring Security 的 @PreAuthorize、SecurityContext、权限决策链……不就全废了?
难道要维护两套安全逻辑?
那一刻,我意识到:
真正的挑战,不是“实现登录”,而是“如何让两种登录方式,被同一个安全体系认可”。
🔍 二、回到原点:Spring Security 的认证,到底怎么工作的?
我翻出 Spring Security 的核心接口:
public interface AuthenticationManager {
Authentication authenticate(Authentication authentication) throws AuthenticationException;
}
所有登录,最终都调用它。
而它的默认实现是:ProviderManager。
🤔 为什么叫 “Provider” Manager?
谁是 “Provider”?它和“登录方式”有什么关系?
我画了一张草图:
[用户名/密码] → Authentication (UsernamePasswordToken)
[手机号/验证码] → Authentication (SmsCodeToken)
↓
AuthenticationManager
↓
┌─────────┴─────────┐
▼ ▼
PasswordAuthenticationProvider SmsCodeAuthenticationProvider
✅ 原来,“Provider” 就是“认证提供者”——每种登录方式,对应一个 Provider!
而 ProviderManager 的作用,就是:根据 Token 类型,自动路由到对应的 Provider。
💡 这就是多认证源的底层逻辑:类型驱动 + 职责分离。
🧱 三、为什么需要 ProviderManager?——设计基础三问
问 1:为什么不能在一个 Provider 里 if-else 处理所有登录?
答:违反单一职责原则。
密码校验、短信校验、微信扫码……逻辑完全不同,混在一起,代码将迅速腐化。
问 2:为什么用 Authentication 子类区分,而不是用字段?
答:类型即契约。
supports(Class<?>) 方法让匹配编译期可见、运行时高效,比字符串判断(如 loginType="sms")更安全、更可扩展。
问 3:为什么需要 ProviderManager,而不是直接调用 Provider?
答:统一入口 + 异常聚合 + 扩展点。
- 业务层只需依赖
AuthenticationManager,无需知道有多少种登录方式; - 失败时可尝试多个 Provider(虽不常用);
- 未来加新登录方式,零修改现有代码。
🧩
ProviderManager的存在,是为了“解耦认证策略”与“认证执行”。
🛠️ 四、实战:三步接入短信登录
第一步:定义专属 Token
public class SmsCodeAuthenticationToken extends AbstractAuthenticationToken {
private final String phone;
private final String code;
public SmsCodeAuthenticationToken(String phone, String code) {
super(null); // 未认证前,权限为 null
this.phone = phone;
this.code = code;
setAuthenticated(false);
}
// 已认证构造函数(Provider 内部使用)
public SmsCodeAuthenticationToken(UserDetails principal, Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.phone = principal.getUsername(); // 或单独存手机号
this.code = null;
setAuthenticated(true);
}
}
第二步:实现 Provider
@Component
public class SmsCodeAuthenticationProvider implements AuthenticationProvider {
@Override
public boolean supports(Class<?> authentication) {
return SmsCodeAuthenticationToken.class.isAssignableFrom(authentication);
}
@Override
public Authentication authenticate(Authentication auth) {
SmsCodeAuthenticationToken token = (SmsCodeAuthenticationToken) auth;
// 1. 校验验证码(Redis)
// 2. 加载用户
// 3. 返回已认证 Token
UserDetails user = userDetailsService.loadUserByPhone(token.getPhone());
return new SmsCodeAuthenticationToken(user, user.getAuthorities());
}
}
第三步:Controller 调用统一入口
@PostMapping("/login/sms")
public ResponseEntity<?> smsLogin(@RequestBody SmsLoginDto dto) {
Authentication auth = authenticationManager.authenticate(
new SmsCodeAuthenticationToken(dto.getPhone(), dto.getCode())
);
// 生成 JWT,和密码登录完全一致!
}
✅ 从此,两种登录方式,在 Spring Security 眼中“一视同仁”。
🧠 五、核心思想提炼:为什么这样设计?
Spring Security 的多认证源机制,背后是三个设计哲学:
- 类型即身份:不同登录方式 = 不同 Token 类型;
- 策略可插拔:每个 Provider 是独立策略,互不影响;
- 入口统一化:业务只面对
AuthenticationManager,不感知底层差异。
这正是 “面向接口编程” + “策略模式” 的经典实践。
🧵 六、终极“中诀”:多认证源三字经
认、分、统
——认证靠类型,职责要分清,入口须统一。
详解:
-
认(认证靠类型):
用Authentication子类区分登录方式,supports()自动匹配。 -
分(职责要分清):
每种登录方式一个 Provider,各司其职,互不干扰。 -
统(入口须统一):
所有认证走AuthenticationManager,安全上下文自动生效。
✅ 三字记心间,多源认证不再难!
✅ 七、写在最后
曾经,我把 Spring Security 当成一个“登录框架”。
现在,我明白它是一套可扩展的安全内核。
ProviderManager 不只是一个类,而是一种设计思维:
面对变化,不要 if-else,而要插件化。
当你下次需要接入微信、人脸、硬件 Key……
只需记住:
“认、分、统” —— 三字破万法。
愿你的系统,既灵活,又安全。 🔐
416



被折叠的 条评论
为什么被折叠?



