zoukankan      html  css  js  c++  java
  • 极客时间-设计模式之美笔记(4) 面向对象-实战(二)

    如何对接口鉴权这样一个功能开发做面向对象分析?

      面向对象分析主要的分析对象是“需求”,因此,面向对象分析可以粗略地看成“需求分析”。

    1. 第一轮基础分析

      用户名和密码的方式

    2. 第二轮分析优化

      调用方将请求接口的 URL 跟 AppID、密码拼接在一起,然后进行加密,生成一个 token。

      调用方在进行接口请求的的时候,将这个 token 及 AppID,随 URL 一块传递给微服务端。微服务端接收到这些数据之后,根据 AppID 从数据库中取出对应的密码,并通过同样的 token 生成算法,生成另外一个 token。用这个新生成的 token 跟调用方传递过来的 token 对比。如果一致,则允许接口调用请求;否则,就拒绝接口调用请求。

      

    3. 第三轮分析优化

       我们可以进一步优化 token 生成算法,引入一个随机变量,让每次接口请求生成的 token 都不一样。我们可以选择时间戳作为随机变量。

      调用方在进行接口请求的时候,将 token、AppID、时间戳,随 URL 一并传递给微服务端。微服务端在收到这些数据之后,会验证当前时间戳跟传递过来的时间戳,是否在一定的时间窗口内(比如一分钟)。如果超过一分钟,则判定 token 过期,拒绝接口请求。如果没有超过一分钟,则说明 token 没有过期,就再通过同样的 token 生成算法,在服务端生成新的 token,与调用方传递过来的 token 比对,看是否一致。如果一致,则允许接口调用请求;否则,就拒绝接口调用请求。

    4. 第四轮分析优化

      针对 AppID 和密码的存储,我们最好能灵活地支持各种不同的存储方式,比如 ZooKeeper、本地配置文件、自研配置中心、MySQL、Redis 等。

    5. 最终确定需求

    •   调用方进行接口请求的时候,将 URL、AppID、密码、时间戳拼接在一起,通过加密算法生成 token,并且将 token、AppID、时间戳拼接在 URL 中,一并发送到微服务端。
    •   微服务端在接收到调用方的接口请求之后,从请求中拆解出 token、AppID、时间戳。
    •   微服务端首先检查传递过来的时间戳跟当前时间,是否在 token 失效时间窗口内。如果已经超过失效时间,那就算接口调用鉴权失败,拒绝接口调用请求。
    •   如果 token 验证没有过期失效,微服务端再从自己的存储中,取出 AppID 对应的密码,通过同样的 token 生成算法,生成另外一个 token,与调用方传递过来的 token 进行匹配;如果一致,则鉴权成功,允许接口调用,否则就拒绝接口调用。

    如何利用面向对象设计和编程开发接口鉴权功能?

     如何进行面向对象设计?

    • 划分职责进而识别出有哪些类;
    • 定义类及其属性和方法;
    • 定义类与类之间的交互关系;
    • 将类组装起来并提供执行入口。

    1. 划分职责进而识别出有哪些类

      根据需求描述,把其中涉及的功能点,一个一个罗列出来,然后再去看哪些功能点职责相近,操作同样的属性,是否应该归为同一个类。让我们拆解上文的最终需求,注意:拆解出来的每个功能点要尽可能的小。每个功能点只负责做一件很小的事情(单一职责)。

    1. 把 URL、AppID、密码、时间戳拼接为一个字符串;
    2. 对字符串通过加密算法加密生成 token;
    3. 将 token、AppID、时间戳拼接到 URL 中,形成新的 URL;
    4. 解析 URL,得到 token、AppID、时间戳等信息;
    5. 从存储中取出 AppID 和对应的密码;
    6. 根据时间戳判断 token 是否过期失效;
    7. 验证两个 token 是否匹配;

      我们发现,1、2、6、7 都是跟 token 有关,负责 token 的生成、验证;3、4 都是在处理 URL,负责 URL 的拼接、解析;5 是操作 AppID 和密码,负责从存储中读取 AppID 和密码。

      所以,我们可以粗略地得到三个核心的类:AuthToken、Url、CredentialStorage。

    • AuthToken 负责实现 1、2、6、7 这四个操作;
    • Url 负责 3、4 两个操作;
    • CredentialStorage 负责 5 这个操作。

      这个需求相对简单,针对复杂的需求开发,我们首先要做的是进行模块划分,将需求先简单划分成几个小的、独立的功能模块,然后再在模块内部,应用我们刚刚讲的方法,进行面向对象设计。而模块的划分和识别,跟类的划分和识别,是类似的套路。

    2. 定义类及其属性和方法

      AuthToken 类相关的功能点有四个:

    • 把 URL、AppID、密码、时间戳拼接为一个字符串;
    • 对字符串通过加密算法加密生成 token;
    • 根据时间戳判断 token 是否过期失效;
    • 验证两个 token 是否匹配。

      识别出需求描述中的动词,作为候选的方法,再进一步过滤筛选。类比一下方法的识别,我们可以把功能点中涉及的名词,作为候选属性,然后同样进行过滤筛选。我们可以借用这个思路,根据功能点描述,识别出来 AuthToken 类的属性和方法,如下所示:

     从上面的类图中,我们可以发现这样三个小细节。

      第一个细节:并不是所有出现的名词都被定义为类的属性,比如 URL、AppID、密码、时间戳这几个名词,我们把它作为了方法的参数。

      第二个细节:我们还需要挖掘一些没有出现在功能点描述中属性,比如 createTime,expireTimeInterval,它们用在 isExpired() 函数中,用来判定 token 是否过期。

      第三个细节:我们还给 AuthToken 类添加了一个功能点描述中没有提到的方法 getToken()。

      第一个细节告诉我们,从业务模型上来说,不应该属于这个类的属性和方法,不应该被放到这个类里。比如 URL、AppID 这些信息,从业务模型上来说,不应该属于 AuthToken,所以我们不应该放到这个类中。

      第二、第三个细节告诉我们,在设计类具有哪些属性和方法的时候,不能单纯地依赖当下的需求,还要分析这个类从业务模型上来讲,理应具有哪些属性和方法。这样可以一方面保证类定义的完整性,另一方面不仅为当下的需求还为未来的需求做些准备。

      Url 类相关的功能点有两个:

    • 将 token、AppID、时间戳拼接到 URL 中,形成新的 URL;
    • 解析 URL,得到 token、AppID、时间戳等信息。

      接口请求并不一定是以 URL 的形式来表达,还有可能是 Dubbo、RPC 等其他形式。为了让这个类更加通用,命名更加贴切,我们接下来把它命名为 ApiRequest。下面是我根据功能点描述设计的 ApiRequest 类。

    CredentialStorage 类相关的功能点有一个:

    •  从存储中取出 AppID 和对应的密码。

     3. 定义类与类之间的交互关系

      泛化、实现、组合、依赖 这四种关系掌握即可

    泛化(Generalization)可以简单理解为继承关系。具体到 Java 代码就是下面这样:

    public class A { ... }
    public class B extends A { ... }

    实现(Realization)一般是指接口和实现类之间的关系。具体到 Java 代码就是下面这样:

    public interface A {...}
    public class B implements A { ... }

    依赖(Dependency)是一种比关联关系更加弱的关系,包含关联关系。不管是 B 类对象是 A 类对象的成员变量,还是 A 类的方法使用 B 类对象作为参数或者返回值、局部变量,只要 B 类对象和 A 类对象有任何使用关系,我们都称它们有依赖关系。具体到 Java 代码就是下面这样:

    public class A {
    private B b;
    public A(B b) {
    this.b = b;
    }
    }
    或者
    public class A {
    private B b;
    public A() {
    this.b = new B();
    }
    }
    或者
    public class A {
    public void func(B b) { ... }
    }

    组合(Association)具体到代码层面,只要 B 类对象是 A 类对象的成员变量,那我们就称,A 类跟 B 类是组合关系。具体到 Java 代码就是下面这样: 

    public class A {
      private B b;
      public A(B b) {
        this.b = b;
      }
    }
    或者
    public class A {
      private B b;
      public A() {
        this.b = new B();
      }
    }

    4. 将类组装起来并提供执行入口

    接口鉴权并不是一个独立运行的系统,而是一个集成在系统上运行的组件,所以,我们封装所有的实现细节,设计了一个最顶层的 ApiAuthenticator 接口类,暴露一组给外部调用者使用的 API 接口,作为触发执行鉴权逻辑的入口。具体的类的设计如下所示:

     如何进行面向对象编程?

      面向对象设计完成之后,我们已经定义清晰了类、属性、方法、类之间的交互,并且将所有的类组装起来,提供了统一的执行入口。接下来,面向对象编程的工作,就是将这些设计思路翻译成代码实现。有了前面的类图,这部分工作相对来说就比较简单了。所以,这里我只给出比较复杂的 ApiAuthenticator 的实现。

    public interface ApiAuthenticator {
      void auth(String url);
      void auth(ApiRequest apiRequest);
    }
    
    public class DefaultApiAuthenticatorImpl implements ApiAuthenticator {
      private CredentialStorage credentialStorage;
      
      public DefaultApiAuthenticatorImpl() {
        this.credentialStorage = new MysqlCredentialStorage();
      }
      
      public DefaultApiAuthenticatorImpl(CredentialStorage credentialStorage) {
        this.credentialStorage = credentialStorage;
      }
    
      @Override
      public void auth(String url) {
        ApiRequest apiRequest = ApiRequest.buildFromUrl(url);
        auth(apiRequest);
      }
    
      @Override
      public void auth(ApiRequest apiRequest) {
        String appId = apiRequest.getAppId();
        String token = apiRequest.getToken();
        long timestamp = apiRequest.getTimestamp();
        String originalUrl = apiRequest.getOriginalUrl();
    
        AuthToken clientAuthToken = new AuthToken(token, timestamp);
        if (clientAuthToken.isExpired()) {
          throw new RuntimeException("Token is expired.");
        }
    
        String password = credentialStorage.getPasswordByAppId(appId);
        AuthToken serverAuthToken = AuthToken.generate(originalUrl, appId, password, timestamp);
        if (!serverAuthToken.match(clientAuthToken)) {
          throw new RuntimeException("Token verfication failed.");
        }
      }
    }
  • 相关阅读:
    【从零开始学Java笔记】学生管理系统
    【从零开始学Java笔记】关键字super和this
    【从零开始学Java笔记】关键字Static
    【从零开始学Java笔记】关键字final
    【从零开始学Java笔记】关键字abstract
    循环结构
    switch选择结构
    if条件语句
    位运算
    Eclipse 报错The method xxx of type must override a superclass method、Description Resource Path Location Type Java compiler level does not match the version of the installed Java project facet
  • 原文地址:https://www.cnblogs.com/xcgShare/p/14716879.html
Copyright © 2011-2022 走看看