真香定律!我用这种模式重构了第三方登录

分享是最有效的学习方式。
~博客:blog.ktdaddy.com/~

~老猫的设计模式专栏已经偷偷发车了。不甘愿做crud boy?看了好几遍的设计模式还记不住?那就不要刻意记了,跟上老猫的步伐,在一个个有趣的职场故事中领悟设计模式的精髓吧。还等什么?赶紧上车吧。~

故事

办公室里,小猫托着腮帮对着电脑陷入了思考。就在刚刚,他接到了领导指派的一个任务,业务调整,登录方式要进行拓展。例如需要接入第三方的微信登录,企业微信授权登录等等。

原因大概是这样,现在大环境不好,原来面向B端企业员工的电商业务并不好做,新客拓展比较困难,业务想要有更好的起色着实比较困难,所以决策层决定要把登录的口子放开,原来支持手机密码登录以及手机验证码进行登录,现在为了更好地推广,需要支持微信扫码关注企业公众号后登录,企业微信,微博等等一些列的第三方登录模式。

说白了未来到底会有多少种登录方式不得而知,那么面对这样一个棘手的问题,小猫又该何去何从?

概述

登录问题相信后端小伙伴都有接触过,最简单的可能就是做一个权限系统就会用到登录名+密码+验证码进行登录,继而稍微复杂一些可能会涉及手机验证码登录。现在随着第三方平台的层出不穷,我们很多网站其实都提供了联合登录。用户掏出手机简单地一个扫码动作即可完成初步的注册登录功能。这种方式一定程度上能够给当前的网站带来更多的流量。

关于小猫遇到的问题,咱们尝试从下面几个点去解决。

登录演化

聊到登录,我们首先去了解一下整个登录认证的发展阶段,以及目前比较常见也相对比较复杂的微信公众号授权登录流程。

基于Cookie/Session进行验证登录

在早期,也就是可能是单体系统的时代,亦或者站在java开发者角度来说是jsp时代的时候,我们用的登录方式就是Cookie/Session验证的方式。关于Cookie以及Session相信很多后端的小伙伴都应该知道,当然若真有不清楚的,大家可以自己查阅一下相关资料。 利用这种方式登录的流程其实还是比较简单的,如下流程:

基于上述登录成功后,服务端将用户的身份信息存储在Session里,并将session ID通过cookie传递给客户端。后续的数据请求都会带上cookie,服 务端根据cookie中携带的session id来得到辨别用户身份。

简单的java伪代码如下:

java 复制代码
...
session.setAtrrbuite("user",user);
...
session.getAttrbuite("user");

当然上述的伪代码还是基于最最原始的写法去写的,关于这种登录的框架,其实目前市面上也有比较成熟的,例如轻量级的shiro,spring本身自带的权限认证框架也有。

随着业务的发展,系统访问量级的增大,我们渐渐发现这种方式存在着一些问题:

  1. 由于服务端需要对接大量的客户端,也就需要存放大量的Seesion ID,这样就会导致服务器压力过大。如果服务器是个集群,为了同步登录的状态,需要将Session ID同步到每一台服务器上,无形中增加了服务器端的维护成本。
  2. 由于Session ID存放在Cookie中,所以无法避免CSRF攻击(跨站请求伪造)。

当然其他问题也欢迎小伙伴们进行补充。 为了解决这一些列的问题,我们渐渐演化出了另外一种登录认证方式------------基于token进行认证登录。

基于TOKEN进行认证登录

现在的系统大部分都是前后端分离开发的。后端大多使用了WEB API,此时token无疑是处理认证的最好方式。

Session 方案中用户信息(以Session记录形式)存储在服务端。而Token方案中(以Token形式)存储在客户端,服务端仅验证Token合法性即可。基于Token的身份验证是无状态的,不将用户信息存在服务器中。这种概念解决了在服务端存储信息时的许多问题。NoSession意味着咱们的程序可以根据需要去增减机器,而不用去担心用户是否登录。

咱们一起来看一下如果使用TOKEN整个流程。

