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

一、问题背景

在多端登录场景中,通常存在以下痛点:

  1. 登录逻辑分散:不同端(Web、APP、小程序)的登录逻辑散落在不同地方
  2. 代码重复:相似的登录逻辑在多处重复实现
  3. 扩展困难:新增登录方式或修改逻辑需要改动多处代码
  4. 维护复杂:登录策略变化时需要追踪所有相关代码

二、解决方案设计

2.1 架构概览

┌─────────────────────────────────────────┐
│            LoginController              │
│  (统一入口,接收登录请求)                │
└───────────────────┬─────────────────────┘
                    │
┌───────────────────▼─────────────────────┐
│          LoginStrategyFactory           │
│     (工厂模式,根据登录类型创建策略)      │
└───────────────────┬─────────────────────┘
                    │
         ┌──────────┴──────────┐
         ▼                     ▼
┌─────────────────┐  ┌─────────────────┐
│ PasswordStrategy│  │   SmsStrategy   │
│ (密码登录策略)   │  │ (短信登录策略)   │
└─────────────────┘  └─────────────────┘
         │                     │
         ▼                     ▼
┌─────────────────┐  ┌─────────────────┐
│   WebLogin      │  │   AppLogin      │
│ (Web端登录实现)  │  │ (APP端登录实现)  │
└─────────────────┘  └─────────────────┘

2.2 核心设计模式

  1. 策略模式:定义一系列登录算法,封装每个算法
  2. 工厂模式:根据登录类型创建对应的策略实例
  3. 模板方法模式:定义登录的标准流程

三、代码实现

3.1 策略接口定义

// 登录策略接口
public interface LoginStrategy {
    
    /**
     * 登录类型枚举
     */
    enum LoginType {
        PASSWORD("password", "密码登录"),
        SMS("sms", "短信登录"),
        WECHAT("wechat", "微信登录"),
        QQ("qq", "QQ登录");
        
        private final String code;
        private final String desc;
        
        LoginType(String code, String desc) {
            this.code = code;
            this.desc = desc;
        }
        
        public String getCode() {
            return code;
        }
    }
    
    /**
     * 支持的登录类型
     */
    LoginType getLoginType();
    
    /**
     * 登录验证
     */
    LoginResult login(LoginRequest request);
    
    /**
     * 登录前置处理
     */
    default void preLogin(LoginRequest request) {
        // 通用前置处理:参数校验、频次控制等
    }
    
    /**
     * 登录后置处理
     */
    default void postLogin(LoginResult result) {
        // 通用后置处理:记录日志、发送事件等
    }
}

// 登录请求DTO
@Data
public class LoginRequest {
    private String loginType;
    private String username;
    private String password;
    private String phone;
    private String smsCode;
    private String openId;
    private String deviceType; // web, app, mini-program
    private String ip;
    private Map<String, Object> extraParams;
}

// 登录结果DTO
@Data
public class LoginResult {
    private boolean success;
    private String token;
    private UserInfo userInfo;
    private String message;
    private Date loginTime;
    private Integer expireIn;
}

3.2 抽象策略类(模板方法)

/**
 * 抽象登录策略,定义登录模板流程
 */
public abstract class AbstractLoginStrategy implements LoginStrategy {
    
    @Override
    public LoginResult login(LoginRequest request) {
        // 1. 前置处理
        preLogin(request);
        
        // 2. 参数验证
        validate(request);
        
        // 3. 具体登录逻辑(由子类实现)
        LoginResult result = doLogin(request);
        
        // 4. 后置处理
        postLogin(result);
        
        return result;
    }
    
    /**
     * 参数验证
     */
    protected void validate(LoginRequest request) {
        if (StringUtils.isEmpty(request.getDeviceType())) {
            throw new BusinessException("设备类型不能为空");
        }
    }
    
    /**
     * 具体登录逻辑(子类实现)
     */
    protected abstract LoginResult doLogin(LoginRequest request);
    
    /**
     * 生成Token
     */
    protected String generateToken(User user) {
        return JwtUtil.generateToken(user.getId(), user.getUsername());
    }
}

3.3 具体策略实现

/**
 * 密码登录策略
 */
@Component
public class PasswordLoginStrategy extends AbstractLoginStrategy {
    
    @Autowired
    private UserService userService;
    
    @Autowired
    private PasswordEncoder passwordEncoder;
    
    @Override
    public LoginType getLoginType() {
        return LoginType.PASSWORD;
    }
    
    @Override
    protected LoginResult doLogin(LoginRequest request) {
        // 1. 查找用户
        User user = userService.findByUsername(request.getUsername());
        if (user == null) {
            throw new BusinessException("用户不存在");
        }
        
        // 2. 验证密码
        if (!passwordEncoder.matches(request.getPassword(), user.getPassword())) {
            throw new BusinessException("密码错误");
        }
        
        // 3. 生成Token
        String token = generateToken(user);
        
        // 4. 返回结果
        LoginResult result = new LoginResult();
        result.setSuccess(true);
        result.setToken(token);
        result.setUserInfo(convertToUserInfo(user));
        result.setLoginTime(new Date());
        result.setExpireIn(3600);
        
        return result;
    }
    
