第一部分:实战二

第一部分:实战二

实战二(上)

案例介绍和难点剖析

  1. 以一个真实的开发案例,从基础的需求分析、职责划分、类的定义、交互、组装运行讲起,将最基础的面向对象分析、设计、编程的套路给你讲清楚,为后面学习设计原则、设计模式打好基础。
  2. 真实案例,给你的微服务增加接口调用鉴权功能。

需求不明确

  1. leader 给到的需求过于模糊、笼统,不够具体、细化,离落地到设计、编码还有一定的距离。
  2. 面向对象分析可以粗略地看成“需求分析”。

缺少锻炼

  1. 鉴权作为一个跟具体业务无关的功能,我们完全可以把它开发成一个独立的框架,集成到很多业务系统中。
  2. 开发这样通用的框架,对工程师的需求分析能力、设计能力、编码能力,甚至逻辑思维能力的要求,都是比较高的。

对案例进行需求分析

第一轮基础分析

设计通过AppID和密钥来认证。

第二轮分析优化

防止“重放攻击”,增加加密token验证。

image

第三轮分析优化

依然有“重放攻击”风险,引入时间戳作为随机变量。

image

第四轮分析优化

  1. 在允许时间范围内(比如1分钟),依然有攻击风险,这是一个攻防策略。
  2. 多端多AppID要如何存储,设计时要留有扩展点,当更换存储时减少代码改动,多运用面向对象的编程思想。
  3. 需求分析主要做的是一个分析的过程,一个思考的过程,如何循序渐进,拆解需求,完善设计,迭代优化。不用过分盯着这个具体案例还不够完善安全等。

最终确定需求

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

实战二(下)

如何进行面向对象设计?

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

  • 回顾上文中需求分析的四点详细需求描述,将需求拆解成小的功能点,遵循“单一职责”。
  • 7个功能点列述:
    1. 把 URL、AppID、密码、时间戳拼接为一个字符串;
    2. 对字符串通过加密算法加密生成 token;
    3. 将 token、AppID、时间戳拼接到 URL 中,形成新的URL;
    4. 解析 URL,得到 token、AppID、时间戳等信息;
    5. 从存储中取出 AppID 和对应的密码;
    6. 根据时间戳判断 token 是否过期失效;
    7. 验证两个 token 是否匹配;
  • 根据功能点划分三个核心类:
    1. AuthToken:1、2、6、7 都是跟 token 有关,负责 token的生成、验证
    2. Url:3、4 都是在处理 URL,负责 URL 的拼接、解析
    3. CredentialStorage:5 是操作 AppID 和密码,负责从存储中读取 AppID和密码
  • 复杂的需求开发,要先进行模块划分,然后在模块内部分别拆解功能点。

定义类及其属性和方法

AuthToken:

  • 一般情况,识别需求中的名词为属性,动词为方法
  • 属性:token,createTime,expireTimeInterval
  • 方法:getToken(),isExpired(),match()
  • 并不是所有出现的名词都定义为属性,有的名词作为了方法得参数,因为这些名词从业务角度不属于当前类
  • 还要挖掘出没在功能点中得属性(在方法中使用)和方法(保证类的完整性)

image

Url类:

  • 类名:ApiRequest
  • 属性:baseUrl,token,appId,timestamp
  • 方法:getBaseUrl(),getToken(),getAppId(),getTimestamp()

image

CredentialStorage类:

  • 设计成接口的形式,基于接口而非实现的编程
  • 可以灵活应对更换存储方式

image

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

  • UML统一建模语言中定义的六种类之间的关系:
    1. 泛化:继承
    2. 实现:接口和实现类
    3. 聚合:A类对象包含B类对象,销毁A类对象不影响B类对象
    4. 组合:A类对象包含B类对象,B类对象不可单独存在
    5. 关联:包含聚合和组合
    6. 依赖:包含关联,只要A类对象和B类对象有任何使用关系
  • 简化的定义:
    1. 泛化
    2. 实现
    3. 依赖
    4. 组合:组合、聚合、关联,并且可以和“多用组合少用继承”中的“组合”统一含义

泛化(Generalization):

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

实现(Realization):

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

聚合(Aggregation):

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

组合(Composition):

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

关联(Association):

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();
	}
}

依赖(Dependency):

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) { ... }
}

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

  • 设计一个最顶层的 ApiAuthenticator 接口类,暴露一组给外部调用者使用的 API 接口,作为触发执行鉴权逻辑的入口。

image

如何进行面向对象编程?

  • 面向对象编程的工作,就是将这些设计思路翻译成代码实现。
  • ApiAuthenticator 接口的实现类代码,此实现类中AppID和密码是使用mysql存储的。
public interface ApiAuthencator {
	void auth(String url);
	void auth(ApiRequest apiRequest);
}

public class DefaultApiAuthencatorImpl implements ApiAuthencator {
	private CredentialStorage credentialStorage;
	
	public ApiAuthencator() {
		this.credentialStorage = new MysqlCredentialStorage();
	}
	
	public ApiAuthencator(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, passw
		if (!serverAuthToken.match(clientAuthToken)) {
			throw new RuntimeException("Token verfication failed.");
		}
	}
}

辩证思考与灵活应用

  • 文中作者将面向对象分析、设计、实现,每个环节的界限划分都比较清楚、细致,即使是没有太多经验的初级工程师,也可以按部就班地做。
  • 然而大部分情况,这些过程可能是在大家脑子里完成的,不要花太多时间放在设计出完美的类图、UML图上,不要拘泥于形式,死搬理论概念。即使设计的时候有遗漏,我们边开发、边思考、边重构,反复迭代,也是代码演变的正常流程。
posted @ 2021-10-03 03:08  起床睡觉  阅读(27)  评论(0编辑  收藏  举报