基于 SpringBoot 工厂+策略模式统一多端登录
·
基于 SpringBoot 工厂+策略模式统一多端登录
一、问题背景
在多端登录场景中,通常存在以下痛点:
- 登录逻辑分散:不同端(Web、APP、小程序)的登录逻辑散落在不同地方
- 代码重复:相似的登录逻辑在多处重复实现
- 扩展困难:新增登录方式或修改逻辑需要改动多处代码
- 维护复杂:登录策略变化时需要追踪所有相关代码
二、解决方案设计
2.1 架构概览
┌─────────────────────────────────────────┐
│ LoginController │
│ (统一入口,接收登录请求) │
└───────────────────┬─────────────────────┘
│
┌───────────────────▼─────────────────────┐
│ LoginStrategyFactory │
│ (工厂模式,根据登录类型创建策略) │
└───────────────────┬─────────────────────┘
│
┌──────────┴──────────┐
▼ ▼
┌─────────────────┐ ┌─────────────────┐
│ PasswordStrategy│ │ SmsStrategy │
│ (密码登录策略) │ │ (短信登录策略) │
└─────────────────┘ └─────────────────┘
│ │
▼ ▼
┌─────────────────┐ ┌─────────────────┐
│ WebLogin │ │ AppLogin │
│ (Web端登录实现) │ │ (APP端登录实现) │
└─────────────────┘ └─────────────────┘
2.2 核心设计模式
- 策略模式:定义一系列登录算法,封装每个算法
- 工厂模式:根据登录类型创建对应的策略实例
- 模板方法模式:定义登录的标准流程
三、代码实现
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 架构优势
- 高内聚低耦合:每种登录策略独立封装,互不影响
- 开闭原则:新增登录方式只需添加新策略,无需修改已有代码
- 统一管理:所有登录逻辑集中管理,便于监控和维护
- 灵活扩展:支持动态添加/禁用登录方式
5.2 业务收益
- 降低复杂度:登录逻辑从分散到集中,降低理解成本
- 提高复用:通用逻辑抽象到基类,避免代码重复
- 增强维护性:修改登录逻辑只需调整对应策略
- 提升扩展性:新设备类型或登录方式可以快速支持
六、最佳实践建议
- 策略粒度:按照登录方式划分策略,避免策略类过于庞大
- 异常处理:在策略基类中统一处理异常,保证流程一致性
- 配置化:将可配置参数提取到配置文件中,便于动态调整
- 监控告警:在关键节点添加监控和日志,便于问题排查
- 降级方案:主登录方式失败时提供备用方案
七、总结
通过工厂+策略模式实现多端统一登录,有效解决了登录逻辑混乱的问题。该方案具有良好的扩展性、维护性和可读性,能够适应复杂的多端登录场景。在实际应用中,可以根据具体需求进行适当调整和扩展,如添加验证码策略、第三方登录策略等。
这种设计不仅适用于登录场景,也可以推广到支付、消息推送等类似的多策略业务场景中。
更多推荐



所有评论(0)