多端登录还在写 if-else?工厂 + 策略模式一招解耦
密码、微信、短信验证码……登录方式一多,LoginService 里就容易堆成一坨 if-else。用策略模式把每种登录封装成独立算法、工厂模式负责按类型创建实例,新增一种登录方式只加一个类、零改动老代码。这篇给一套能直接落地的 Spring Boot 实现。
登录接口大概是后端项目里最容易”长歪”的地方。 Product 一句”我们要支持微信扫码”,你就往 LoginService 里加一个 else if;过两周又来”再加个手机号验证码”,再加一个。等支持到第五种方式时,一个方法里塞了上百行分支,改一处生怕漏了另一处的校验。
这不是你代码写得差,是用 if-else 表达”多分支算法”天生就难维护——它把”算法选择”和”算法本身”揉在了一起。解决办法其实是设计模式里的两个老面孔:策略模式(Strategy)负责把每种登录拆成独立算法,工厂模式(Factory)负责按类型把算法挑出来。两者一组合,登录模块就变成了一个”热插拔”接口。
下面是一套能直接落地的 Spring Boot 实现,从接口定义到控制器集成,最后看怎么”加一种登录方式只动一个文件”。
一、先把问题说清楚:传统写法卡在哪
典型的”面条登录”长这样:
public String login(String loginType, Map<String, Object> params) {
if ("password".equals(loginType)) {
// 用户名密码:校验密码、查账号是否锁定
} else if ("wechat".equals(loginType)) {
// 微信:拿 authCode 调开放平台、绑定关系
} else if ("sms".equals(loginType)) {
// 短信:校验验证码有效期和正确性
} else {
throw new IllegalArgumentException("不支持的登录类型");
}
}
它有三个绕不开的坑:
- 违反开闭原则:每加一种登录方式,就得回来改这个方法。改得越多,回归测试越心惊。
- 职责混乱:密码加密、调微信 API、查 Redis 验证码全挤在一个类里,可读性差,新人接手一头雾水。
- 公共逻辑抽不出:登录成功后生成 Token、写登录日志,每种方式都要一遍——复制粘贴就埋下了不一致的种子。
策略模式正好对症:把”每种登录怎么走”封装成独立类,调用方只认一个统一接口,不关心里面是什么。
二、定义统一策略接口
先约定一个接口,所有登录方式都实现它。getLoginType() 返回和前端约定的类型标识(如 password / wechat / sms),execute() 是真正的登录逻辑。
public interface LoginStrategy {
/** 登录类型标识,与前端约定的值一致,如 "password"、"wechat" */
String getLoginType();
/** 执行登录,返回登录结果(Token、用户信息等) */
LoginResult execute(LoginRequest request);
}
请求参数用一个统一入参包住类型 + 业务参数,不同登录方式参数不同,业务参数放在 params 里:
public class LoginRequest {
/** 登录类型:password / wechat / sms */
private String loginType;
/** 具体业务参数,不同登录方式字段不同 */
private Map<String, Object> params;
// getter / setter 省略
}
这里
params用Map是为了让接口不被某一种登录方式的字段绑架。如果你追求更强类型,也可以让每个策略自己定义一个 DTO,在execute内部转换——接口层保持宽松,实现层各自严丝合缝。
三、三种登录策略:每种都是独立”演员”
每个策略只管自己的事。注意它们都标了 @Service,交给 Spring 管理。
用户名密码登录:
@Service
public class PasswordLoginStrategy implements LoginStrategy {
@Override
public String getLoginType() {
return "password";
}
@Override
public LoginResult execute(LoginRequest request) {
String username = (String) request.getParams().get("username");
String password = (String) request.getParams().get("password");
// 实际应从数据库查用户 + 校验加密后的密码
if (!isValidPassword(username, password)) {
throw new LoginException("用户名或密码错误");
}
checkUserLocked(username);
return buildLoginResult(username);
}
private void checkUserLocked(String username) {
// 查账号是否被锁定,锁了抛异常
}
}
微信扫码登录:
@Service
public class WechatLoginStrategy implements LoginStrategy {
@Override
public String getLoginType() {
return "wechat";
}
@Override
public LoginResult execute(LoginRequest request) {
String authCode = (String) request.getParams().get("authCode");
// 拿 authCode 调微信开放平台,换 openid
String openId = wechatClient.exchangeOpenId(authCode);
// 用 openid 查系统用户绑定关系
String userId = userBindingRepository.findUserIdByOpenId(openId);
if (userId == null) {
throw new LoginException("微信账号未绑定系统用户");
}
return buildLoginResult(userId);
}
}
手机号验证码登录:
@Service
public class SmsLoginStrategy implements LoginStrategy {
@Override
public String getLoginType() {
return "sms";
}
@Override
public LoginResult execute(LoginRequest request) {
String phone = (String) request.getParams().get("phone");
String code = (String) request.getParams().get("code");
// 实际应从 Redis 取验证码并比对,顺便校验有效期
if (!smsCodeRepository.matches(phone, code)) {
throw new LoginException("验证码错误");
}
checkPhoneRegistered(phone);
return buildLoginResult(phone);
}
}
每个类各写各的,互不干扰,单测也能各自独立跑——这是和 if-else 版本比最直观的收益。
四、工厂:按类型把策略挑出来(关键的一步)
思路是:让 Spring 把所有的 LoginStrategy 实现类收集起来,工厂在构造时按 getLoginType() 建一张映射表。调用方拿类型来查,查不到就直接报错。
@Component
public class LoginStrategyFactory {
private final Map<String, LoginStrategy> strategyMap;
/** Spring 自动注入所有 LoginStrategy 实现,构造时按类型建索引 */
public LoginStrategyFactory(List<LoginStrategy> strategies) {
this.strategyMap = strategies.stream()
.collect(Collectors.toMap(
LoginStrategy::getLoginType,
s -> s,
(a, b) -> { throw new IllegalStateException("登录类型重复: " + a.getLoginType()); }
));
}
public LoginStrategy getStrategy(String loginType) {
LoginStrategy strategy = strategyMap.get(loginType);
if (strategy == null) {
throw new UnsupportedLoginTypeException("不支持的登录类型: " + loginType);
}
return strategy;
}
}
网上不少教程会再写一个
@Configuration去逐个@Bean并手动指定 Bean 名,其实多此一举——既然每个策略已经用getLoginType()自报类型,直接按这个字段建表就够了,不需要再维护一份 Bean 名到类型的对应关系。少一层映射,就少一处会过期的配置。那个
(a, b) -> throw ...是防止两个策略返回了同一个loginType,及早暴露而不是静默覆盖。
五、控制器:对外只有这一个入口
控制器完全不关心具体登录逻辑,只负责”解析类型 → 问工厂拿策略 → 执行”:
@RestController
@RequestMapping("/login")
public class LoginController {
private final LoginStrategyFactory factory;
public LoginController(LoginStrategyFactory factory) {
this.factory = factory;
}
@PostMapping
public LoginResult login(@RequestBody LoginRequest request) {
LoginStrategy strategy = factory.getStrategy(request.getLoginType());
return strategy.execute(request);
}
}
前端三种登录方式,请求体只是 loginType 不同:
{ "loginType": "password", "params": { "username": "user123", "password": "******" } }
{ "loginType": "wechat", "params": { "authCode": "wechat_auth_code_456" } }
{ "loginType": "sms", "params": { "phone": "13800000000", "code": "666888" } }
六、加一种登录方式:只加一个类
现在产品说”再加个支付宝登录”。你只需要新建一个类,不用动任何老代码:
@Service
public class AlipayLoginStrategy implements LoginStrategy {
@Override
public String getLoginType() {
return "alipay";
}
@Override
public LoginResult execute(LoginRequest request) {
String authCode = (String) request.getParams().get("authCode");
// 支付宝登录逻辑:用 authCode 换 userId、绑定关系……
return buildLoginResult(exchangeAlipayUserId(authCode));
}
}
Spring 启动时会把它自动收进 List<LoginStrategy>,工厂的映射表自动多一项,/login 接口自动支持 alipay。这就是开闭原则落到实处——对扩展开放,对修改关闭。
七、什么时候该上这套,什么时候别上
好处很明确:
- 解耦:每种登录逻辑独立成类,可读性、可测性都上来了。
- 易扩展:加方式 = 加类,老代码一行不动。
- 类型安全:工厂在运行期就能拦掉非法
loginType,不会等到策略里才 NPE。
但也要说实话:如果登录方式就一两种,且长期不变,直接用 if-else 反而更直白。设计模式是为了对抗”变化”,没有变化就别提前上模式,那叫过度设计。判断标准很简单——你预计未来半年还会加登录方式吗?会,就上;不会,就别折腾。
顺带一提,平台级登录尤其适合这套。像我们 FOLIO 项目里要同时接密码登录和机构 SSO,登录入口一旦用 if-else 堆起来,每加一种认证源就是一次”地震”;换成策略 + 工厂,新增认证源只是注册一个策略 Bean,auth 模块的骨架稳得住。
八、几个别忽略的实践细节
- 参数校验前置:
loginType是否为空、必填字段是否齐,在控制器或统一校验里先拦,别让每个策略类都重复判空。 - 公共逻辑抽抽象类:登录成功生成 Token、写登录日志,这些大家都有的动作,抽到一个
AbstractLoginStrategy,具体策略继承它,避免到处复制。 - 统一异常:策略里抛具体的
LoginException/UnsupportedLoginTypeException,由全局异常处理器翻译成友好的错误结构和状态码,别让 Controller 直接兜底。 - 配置化支持的类型:把当前支持的
loginType列表暴露成一个接口(或读配置文件),前端渲染登录按钮时先拉这份清单,避免硬编码。
小结
登录逻辑别硬刚,策略模式来分装;工厂负责挑实例,开闭原则记心上。
说到底,工厂 + 策略不是什么高深技巧,它只是把”选择算法的代码”和”算法的代码”分开——这一分,登录模块就从一团乱麻变成了可插拔的插件集。下次 Product 再提”加个登录方式”,你回一句”加个类就行”,比改十七处代码体面多了。
关注我