登录系统 - 三方通道服务的框架设计演化

登录系统 - 三方通道服务的框架设计演化

前言

做系统开发的同学都懂,对接三方系统就像拆盲盒——你永远不知道对方的接口设计会给你埋多少坑。这种“坑”往往体现为三方接口升级、设计逻辑混乱,导致咱们自己的系统被迫适配,慢慢就变成了“补丁摞补丁”的形态。到最后,代码里的 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的缓存管理等,这些都需要在服务实现类中单独处理,让上层调用方完全感知不到差异。

另外,文中的实现方案也不是银弹,比如类的数量增多可能会让新手感到困惑,但通过合理的包结构划分(按平台或功能分类)可以缓解这个问题。

以上都是我在实际开发中踩坑总结的经验,难免有考虑不周的地方。如果大家有更好的设计思路、优化方案,欢迎随时交流探讨,一起把系统设计得更优雅~

posted @ 2026-01-20 09:38  裸奔的青春  阅读(1)  评论(0)    收藏  举报