登录系统 - 三方通道服务的框架设计演化
登录系统 - 三方通道服务的框架设计演化
前言
做系统开发的同学都懂,对接三方系统就像拆盲盒——你永远不知道对方的接口设计会给你埋多少坑。这种“坑”往往体现为三方接口升级、设计逻辑混乱,导致咱们自己的系统被迫适配,慢慢就变成了“补丁摞补丁”的形态。到最后,代码里的 If-else 能绕地球半圈,简单的登录逻辑藏在层层判断里,维护时找个方法都得像侦探破案。
所以,三方登录通道服务的高扩展性设计,绝对是系统架构的重中之重。为了把这个演化过程说清楚,我打算从初版开始,一步步拆解优化,最后给出一个经得起业务考验的设计(当然不完美,欢迎大家拍砖)。下面就用讲故事的方式,带大家看看小希是怎么踩坑、填坑的(纯属虚构,请勿对号入座)。
v1 简单工厂 + 策略模式
周三早上刚到公司,小希就被领导老王堵在了工位门口。“小希啊,最近公司要做新用户体系,需要对接微信、QQ、微博这三个三方登录通道,这个活儿交给你,没问题吧?” 老王手里端着泡满枸杞的保温杯,眼神里满是“信任”。
小希刚啃了一口肉包,含糊不清地应道:“没问题!” 心里却在打鼓:“三方登录我只用过,没开发过啊……不过男人不能说不行,装X一时爽,硬搞火葬场也得上!”
接下来的两天,小希泡在三个平台的开放平台文档里,头发都掉了两根。好在他平时爱琢磨设计模式,琢磨着“不就是多通道统一入口吗?简单工厂加策略模式搞定!” 很快,设计草稿就新鲜出炉了。
核心思路很简单:定义一个统一的登录接口,每个三方平台写个实现类,再用工厂方法根据平台编号获取对应的服务实例。代码大概长这样:
// 统一登录接口
public interface IThirdLoginService {
// 发起三方登录
LoginResultDTO doLogin(LoginRequestDTO requestDTO);
// 验证登录回调
CallbackResultDTO verifyCallback(CallbackRequestDTO requestDTO);
}
// 微信登录实现
public class WechatLoginServiceImpl implements IThirdLoginService {
@Override
public LoginResultDTO doLogin(LoginRequestDTO requestDTO) {
// 调用微信开放平台接口,省略实现...
return new LoginResultDTO();
}
@Override
public CallbackResultDTO verifyCallback(CallbackRequestDTO requestDTO) {
// 验证微信回调参数,省略实现...
return new CallbackResultDTO();
}
}
// QQ登录实现(微博类似,省略)
public class QqLoginServiceImpl implements IThirdLoginService {
// 省略实现...
}
// 登录服务工厂
public class LoginServiceFactory {
public static IThirdLoginService getLoginService(PlatformEnum platform) {
switch (platform) {
case WECHAT:
return new WechatLoginServiceImpl();
case QQ:
return new QqLoginServiceImpl();
case WEIBO:
return new WeiboLoginServiceImpl();
default:
throw new IllegalArgumentException("不支持的登录平台");
}
}
}
// 统一入口服务
@Service
public class LoginCenterServiceImpl {
public LoginResultDTO login(LoginRequestDTO requestDTO) {
log.info("发起三方登录,平台:{}", requestDTO.getPlatform());
IThirdLoginService loginService = LoginServiceFactory.getLoginService(requestDTO.getPlatform());
return loginService.doLogin(requestDTO);
}
// 回调验证方法省略...
}
旁边工位的大哥路过,扫了眼屏幕,忍不住夸道:“可以啊小希,这设计够规范!把所有登录通道都抽象成接口,工厂负责实例化,后续加新平台只需要加实现类和工厂分支,符合开闭原则啊。” 小希表面云淡风轻:“常规操作~” 心里早就乐开了花:“总算没白啃设计模式的书!”
大哥又指着 LoginCenterServiceImpl 问:“这个统一入口除了打日志,还能干嘛?” 小希嘿嘿一笑:“这是预留的扩展点啊,以后加统一鉴权、限流、日志脱敏,都能在这里搞,懂?” 大哥点点头,端着枸杞茶回去了,心里默念:“现在的年轻人,比我当年会设计多了。”
v2 适配器模式
没想到公司用户增长这么快,半年后,产品经理拿着需求文档找到小希:“小希,咱们得加登录方式了!手机号验证码登录、Apple登录、Google登录(海外版要用),还有抖音登录也得安排上。”
小希看着原来的 IThirdLoginService 接口,头都大了。原来的接口只有两个方法,现在要加这么多登录方式,接口很快就膨胀了:
public interface IThirdLoginService {
// 原有方法
LoginResultDTO doLogin(LoginRequestDTO requestDTO);
CallbackResultDTO verifyCallback(CallbackRequestDTO requestDTO);
// 新增方法
LoginResultDTO mobileCodeLogin(MobileLoginRequestDTO requestDTO);
LoginResultDTO appleLogin(AppleLoginRequestDTO requestDTO);
LoginResultDTO googleLogin(GoogleLoginRequestDTO requestDTO);
LoginResultDTO douyinLogin(DouyinLoginRequestDTO requestDTO);
}
更坑的是,微信、QQ这些老平台压根没有手机号登录、Apple登录这些功能,但因为实现了接口,必须强制实现这些方法。小希只能无奈地写抛异常的代码:
public class WechatLoginServiceImpl implements IThirdLoginService {
// 原有方法实现省略...
@Override
public LoginResultDTO mobileCodeLogin(MobileLoginRequestDTO requestDTO) {
throw new LoginServiceException(ResultCode.FUNCTION_NOT_SUPPORTED);
}
@Override
public LoginResultDTO appleLogin(AppleLoginRequestDTO requestDTO) {
throw new LoginServiceException(ResultCode.FUNCTION_NOT_SUPPORTED);
}
// 其他不支持的方法全是抛异常...
}
“这也太反人类了!” 小希吐槽道。突然他灵光一闪:适配器模式啊!或者用JDK8的default方法也行。干脆写个适配器类,实现所有方法并默认抛异常,让具体的通道实现类继承适配器,只重写自己支持的方法:
// 登录服务适配器
public class LoginServiceAdapter implements IThirdLoginService {
@Override
public LoginResultDTO doLogin(LoginRequestDTO requestDTO) {
throw new LoginServiceException(ResultCode.FUNCTION_NOT_SUPPORTED);
}
@Override
public CallbackResultDTO verifyCallback(CallbackRequestDTO requestDTO) {
throw new LoginServiceException(ResultCode.FUNCTION_NOT_SUPPORTED);
}
@Override
public LoginResultDTO mobileCodeLogin(MobileLoginRequestDTO requestDTO) {
throw new LoginServiceException(ResultCode.FUNCTION_NOT_SUPPORTED);
}
// 其他方法全默认抛异常...
}
// 微信登录实现类优化后
public class WechatLoginServiceImpl extends LoginServiceAdapter {
@Override
public LoginResultDTO doLogin(LoginRequestDTO requestDTO) {
// 原有实现...
return new LoginResultDTO();
}
@Override
public CallbackResultDTO verifyCallback(CallbackRequestDTO requestDTO) {
// 原有实现...
return new CallbackResultDTO();
}
}
这么一改,确实解决了子类强制实现无关方法的问题,但小希心里清楚:这只是“治标不治本”,接口臃肿的核心问题还没解决,以后再加新登录方式,还是得改接口、改适配器,扩展性太差。
v3 服务插件 + 服务收口 + 泛型设计
吃一堑长一智,小希明白,想要彻底解决问题,必须打破“一个接口包打天下”的思路——拆!但怎么拆才合理呢?
他琢磨了两种方案:第一种是按平台分组,比如微信组包含微信扫码登录、微信小程序登录,QQ组包含QQ快捷登录、QQ空间登录,但这样一来,一个类里会有多个相关方法,改一个功能可能影响其他功能;第二种是“一个功能一个类”,每个登录方式单独写实现类,用泛型定义顶层接口,通过工厂方法精准定位。
权衡之下,小希选了第二种方案——虽然类的数量会增加,但胜在解耦彻底,排查问题时能精准定位,维护成本反而降低了。
第一步:顶层泛型接口设计
先定义一个抽象的通道服务接口,用泛型限定入参和出参,所有登录相关的实现类都要实现这个接口:
/**
* 抽象三方通道服务接口
* @param <T> 入参模型(继承自统一请求父类)
* @param <R> 出参模型(继承自统一响应父类)
*/
public interface IThirdChannelService<T extends AbstractReqModel, R extends AbstractRspModel> {
R invoke(T request) throws LoginServiceException;
}
// 统一请求父类(存放公共字段:平台编号、服务ID等)
public abstract class AbstractReqModel {
private PlatformEnum platform;
private ServiceIdEnum serviceId;
// getter/setter省略...
}
// 统一响应父类
public abstract class AbstractRspModel {
private boolean success;
private String code;
private String msg;
// getter/setter省略...
}
第二步:具体服务实现类
每个登录方式单独写一个实现类,入参和出参根据实际需求定义,互不干扰:
// 微信扫码登录
public class WechatScanLogin implements IThirdChannelService<WechatScanLoginReqDTO, WechatScanLoginRspDTO> {
@Override
public WechatScanLoginRspDTO invoke(WechatScanLoginReqDTO request) throws LoginServiceException {
// 调用微信扫码登录接口,省略实现...
return new WechatScanLoginRspDTO();
}
}
// 手机号验证码登录
public class MobileCodeLogin implements IThirdChannelService<MobileCodeLoginReqDTO, MobileCodeLoginRspDTO> {
@Override
public MobileCodeLoginRspDTO invoke(MobileCodeLoginReqDTO request) throws LoginServiceException {
// 调用短信平台接口,省略实现...
return new MobileCodeLoginRspDTO();
}
}
// Apple登录
public class AppleLogin implements IThirdChannelService<AppleLoginReqDTO, AppleLoginRspDTO> {
@Override
public AppleLoginRspDTO invoke(AppleLoginReqDTO request) throws LoginServiceException {
// 调用Apple登录验证接口,省略实现...
return new AppleLoginRspDTO();
}
}
// 登录结果查询(比如查询登录状态)
public class LoginQueryService implements IThirdChannelService<LoginQueryReqDTO, LoginQueryRspDTO> {
@Override
public LoginQueryRspDTO invoke(LoginQueryReqDTO request) throws LoginServiceException {
// 调用三方平台查询接口,省略实现...
return new LoginQueryRspDTO();
}
}
第三步:服务ID枚举定义
为了让工厂能精准找到对应的服务实现类,新增一个服务ID枚举,每个枚举值对应一个具体的登录功能:
public enum ServiceIdEnum {
// 登录相关
WECHAT_SCAN_LOGIN("wechat_scan_login", "微信扫码登录"),
MOBILE_CODE_LOGIN("mobile_code_login", "手机号验证码登录"),
APPLE_LOGIN("apple_login", "Apple登录"),
GOOGLE_LOGIN("google_login", "Google登录"),
// 查询相关
LOGIN_QUERY("login_query", "登录状态查询"),
LOGIN_CALLBACK_VERIFY("login_callback_verify", "登录回调验证"),
// 后续可扩展其他服务...
;
private String code;
private String desc;
// 构造方法、getter省略...
}
第四步:工厂方法与服务调度
工厂方法需要同时接收“平台编号”和“服务ID”两个参数,才能定位到唯一的服务实现类。这里用Guava的Table数据结构来存储三者的映射关系(Table类似Excel表格,支持行、列双维度定位):
// 通道服务工厂接口
public interface IThirdChannelServiceFactory {
/**
* 通过平台和服务ID获取通道服务
* @param platform 平台枚举
* @param serviceId 服务ID枚举
* @return 通道服务实现类(未找到返回null)
*/
IThirdChannelService getChannelService(PlatformEnum platform, ServiceIdEnum serviceId);
}
// 工厂实现类(结合Spring容器)
@Service
public class ThirdChannelServiceFactoryImpl implements IThirdChannelServiceFactory, InitializingBean {
@Autowired
private ApplicationContext applicationContext;
// Guava Table:行=平台,列=服务ID,值=通道服务实现类
private Table<PlatformEnum, ServiceIdEnum, IThirdChannelService> serviceTable =
Tables.newCustomTable(new ConcurrentHashMap<>(), ConcurrentHashMap::new);
@Override
public IThirdChannelService getChannelService(PlatformEnum platform, ServiceIdEnum serviceId) {
return serviceTable.get(platform, serviceId);
}
// Spring初始化完成后,注册服务映射关系
@Override
public void afterPropertiesSet() throws Exception {
BeanDefinitionRegistry beanRegistry = (BeanDefinitionRegistry) applicationContext;
// 这里实际项目中可通过XML配置或自定义注解扫描服务实现类
// 简化示例:手动注册(实际可通过配置文件批量解析)
registerService(PlatformEnum.WECHAT, ServiceIdEnum.WECHAT_SCAN_LOGIN, WechatScanLogin.class);
registerService(PlatformEnum.COMMON, ServiceIdEnum.MOBILE_CODE_LOGIN, MobileCodeLogin.class);
registerService(PlatformEnum.IOS, ServiceIdEnum.APPLE_LOGIN, AppleLogin.class);
// 其他服务注册...
}
// 注册服务到Spring容器和Table中
private <T extends IThirdChannelService> void registerService(PlatformEnum platform, ServiceIdEnum serviceId, Class<T> serviceClass) {
String beanName = platform.name() + "_" + serviceId.name();
// 注册Bean到Spring容器
BeanDefinitionBuilder beanBuilder = BeanDefinitionBuilder.genericBeanDefinition(serviceClass);
((BeanDefinitionRegistry) applicationContext).registerBeanDefinition(beanName, beanBuilder.getBeanDefinition());
// 获取Bean实例,存入Table
T service = applicationContext.getBean(beanName, serviceClass);
serviceTable.put(platform, serviceId, service);
}
}
然后写一个服务调度类,作为统一入口,处理请求分发、生命周期监听(预留QOS统计、日志打印等扩展点):
@Component
@Slf4j
public class ServiceDispatcher {
@Autowired
private IThirdChannelServiceFactory channelServiceFactory;
// 生命周期监听器(预留扩展:前置处理、异常处理、后置处理)
private ChannelLifeCycleListener lifeCycleListener = new ChannelLifeCycleListener.Adapter();
public <R extends AbstractRspModel> R dispatch(AbstractReqModel reqModel) throws LoginServiceException {
PlatformEnum platform = reqModel.getPlatform();
ServiceIdEnum serviceId = reqModel.getServiceId();
// 获取通道服务
IThirdChannelService channelService = channelServiceFactory.getChannelService(platform, serviceId);
if (channelService == null) {
log.error("通道服务未找到!platform={}, serviceId={}", platform.getName(), serviceId.getDesc());
throw new LoginServiceException(ResultCode.SERVICE_NOT_FOUND, "不支持的登录方式");
}
log.info("获取通道服务成功,service={}", channelService.getClass().getSimpleName());
// 生命周期前置处理
lifeCycleListener.beforeInvoke(reqModel);
StopWatch stopWatch = StopWatch.createStarted();
R rspModel = null;
try {
// 调用服务
rspModel = (R) channelService.invoke(reqModel);
} catch (LoginServiceException e) {
lifeCycleListener.onException(reqModel, e);
throw e;
} finally {
stopWatch.stop();
}
// 生命周期后置处理
lifeCycleListener.afterInvoke(reqModel, rspModel, stopWatch.getTime(TimeUnit.MILLISECONDS));
log.info("通道服务调用完成,耗时{}ms,req={}, rsp={}",
stopWatch.getTime(TimeUnit.MILLISECONDS),
JSON.toJSONString(reqModel),
JSON.toJSONString(rspModel));
return rspModel;
}
// 生命周期监听器适配器(默认空实现,方便扩展)
public static abstract class ChannelLifeCycleListener {
public void beforeInvoke(AbstractReqModel reqModel) {}
public void afterInvoke(AbstractReqModel reqModel, AbstractRspModel rspModel, long cost) {}
public void onException(AbstractReqModel reqModel, Exception e) {}
// 空实现适配器
public static class Adapter extends ChannelLifeCycleListener {}
}
}
第五步:配置优化(可维护性升级)
为了让新来的同事能快速找到对应的服务实现类,小希把服务注册的配置抽到了XML文件中(channel-login.xml),按平台分类管理:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<!-- 微信平台服务配置 -->
<bean class="com.example.login.service.impl.WechatScanLogin" id="WECHAT_WECHAT_SCAN_LOGIN"/>
<bean class="com.example.login.service.impl.WechatMiniLogin" id="WECHAT_WECHAT_MINI_LOGIN"/>
<!-- 通用平台服务配置(手机号登录等) -->
<bean class="com.example.login.service.impl.MobileCodeLogin" id="COMMON_MOBILE_CODE_LOGIN"/>
<bean class="com.example.login.service.impl.LoginQueryService" id="COMMON_LOGIN_QUERY"/>
<!-- iOS平台服务配置 -->
<bean class="com.example.login.service.impl.AppleLogin" id="IOS_APPLE_LOGIN"/>
</beans>
然后修改工厂类的afterPropertiesSet方法,通过解析XML文件自动注册服务,不用手动写注册逻辑,可维护性直接拉满:
// 工厂实现类修改后
@Override
public void afterPropertiesSet() throws Exception {
// 解析XML配置,获取所有通道服务Bean
Map<String, IThirdChannelService> serviceMap = applicationContext.getBeansOfType(IThirdChannelService.class);
for (Map.Entry<String, IThirdChannelService> entry : serviceMap.entrySet()) {
String beanId = entry.getKey();
IThirdChannelService service = entry.getValue();
// BeanId格式:PLATFORM_SERVICEID(如WECHAT_WECHAT_SCAN_LOGIN)
String[] parts = beanId.split("_");
if (parts.length != 2) {
log.warn("BeanId格式错误,跳过注册:{}", beanId);
continue;
}
PlatformEnum platform = PlatformEnum.valueOf(parts[0]);
ServiceIdEnum serviceId = ServiceIdEnum.valueOf(parts[1]);
serviceTable.put(platform, serviceId, service);
}
log.info("通道服务注册完成,共注册{}个服务", serviceTable.size());
}
至此,整个框架设计就完成了。小希看着整洁的代码结构,忍不住给自己买了瓶肥宅快乐水——以后再加新的登录通道,只需要新增实现类、配置XML、扩展枚举,不用改任何核心逻辑,真正做到了“开闭原则”。
后语
本文分享的三方登录通道服务设计,主要解决了扩展性和可维护性问题,但实际项目中还有很多细节需要考虑:比如三方接口的版本兼容(如微信登录V1、V2接口差异)、请求参数的加密解密、access_token的缓存管理等,这些都需要在服务实现类中单独处理,让上层调用方完全感知不到差异。
另外,文中的实现方案也不是银弹,比如类的数量增多可能会让新手感到困惑,但通过合理的包结构划分(按平台或功能分类)可以缓解这个问题。
以上都是我在实际开发中踩坑总结的经验,难免有考虑不周的地方。如果大家有更好的设计思路、优化方案,欢迎随时交流探讨,一起把系统设计得更优雅~

浙公网安备 33010602011771号