基于 SpringBoot 工厂+策略模式统一多端登录

35 阅读7分钟

点击上方“程序员蜗牛g”,选择“设为星标”

跟蜗牛哥一起,每天进步一点点

程序员蜗牛g

大厂程序员一枚 跟蜗牛一起 每天进步一点点

33篇原创内容

**

公众号

 

一、需求分析:当产品说 "我们要支持 10 种登录方式"

1. 典型登录场景:代码里的 "联合国"

假设我们要实现一个支持三种登录方式的系统:

  • 用户名密码登录:  需要校验密码加密、账号是否锁定
  • 微信扫码登录:  需要调用微信开放平台 API,校验授权码
  • 手机号验证码登录:  需要生成验证码、校验有效期和正确性

传统写法是写一个LoginService,用if-else判断登录类型:

public String login(String loginType, Map<String, Object> params) {
    if ("password".equals(loginType)) {
        // 用户名密码登录逻辑
    } else if ("wechat".equals(loginType)) {
        // 微信登录逻辑
    } else if ("sms".equals(loginType)) {
        // 手机号登录逻辑
    } else {
        throw new IllegalArgumentException("不支持的登录类型");
    }
}

这种写法的问题在于:

  • 扩展性差:  新增登录方式要改if-else,违反开闭原则
  • 职责混乱:  所有逻辑挤在一个类里,可读性差
  • 复用困难:  不同登录方式的公共逻辑(如用户校验)无法抽取
2. 设计模式选择:策略模式解耦算法,工厂模式创建实例

策略模式:将每种登录方式封装成独立策略类,实现统一接口,调用者无需关心具体实现 工厂模式:通过工厂类根据登录类型创建对应的策略实例,避免调用者直接 new 对象

二、Spring Boot 项目搭建:先搭好 "舞台"

1. 创建 Spring Boot 项目

添加 Web 依赖,项目结构如下:

src/main/java/com/example/login
├── config
│   └── StrategyConfig.java  // 策略Bean配置
├── controller
│   └── LoginController.java // 登录控制器
├── factory
│   └── LoginStrategyFactory.java // 登录策略工厂
├── model
│   └── LoginRequest.java    // 登录请求参数
├── service
│   ├── impl
│   │   ├── PasswordLoginStrategy.java  // 用户名密码策略
│   │   ├── WechatLoginStrategy.java    // 微信策略
│   │   └── SmsLoginStrategy.java       // 手机号策略
│   └── LoginStrategy.java   // 登录策略接口
└── Application.java
2. 定义统一登录策略接口
public interface LoginStrategy {
    // 登录类型标识,如"password"、"wechat"
    String getLoginType();
    
    // 登录方法,参数用Map传递不同登录方式的参数
    String execute(Map<String, Object> params);
}

三、策略模式实现:每种登录方式都是 "独立演员"

1. 用户名密码登录策略
@Service
publicclass PasswordLoginStrategy implements LoginStrategy {
    @Override
    public String getLoginType() {
        return"password";
    }
    @Override
    public String execute(Map<String, Object> params) {
        String username = (String) params.get("username");
        String password = (String) params.get("password");
        // 模拟密码校验(实际应从数据库查询+密码解密)
        if (!"123456".equals(password)) {
            thrownew IllegalArgumentException("密码错误");
        }
        // 模拟用户校验
        checkUserLocked(username);
        return"登录成功(用户名密码)";
    }
    private void checkUserLocked(String username) {
        // 调用用户服务检查账号是否锁定
        System.out.println("检查用户" + username + "是否锁定");
    }
}
2. 微信扫码登录策略
@Service
publicclass WechatLoginStrategy implements LoginStrategy {
    @Override
    public String getLoginType() {
        return"wechat";
    }
    @Override
    public String execute(Map<String, Object> params) {
        String authCode = (String) params.get("authCode");
        // 模拟调用微信接口获取用户信息
        String openId = callWechatApi(authCode);
        // 模拟数据库查询用户绑定关系
        String userId = getUserIdByOpenId(openId);
        if (userId == null) {
            thrownew IllegalArgumentException("微信账号未绑定系统用户");
        }
        return"登录成功(微信扫码)";
    }
    private String callWechatApi(String authCode) {
        // 实际应调用微信开放平台API
        System.out.println("调用微信接口,authCode=" + authCode);
        return"wechat_open_id_123";
    }
}
3. 手机号验证码登录策略
@Service
publicclass SmsLoginStrategy implements LoginStrategy {
    @Override
    public String getLoginType() {
        return"sms";
    }
    @Override
    public String execute(Map<String, Object> params) {
        String phone = (String) params.get("phone");
        String code = (String) params.get("code");
        // 模拟验证码校验(实际应从Redis获取)
        if (!"666888".equals(code)) {
            thrownew IllegalArgumentException("验证码错误");
        }
        // 模拟用户校验
        checkPhoneRegistered(phone);
        return"登录成功(手机号验证码)";
    }
    private void checkPhoneRegistered(String phone) {
        // 检查手机号是否注册
        System.out.println("检查手机号" + phone + "是否注册");
    }
}

四、工厂模式实现:让 "导演" 决定用哪个 "演员"

1. 登录策略工厂类
@Component
publicclass LoginStrategyFactory {
    privatefinal Map<String, LoginStrategy> strategyMap;
    // 通过Spring依赖注入获取所有LoginStrategy实现类
    public LoginStrategyFactory(Map<String, LoginStrategy> strategyMap) {
        this.strategyMap = strategyMap;
    }
    public LoginStrategy getStrategy(String loginType) {
        LoginStrategy strategy = strategyMap.get(loginType);
        if (strategy == null) {
            thrownew IllegalArgumentException("不支持的登录类型:" + loginType);
        }
        return strategy;
    }
}