    @Override
    protected void validate(LoginRequest request) {
        super.validate(request);
        if (StringUtils.isEmpty(request.getUsername())) {
            throw new BusinessException("用户名不能为空");
        }
        if (StringUtils.isEmpty(request.getPassword())) {
            throw new BusinessException("密码不能为空");
        }
    }
}

/**
 * 短信登录策略
 */
@Component
public class SmsLoginStrategy extends AbstractLoginStrategy {
    
    @Autowired
    private UserService userService;
    
    @Autowired
    private SmsService smsService;
    
    @Override
    public LoginType getLoginType() {
        return LoginType.SMS;
    }
    
    @Override
    protected LoginResult doLogin(LoginRequest request) {
        // 1. 验证短信验证码
        boolean valid = smsService.verifyCode(request.getPhone(), request.getSmsCode());
        if (!valid) {
            throw new BusinessException("验证码错误或已过期");
        }
        
        // 2. 查找或创建用户
        User user = userService.findByPhone(request.getPhone());
        if (user == null) {
            user = userService.createUserByPhone(request.getPhone());
        }
        
        // 3. 生成Token
        String token = generateToken(user);
        
        // 4. 返回结果
        LoginResult result = new LoginResult();
        result.setSuccess(true);
        result.setToken(token);
        result.setUserInfo(convertToUserInfo(user));
        result.setLoginTime(new Date());
        
        return result;
    }
}

3.4 端适配器(处理端差异)

/**
 * 端适配器接口
 */
public interface DeviceAdapter {
    
    /**
     * 支持的设备类型
     */
    String getDeviceType();
    
    /**
     * 设备特定的登录处理
     */
    void adaptLogin(LoginRequest request, LoginResult result);
    
    /**
     * 设备特定的Token处理
     */
    String adaptToken(String originalToken);
}

/**
 * Web端适配器
 */
@Component
public class WebDeviceAdapter implements DeviceAdapter {
    
    @Override
    public String getDeviceType() {
        return "web";
    }
    
    @Override
    public void adaptLogin(LoginRequest request, LoginResult result) {
        // Web端特定处理:设置Cookie、Session等
        result.setExpireIn(7200); // Web端Token有效期2小时
    }
    
    @Override
    public String adaptToken(String originalToken) {
        // Web端可能需要在Token前加前缀
        return "web_" + originalToken;
    }
}

/**
 * APP端适配器
 */
@Component
public class AppDeviceAdapter implements DeviceAdapter {
    
    @Override
    public String getDeviceType() {
        return "app";
    }
    
    @Override
    public void adaptLogin(LoginRequest request, LoginResult result) {
        // APP端特定处理:设备信息绑定、推送Token注册等
        result.setExpireIn(2592000); // APP端Token有效期30天
    }
    
    @Override
    public String adaptToken(String originalToken) {
        // APP端Token处理
        return "app_" + originalToken;
    }
}

3.5 策略工厂

/**
 * 登录策略工厂
 */
@Component
public class LoginStrategyFactory {
    
    /**
     * 策略映射:登录类型 -> 策略实例
     */
    private final Map<LoginType, LoginStrategy> strategyMap = new ConcurrentHashMap<>();
    
    /**
     * 设备适配器映射:设备类型 -> 适配器实例
     */
    private final Map<String, DeviceAdapter> adapterMap = new ConcurrentHashMap<>();
    
    /**
     * 通过构造函数注入所有策略
     */
    @Autowired
    public LoginStrategyFactory(List<LoginStrategy> strategies, 
                                List<DeviceAdapter> adapters) {
        // 初始化策略映射
        for (LoginStrategy strategy : strategies) {
            strategyMap.put(strategy.getLoginType(), strategy);
        }
        
        // 初始化适配器映射
        for (DeviceAdapter adapter : adapters) {
            adapterMap.put(adapter.getDeviceType(), adapter);
        }
    }
    
    /**
     * 获取登录策略
     */
    public LoginStrategy getStrategy(LoginType loginType) {
        LoginStrategy strategy = strategyMap.get(loginType);
        if (strategy == null) {
            throw new BusinessException("不支持的登录方式: " + loginType);
        }
        return strategy;
    }
    
    /**
     * 获取设备适配器
     */
    public DeviceAdapter getAdapter(String deviceType) {
        DeviceAdapter adapter = adapterMap.get(deviceType);
        if (adapter == null) {
            // 默认使用Web适配器
            adapter = adapterMap.get("web");
        }
        return adapter;
    }
}

3.6 统一登录服务

/**
 * 统一登录服务
 */
@Service
public class UnifiedLoginService {
    
    @Autowired
    private LoginStrategyFactory strategyFactory;
    
    @Autowired
    private LoginLogService logService;
    
