写点什么

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

作者:EquatorCoco
  • 2024-03-05
    福建
  • 本文字数:5055 字

    阅读完需:约 17 分钟

故事


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

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

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


概述


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


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



登录演化


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


基于 Cookie/Session 进行验证登录


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



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


简单的 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)是指将一个类的接口转换成用户期望的另一个接口,使得原本接口不兼容的类可以一起工作。


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


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

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


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


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


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


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;    }}
复制代码


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


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);    }}
复制代码


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

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


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


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


/** * @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;    }}
复制代码


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


 public interface IPassportForThird {    ApiResponse loginForQQ(String openId);     ApiResponse loginForWechat(String openId);     ApiResponse<String> loginForRegist(String userName, String password);}
复制代码


最后创建统一适配器。


@Slf4jpublic 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 的对接的场景也有适用。例如,在电商业务场景中会涉及到各种对接,说到买卖就会牵扯到供应商的对接,第三方分销渠道客户的对接,其中必然涉及模型不一致需要适配转换的场景,比如供应商商品信息和标准商城商品信息等等。当然老猫在此也只是做了一下简单罗列。希望大家在后面的工作中可以参考用到。


文章转载自:程序员老猫

原文链接:https://www.cnblogs.com/kdaddy/p/18052885

体验地址:http://www.jnpfsoft.com/?from=001

用户头像

EquatorCoco

关注

还未添加个人签名 2023-06-19 加入

还未添加个人简介

评论

发布
暂无评论
真香定律!我用这种模式重构了第三方登录_Python_EquatorCoco_InfoQ写作社区