如果针对框架,类库,组件等非业务系统的开发,就有一个比较大的难点:需求一般比较抽象,模糊,需要你自己去挖掘,做合理取舍,权衡,假设,把抽象的问题具体化,最终产生清晰的,可落地的需求定义。
需求定义是否清晰合理,直接影响后续的设计,编码实现是否顺畅。
需求分析的过程实际上是一个不断迭代优化的过程,不要试图一下就能给出完美的解决方案,而是先给出一个粗糙,基础的方案,有一个迭代的基础,然后再慢慢优化,这样一个思考过程能让我们摆脱无法下手的窘境。
根据需求描述,我们把其中涉及的功能点,以一个个罗列出来,然后再去看哪些功能点职责相近,操作同样的属性,可否归为同一个类。
我们识别出需求描述中的动词,作为候选的方法,再进一步过滤筛选,把功能点中涉及的名词,作为候选属性,然后同样再进行过滤筛选。
UML统一建模语言中定义了六种类之间的关系。它们分别是:泛化,实现,关联,聚合,组合,依赖。
我们将所有的类组装在一起,提供一个执行入口。这个入口可能是main()函数,也可能是一个组给外部用的API接口。通过这个入口,我们能触发整个代码跑起来。
需求分析是不断迭代优化的过程。
第一轮基础分析
通过用户名和密码做接口权限认证:
(1)给允许访问服务的调用方,派发一个AppId和密码
(2)调用方每次接口请求时候,携带这个AppId和密码
(3)服务方接收到请求后,对比AppId和密码
第二轮分析优化
AppId和密码用明文传输,被截获不安全,可以借助加密算法(比如SHA)进行加密。
不过,未认证系统截获加密密码,还可以利用加密密码伪装成已认证系统访问接口。
我们可以利用OAuth的验证思路来解决:
第三轮分析优化
这种token方式可以解决截获方修改id参数获取更多接口数据的问题。但是,截获方还是可以不改动id参数,重复不断调用接口。
我们可以引入随机变量时间戳,只能在一定时间窗口内(比如一分钟)才能请求成功,避免截获方重复攻击的风险。
2、面向对象设计
(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的生成和验证,归为AuthToken类;
【3】【4】负责URL生成和解析,归为URL类;
【5】负责从存储读取AppID和密码,归为CredentialStorage类;
(2)定义类及其属性和方法
AuthToken类:
从业务模型角度看,URL,AppID,密码,时间戳并不属于AuthToken类的属性,就没有放在这个类里,而是在外部拼接成token参数传进构造函数里。
还从业务模型角度看,理应还具有哪些属性,增加了createTime,expiredTimeInterval属性。
类的属性和方法设计要从业务模型角度思考,保证类定义的完整性,甚至需要为未来需求做些准备。
ApiRequest类:
接口请求并不一定以URL形式来表达,还可能是Dubbo,RPC等其他形式,为了让类更加通用,所以类命名为ApiRequest。
CredentialStorage接口:
为了做到抽象封装具体的存储方式,将CredentialStorage设计成接口,基于接口而非具体的实现编程。
(3)定义类与类之间的交互关系
UML统一建模语言定义了六种类之间的关系:泛化,实现,关联,聚合,组合,依赖。
泛化:继承关系,比如B子类继承A父类;
public class A { ... }
public class B extends A { ... }
实现:接口和实现类关系,比如B实现类实现A接口类;
public interface A {...}
public class B implements A { ... }
聚合:包含关系,比如A类对象包含B类对象,A类对象销毁不影响B类对象,就像课程与学生之间的关系;
public class A {
private B b;
public A(B b) {
this.b = b;
}
}
组合:包含关系,比如A类对象包含B类对象,B类对象不能单独存在,就像鸟与翅膀的关系;
public class A {
private B b;
public A() {
this.b = new B();
}
}
关联:弱包含关系,包括聚合和组合,比如B类对象是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();
}
}
依赖:比关联关系更弱的关系,包含关联关系,只要B类对象和A类对象有任何使用关系就行,比如A类的方法使用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();
}
}
或者
public class A {
public void func(B 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.");
}
}
}