    /**
     * 统一登录入口
     */
    public LoginResult login(LoginRequest request) {
        // 1. 参数验证
        validateRequest(request);
        
        // 2. 获取登录策略
        LoginType loginType = LoginType.valueOf(request.getLoginType().toUpperCase());
        LoginStrategy strategy = strategyFactory.getStrategy(loginType);
        
        // 3. 执行登录
        LoginResult result = strategy.login(request);
        
        // 4. 设备适配处理
        DeviceAdapter adapter = strategyFactory.getAdapter(request.getDeviceType());
        adapter.adaptLogin(request, result);
        result.setToken(adapter.adaptToken(result.getToken()));
        
        // 5. 记录登录日志
        logService.logLogin(request, result);
        
        return result;
    }
    
    /**
     * 批量登录(支持多种方式同时验证)
     */
    public Map<String, LoginResult> batchLogin(List<LoginRequest> requests) {
        Map<String, LoginResult> results = new HashMap<>();
        
        for (LoginRequest request : requests) {
            try {
                LoginResult result = login(request);
                results.put(request.getLoginType(), result);
            } catch (Exception e) {
                // 记录失败结果
                LoginResult failedResult = new LoginResult();
                failedResult.setSuccess(false);
                failedResult.setMessage(e.getMessage());
                results.put(request.getLoginType(), failedResult);
            }
        }
        
        return results;
    }
    
    private void validateRequest(LoginRequest request) {
        if (request == null) {
            throw new BusinessException("登录请求不能为空");
        }
        if (StringUtils.isEmpty(request.getLoginType())) {
            throw new BusinessException("登录类型不能为空");
        }
    }
}

3.7 统一控制器

/**
 * 统一登录控制器
 */
@RestController
@RequestMapping("/api/auth")
public class LoginController {
    
    @Autowired
    private UnifiedLoginService loginService;
    
    /**
     * 统一登录接口
     */
    @PostMapping("/login")
    public Result<LoginResult> login(@RequestBody LoginRequest request) {
        try {
            LoginResult result = loginService.login(request);
            return Result.success(result);
        } catch (BusinessException e) {
            return Result.error(e.getMessage());
        }
    }
    
    /**
     * 多端同时登录(一次请求支持多种登录方式)
     */
    @PostMapping("/multi-login")
    public Result<Map<String, LoginResult>> multiLogin(@RequestBody List<LoginRequest> requests) {
        Map<String, LoginResult> results = loginService.batchLogin(requests);
        return Result.success(results);
    }
}

四、配置与扩展

4.1 应用配置

# application.yml
login:
  strategy:
    # 启用的登录策略
    enabled:
      - password
      - sms
      - wechat
      - qq
    
    # 各策略配置
    password:
      max-retry: 5
      lock-duration: 1800
    sms:
      code-length: 6
      expire-in: 300
    wechat:
      app-id: ${wechat.app-id}
      app-secret: ${wechat.app-secret}

4.2 新增登录方式示例

/**
 * 新增微信登录策略(扩展示例)
 */
@Component
public class WechatLoginStrategy extends AbstractLoginStrategy {
    
    @Value("${login.strategy.wechat.app-id}")
    private String appId;
    
    @Value("${login.strategy.wechat.app-secret}")
    private String appSecret;
    
    @Autowired
    private WechatService wechatService;
    
    @Override
    public LoginType getLoginType() {
        return LoginType.WECHAT;
    }
    
    @Override
    protected LoginResult doLogin(LoginRequest request) {
        // 1. 使用code获取openid
        String openId = wechatService.getOpenId(request.getCode());
        
        // 2. 查找或创建用户
        User user = userService.findByOpenId(openId);
        if (user == null) {
            user = userService.createUserByWechat(openId, request.getExtraParams());
        }
        
        // 3. 生成Token
        String token = generateToken(user);
        
        // 4. 返回结果
        LoginResult result = new LoginResult();
        result.setSuccess(true);
        result.setToken(token);
        result.setUserInfo(convertToUserInfo(user));
        
        return result;
    }
}

五、优势与收益

5.1 架构优势

  1. 高内聚低耦合:每种登录策略独立封装,互不影响
  2. 开闭原则:新增登录方式只需添加新策略,无需修改已有代码
  3. 统一管理:所有登录逻辑集中管理,便于监控和维护
  4. 灵活扩展:支持动态添加/禁用登录方式

5.2 业务收益

  1. 降低复杂度:登录逻辑从分散到集中,降低理解成本
  2. 提高复用:通用逻辑抽象到基类,避免代码重复
  3. 增强维护性:修改登录逻辑只需调整对应策略
  4. 提升扩展性:新设备类型或登录方式可以快速支持

六、最佳实践建议

  1. 策略粒度:按照登录方式划分策略,避免策略类过于庞大
  2. 异常处理:在策略基类中统一处理异常,保证流程一致性
  3. 配置化:将可配置参数提取到配置文件中,便于动态调整
  4. 监控告警:在关键节点添加监控和日志,便于问题排查
  5. 降级方案:主登录方式失败时提供备用方案

七、总结

通过工厂+策略模式实现多端统一登录,有效解决了登录逻辑混乱的问题。该方案具有良好的扩展性、维护性和可读性,能够适应复杂的多端登录场景。在实际应用中,可以根据具体需求进行适当调整和扩展,如添加验证码策略、第三方登录策略等。

这种设计不仅适用于登录场景,也可以推广到支付、消息推送等类似的多策略业务场景中。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