面向对象分析主要的分析对象是“需求”,因此,面向对象分析可以粗略地看成“需求分析”。
用户名和密码的方式
调用方将请求接口的 URL 跟 AppID、密码拼接在一起,然后进行加密,生成一个 token。
调用方在进行接口请求的的时候,将这个 token 及 AppID,随 URL 一块传递给微服务端。微服务端接收到这些数据之后,根据 AppID 从数据库中取出对应的密码,并通过同样的 token 生成算法,生成另外一个 token。用这个新生成的 token 跟调用方传递过来的 token 对比。如果一致,则允许接口调用请求;否则,就拒绝接口调用请求。
我们可以进一步优化 token 生成算法,引入一个随机变量,让每次接口请求生成的 token 都不一样。我们可以选择时间戳作为随机变量。
调用方在进行接口请求的时候,将 token、AppID、时间戳,随 URL 一并传递给微服务端。微服务端在收到这些数据之后,会验证当前时间戳跟传递过来的时间戳,是否在一定的时间窗口内(比如一分钟)。如果超过一分钟,则判定 token 过期,拒绝接口请求。如果没有超过一分钟,则说明 token 没有过期,就再通过同样的 token 生成算法,在服务端生成新的 token,与调用方传递过来的 token 比对,看是否一致。如果一致,则允许接口调用请求;否则,就拒绝接口调用请求。
针对 AppID 和密码的存储,我们最好能灵活地支持各种不同的存储方式,比如 ZooKeeper、本地配置文件、自研配置中心、MySQL、Redis 等。
根据需求描述,把其中涉及的功能点,一个一个罗列出来,然后再去看哪些功能点职责相近,操作同样的属性,是否应该归为同一个类。让我们拆解上文的最终需求,注意:拆解出来的每个功能点要尽可能的小。每个功能点只负责做一件很小的事情(单一职责)。
我们发现,1、2、6、7 都是跟 token 有关,负责 token 的生成、验证;3、4 都是在处理 URL,负责 URL 的拼接、解析;5 是操作 AppID 和密码,负责从存储中读取 AppID 和密码。
所以,我们可以粗略地得到三个核心的类:AuthToken、Url、CredentialStorage。
这个需求相对简单,针对复杂的需求开发,我们首先要做的是进行模块划分,将需求先简单划分成几个小的、独立的功能模块,然后再在模块内部,应用我们刚刚讲的方法,进行面向对象设计。而模块的划分和识别,跟类的划分和识别,是类似的套路。
识别出需求描述中的动词,作为候选的方法,再进一步过滤筛选。类比一下方法的识别,我们可以把功能点中涉及的名词,作为候选属性,然后同样进行过滤筛选。我们可以借用这个思路,根据功能点描述,识别出来 AuthToken 类的属性和方法,如下所示:
从上面的类图中,我们可以发现这样三个小细节。
第一个细节:并不是所有出现的名词都被定义为类的属性,比如 URL、AppID、密码、时间戳这几个名词,我们把它作为了方法的参数。
第二个细节:我们还需要挖掘一些没有出现在功能点描述中属性,比如 createTime,expireTimeInterval,它们用在 isExpired() 函数中,用来判定 token 是否过期。
第三个细节:我们还给 AuthToken 类添加了一个功能点描述中没有提到的方法 getToken()。
第一个细节告诉我们,从业务模型上来说,不应该属于这个类的属性和方法,不应该被放到这个类里。比如 URL、AppID 这些信息,从业务模型上来说,不应该属于 AuthToken,所以我们不应该放到这个类中。
第二、第三个细节告诉我们,在设计类具有哪些属性和方法的时候,不能单纯地依赖当下的需求,还要分析这个类从业务模型上来讲,理应具有哪些属性和方法。这样可以一方面保证类定义的完整性,另一方面不仅为当下的需求还为未来的需求做些准备。
接口请求并不一定是以 URL 的形式来表达,还有可能是 Dubbo、RPC 等其他形式。为了让这个类更加通用,命名更加贴切,我们接下来把它命名为 ApiRequest。下面是我根据功能点描述设计的 ApiRequest 类。
泛化、实现、组合、依赖 这四种关系掌握即可
public class A { ... } public class B extends A { ... }
public interface A {...} public class B implements A { ... }
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) { ... } }
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(); } }
接口鉴权并不是一个独立运行的系统,而是一个集成在系统上运行的组件,所以,我们封装所有的实现细节,设计了一个最顶层的 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."); } } }