这里利用 Spring 的自动装配,将所有@Service标记的LoginStrategy实现类注入到strategyMap中,键为 Bean 名称(默认是类名首字母小写,如passwordLoginStrategy),但我们在策略类中通过getLoginType()返回自定义的类型标识,所以需要在配置类中调整 Bean 名称:

2. 策略 Bean 配置(关键!)
@Configuration
publicclass StrategyConfig {
    @Bean("passwordStrategy"// 自定义Bean名称
    public LoginStrategy passwordLoginStrategy() {
        returnnew PasswordLoginStrategy();
    }
    @Bean("wechatStrategy")
    public LoginStrategy wechatLoginStrategy() {
        returnnew WechatLoginStrategy();
    }
    @Bean("smsStrategy")
    public LoginStrategy smsLoginStrategy() {
        returnnew SmsLoginStrategy();
    }
}

然后在策略类中重写getLoginType()返回和前端约定的类型标识(如 "password"),并在工厂类中建立类型标识到 Bean 的映射:

// 修改工厂类的构造方法,建立正确映射
public LoginStrategyFactory(Map<String, LoginStrategy> strategyMap) {
    this.strategyMap = new HashMap<>();
    strategyMap.forEach((beanName, strategy) -> 
        this.strategyMap.put(strategy.getLoginType(), strategy)
    );
}

五、控制器集成:对外提供统一接口

1. 登录请求参数类
public class LoginRequest {
    private String loginType; // 登录类型,如"password"、"wechat"
    private Map<String, Object> params; // 具体参数,不同登录方式不同
    // 省略getter/setter
}
2. 登录控制器
@RestController
@RequestMapping("/login")
publicclass LoginController {
    privatefinal LoginStrategyFactory factory;
    @Autowired
    public LoginController(LoginStrategyFactory factory) {
        this.factory = factory;
    }
    @PostMapping
    public String login(@RequestBody LoginRequest request) {
        String loginType = request.getLoginType();
        Map<String, Object> params = request.getParams();
        LoginStrategy strategy = factory.getStrategy(loginType);
        return strategy.execute(params);
    }
}

六、测试验证:三种登录方式轻松切换

1. 用户名密码登录请求
{
  "loginType": "password",
  "params": {
    "username": "user123",
    "password": "123456"
  }
}
2. 微信扫码登录请求
{
  "loginType": "wechat",
  "params": {
    "authCode": "wechat_auth_code_456"
  }
}
3. 新增登录方式:比如支付宝登录

只需新增AlipayLoginStrategy类并实现接口,无需修改现有代码:

@Service("alipayStrategy")
public class AlipayLoginStrategy implements LoginStrategy {
    @Override
    public String getLoginType() {
        return "alipay";
    }
    @Override
    public String execute(Map<String, Object> params) {
        // 支付宝登录逻辑
        return "登录成功(支付宝)";
    }
}

七、核心优势:让代码具备 "抗需求变化体质"

1. 策略模式的好处
  • 解耦算法:  每种登录逻辑独立在策略类中,可读性强
  • 易于扩展:  新增登录方式只需添加新策略类,符合开闭原则
  • 方便测试:  可以单独测试每个策略类,无需关心其他逻辑
2. 工厂模式的好处
  • 封装创建逻辑:  调用者无需知道策略类的创建细节
  • 集中管理实例:  通过 Spring 管理策略 Bean,支持依赖注入和生命周期管理
3. 结合 Spring Boot 的优势
  • 自动装配:  通过@ServiceMap<String, LoginStrategy>自动收集所有策略 Bean
  • 类型安全:  工厂类在运行时检查登录类型是否合法,避免NullPointerException

八、最佳实践:这些细节别忽略

1. 参数校验前置

在控制器中先对loginTypeparams做基础校验,比如必填参数检查,避免策略类中重复校验

2. 公共逻辑抽取

如果多种登录方式有公共逻辑(如登录成功后的 Token 生成),可以创建抽象策略类,让具体策略类继承

3. 日志和异常处理

在策略类中添加登录日志记录,统一捕获异常并返回友好的错误信息:

@Service
public class PasswordLoginStrategy implements LoginStrategy {
    @Override
    public String execute(Map<String, Object> params) {
        try {
            // 登录逻辑
        } catch (Exception e) {
            log.error("用户名密码登录失败:{}", e.getMessage());
            throw new LoginException("登录失败,请重试"); // 自定义业务异常
        }
    }
}
4. 配置化登录类型

将支持的登录类型存储在配置文件中,前端调用时先获取支持的登录类型列表,避免硬编码

九、总结:设计模式让代码更有 "尊严"

回顾三年前的面条代码,再看现在的实现,最大的感受是:好的设计模式能让代码在需求变化面前保持优雅。工厂模式和策略模式的组合,就像给登录模块装了一个 "热插拔" 接口,新增功能时不用改核心逻辑,只需要添加新的 "插件"。

如果这篇文章对您有所帮助,或者有所启发的话,求一键三连:点赞、转发、在看。

关注公众号:woniuxgg,在公众号中回复:笔记  就可以获得蜗牛为你精心准备的java实战语雀笔记,回复面试、开发手册、有超赞的粉丝福利