关于上述token机制的特点有以下几点:

  • 无状态、可扩展:在客户端存储的Token是无状态的,并且能够被扩展。基于这种无状态的和不存储Session信息,所以不会对服务器端造成压力,负载均衡器能够将用户信息从一个服务器传到其他服务器上,即使是服务器集群,也不需要增加维护成本。

  • 可扩展性:Tokens能够创建与其它程序共享权限的程序。(即,我们所说的第三方平台联合登录的时候,token的生成机制以及验证可以由第三方系统进行联合验证登录)

  • 安全性:请求中发送Token而不是发送Cookie,能够防止CSRF(跨站请求伪造)。即客户端使用Cookie存储了Tooken,Cookie也仅仅是一个存储机制而不是用于认证。不将信息存储在Session中,让我们少了对Session的操作。Token也可以存放在前端任何地方,可以不用保存在Cookie中,提升了页面的安全性。Token是会失效的,一段时间之后用户需要重新验证。

  • 多平台跨域:对应用程序和服务进行扩展的时候,需要介入各种各种的设备和应用程序。只要用户有一个通过了验证的token,数据和资源就能够在任何域上被请求到。

微信扫码跳转公众号认证登录

这也是后续小猫遇到的问题,以及需要和其他第三方Api主要对接的。其实关于扫码认证登录也是基于token机制的一种拓展。只不过第三方的平台在token机制上新增了获取二维码进行二次确认的过程。咱们以微信扫码跳转公众号登录为例来看一下整个流程。其他的第三方登录流程其实也是大同小异,咱们了解一个流程即可,不同的平台只是对接不同的api而已。流程图如下:

从上面这幅图看到,扫码登录其实复杂就复杂在获取token这个步骤上,当获取完毕token之后,其后续的业务逻辑其实基本也是一样的。

其实其他第三方的登录其实也是大同小异,最主要的难点是在如何获取token上,我们只要认真看完对接的api,其实问题也基本都能迎刃而解。

说明一下,老猫这里绘图用了drawio工具,如果想要知道老猫的绘图思路,大家可以看看这里《绘图思路

如何兼容多套?

看完上述之后,相信大家会对认证登录心里有杆秤了。细节方面其实只要去查询相关平台的api,然后去撸代码就好了。但是实现一套倒是还好,但是现在小猫遇到的问题是需要在原逻辑上去丰富登录的代码。如果在老的代码上通过if else的方式去实现多套登录逻辑,那估计后面又是屎山。

这里,其实我们可以引入"适配器设计模式"去解决这样的问题。

什么是适配器模式?

适配器模式(英文名:Adapter Pattern)是指将一个类的接口转换成用户期望的另一个接口,使得原本接口不兼容的类可以一起工作。

适配器模式可以分为两类:对象适配器模式和类适配器模式。对象适配器模式通过组合实现适配,而类适配器模式则通过继承实现适配。

此外,还有一种特殊的适配器模式------缺省适配器模式它由一个抽象类实现,并在其中实现目标接口中所规定的所有方法,但这些方法的实现通常是空方法,由具体的子类来实现具体的功能。适配器模式的应用可以提高代码的复用性和可维护性,同时帮助解决不同接口之间的兼容性问题。

上面的概念比较抽象,其实在咱们的日常生活中也有这样的例子,例如手机充电转换头,显示器转接头等等。

适配器模式重构第三方登录

话不多说,直接开干,我们就针对小猫的遇到这个第三方登录的场景,咱们用代码重构一把。(当然,这里我们侧重的还是伪代码)。跟着老猫,咱们一步步走好代码的演化。

咱们先看一下老的业务代码,如下:

java 复制代码
public class UserLoginService {
    public ApiResponse<String> regist(String userName,String password) {
        //...dosomething
        return ApiResponse.success("success");
    }
    public ApiResponse login(String userName, String password) {
        return null;
    }
}

接下来由于小猫的业务会发生变更,新的登录方式会层出不穷,所以,我们得遵循之前提到的软件设计原则去更好地写一下业务代码。我们遵循之前提到的开闭原则,于是我们迈出了重构代码的第一步,我们将创建一个新的第三方登录的类来专门处理第三方的登录对接。如下:

java 复制代码
public class ThirdPartyUserLoginService extends UserLoginService {

    public ApiResponse loginForQQ(String openId) {
        /**
         * openid 全局唯一,咱们直接作为用户名
         * 默认密码QQ_EMPTY
         * 注册(原来父类中有注册实现)
         * 调用原来的登录
         */
        return loginForRegist(openId, null);
    }

    public ApiResponse loginForWechat(String openId) {
        return null;
    }

    public ApiResponse loginForToken(String token) {
        return null;
    }

    public ApiResponse loginForTel(String tel, String code) {
        return null;
    }

    public ApiResponse<String> loginForRegist(String userName, String password) {
        super.login(userName, password);
        return super.login(userName, password);
    }
}

写到这里,其实咱们已经集成了多种登录方式的代码兼容,但是这种实现方式显然是不太优雅的,看起来比较死板,在登录的时候我们甚至还得去判断客户到底是用什么去做登录的,然后去分别调用不同第三方平台的认证方式。

我们接下来演化开始用适配器。如下代码: 首先我们定义出一个标准的适配接口:

java 复制代码
public interface LoginAdapter {
    boolean support(Object adapter);
    ApiResponse login(String id,Object adapter);
}

根据上面我们看到,我们有QQ方式登录,有微信方式登录,有电话验证码方式登录。所以我们对应的就应该有相关的这些方式的适配器的实现。由于代码重复,所以在此老猫就写QQ和微信这两种伪代码,其他的暂时先偷个懒。

java 复制代码
/**
 * @author 公众号:程序员老猫
 * @date 2024/3/3 22:47
 */
public class LoginForQQAdapter implements LoginAdapter {
    @Override
    public boolean support(Object adapter) {
        return adapter instanceof LoginForQQAdapter;
    }

    @Override
    public ApiResponse login(String id, Object adapter) {
        return null;
    }
}

public class LoginForWeChatAdapter implements LoginAdapter {
    @Override
    public boolean support(Object adapter) {
        return adapter instanceof LoginForWeChatAdapter;
    }

    @Override
    public ApiResponse login(String id, Object adapter) {
        return null;
    }
}

有了这些适配器之后,我们就统一对外给出去接口:

java 复制代码
public interface IPassportForThird {
    ApiResponse loginForQQ(String openId);

    ApiResponse loginForWechat(String openId);

    ApiResponse<String> loginForRegist(String userName, String password);
}

最后创建统一适配器。

java 复制代码
@Slf4j
public class PassportForThirdAdapter extends UserLoginService implements IPassportForThird{
    @Override
    public ApiResponse loginForQQ(String openId) {
        return doLogin(openId,LoginForQQAdapter.class);
    }

    @Override
    public ApiResponse loginForWechat(String openId) {
        return doLogin(openId,LoginForWeChatAdapter.class);
    }

  
    @Override
    public ApiResponse<String> loginForRegist(String userName, String password) {
        super.login(userName, password);
        return super.login(userName, password);
    }
    
    //用到简单工厂模式以及策略模式
    private ApiResponse doLogin(String openId,Class<? extends LoginAdapter> clazz) {
        try {
            LoginAdapter adapter = clazz.newInstance();
            if(adapter.support(adapter)){
                return adapter.login(openId,adapter);
            }
        }catch (Exception e) {
            log.error("exception is",e);
        }
        return null;
    }
}

最终我们看一下实现的类图:

上述我们就用了适配器的模式简单重构了现有的第三方登录的代码,当然上述可能还存在一些代码的缺陷,大家也不要太过较真,在此给大家在日常开发中多点思路。

大家可能会对每个适配器的support()方法有点疑问,用来决断兼容。这里support()方法的参数也是Object类型的,而support()方法来自接口。适配器的实现并不依赖接口,其实我们也可以直接将LoginAdapter移除。

在上述重构的例子中,其实咱们不仅仅用到了适配器模式,其实还用到了简单工厂模式的特性。

总结

其实在我们日常的开发中,适配器模式是比较常用的一种设计模式,不仅仅使用上述场景,其实在很多其他api的对接的场景也有适用。例如,在电商业务场景中会涉及到各种对接,说到买卖就会牵扯到供应商的对接,第三方分销渠道客户的对接,其中必然涉及模型不一致需要适配转换的场景,比如供应商商品信息和标准商城商品信息等等。当然老猫在此也只是做了一下简单罗列。希望大家在后面的工作中可以参考用到。

相关推荐
一只叫煤球的猫5 小时前
写代码很6,面试秒变菜鸟?不卖课,面试官视角走心探讨
前端·后端·面试
bobz9655 小时前
tcp/ip 中的多路复用
后端
bobz9655 小时前
tls ingress 简单记录
后端
皮皮林5516 小时前
IDEA 源码阅读利器,你居然还不会?
java·intellij idea
你的人类朋友6 小时前
什么是OpenSSL
后端·安全·程序员
bobz9657 小时前
mcp 直接操作浏览器
后端
前端小张同学9 小时前
服务器部署 gitlab 占用空间太大怎么办,优化思路。
后端
databook9 小时前
Manim实现闪光轨迹特效
后端·python·动效
武子康10 小时前
大数据-98 Spark 从 DStream 到 Structured Streaming:Spark 实时计算的演进
大数据·后端·spark
该用户已不存在10 小时前
6个值得收藏的.NET ORM 框架
前端·后端·.net