建设网站安全性,飘雪影视在线观看免费完整,com网站是用什么做的,长沙网站建设哪家强1.如何对接口鉴权这样一个功能开发做面向对象分析 
本章会结合一个真实的案例#xff0c;从基础的需求分析、职责划分、类的定义、交互、组装运行讲起#xff0c;将最基础的面向对象分析#xff08;00A#xff09;、设计#xff08;00D#xff09;、编程#xff08;00P从基础的需求分析、职责划分、类的定义、交互、组装运行讲起将最基础的面向对象分析00A、设计00D、编程00P的套路讲清楚为后面的设计原则和设计模型打好基础。 
1.1 案例介绍和难点剖析 
假设你参与开发一个微服务。微服务通过 HTTP 暴露接口给其他系统调用。有一天你的领导找到你说“为了保证接口调用的安全希望设计实现一个接口调用鉴权功能只有经过认证的系统才能调用微服务接口没有认证过的系统会被拒绝。希望由你来开发争取尽管上线”。 
这个时候你可能会有脑子里一团浆糊一时间无从下手的感觉 有这种感觉的原因个人觉得有以下两点。 
1.需求不明确 
领导给的需求过于模糊、笼统离落地到设计、编码还有一定的距离。而人的大脑不擅长思考这种过于抽象的问题。 
前面讲过面向对象分析主要的分析对象是“需求”。因此面向对象分析可以粗略地看成“需求分析”。实际上不管是需求分析还是面向对象分析首先要做的是将笼统的需求细化到足够清晰、可执行。需要通过沟通、挖掘、分析、假设、梳理搞清楚具体的需求有哪些哪些是现在要做哪些是未来可能要做的哪些是不用考虑的。 
2.缺少锻炼 
相比单纯的 CRUD 开发鉴权这个开发任务更有难度。鉴权作为一个根具体业务无关的功能完全可以把它独立开发成一个独立的框架集成到很多业务系统中。而作为被很多系统复用的通用框架比如普通的代码我们对框架的代码质量要求更高。 
开发这样的通用框架对工程师的需求分析能力、设计能力、编码能力甚至逻辑思维能力的要求都是比较高的。如果你平时做的都是简单的 CRUD 业务开发那这方面的锻炼肯定不会很多所以一旦遇到这种开发需求很容易因缺少锻炼脑子放空不知道从何入手完全没有思路。 
1.2 对案例进行需求分析 
实际上需求分析的工作很琐碎没有固定的方法论。系统通过这个例子给你展示下需求分析时完整的考虑思路是什么样的。希望你自己体会举一反三地应用到其他项目的需求分析中。 
针对鉴权这个功能的开发该如何做需求分析 
实际上这和做算法题类似先从最简单的法案想起然后再优化。所以我把分析的过程分为了循序渐进的四轮。 
第一轮基础分析 
对于如何鉴权这样的问题最简单的解决方案是通过用户名加密码来做认证。我们给每个允许访问服务的调用方派发一个 APPID 和一个对应的密码。调用方每次请求时都携带自己的 APPID 和密码。微服务在接受到接口调用请求后会解析出 APPID 和密码和存储的 APPID 和密码进行对比。如果一致则允许调用请求否则拒绝调用。 
第二轮分析优化 
这样的验证方式每次都要传输明文密码。密码很容易被屏蔽是不安全的。那如果借助加密算法比如 SHA对密码进行加密后再传递到微服务端验证是不是就可以了 
实际上这样也不安全因为加密之后的密码及 APPID照样可以被未认证系统或黑客截获未认证系统可以携带这个加密之后的面以及对应的 APPID伪装成已认证系统来访问我们的接口。这就是典型的“重放攻击”。 
提出问题再解决问题是一个非常好的迭代方式。对于刚刚的问题可以借助 OAuth 的验证思路来解决。 调用方将请求的 URL 跟 APPID、密码拼接在一起然后进行加密生成一个 token 。调用方在接口请求的时候将这个 token 及 APPID跟着 URL 一块传递给服务端。服务端接受到这些数据后根据 APPID 从数据库中取出对应的密码并通过同样的 token 生成算法生成另外一个 token。用这个新生成的 token 和调用方传递过来的 token 对比。如果一致则允许接口调用请求否则拒绝调用。 
客户端过程 
1.生成token SHA(http://www.test.com/user?id123appidabcpwddef)
2.生成新URLhttp://www.test.com/user?id123appidabcpwddeftokenxxx服务端过程 
3.解析出 URL、Appid、token
4.从数据库中根据 Appid 取出 pwd
5.使用同样的算法生成服务端 token_s
6. token  token_s允许访问token ! token_s拒绝访问。第三轮分析优化 
经过第二轮优化后仍然存在重放攻击的风险。因为每个 URL 拼接上 Appid 、密码生成的 token 都是固定的。 
为解决这个问题可以进一步优化 token 生成算法引入一个随机变量让每次接口请求生成的 token 都不一样。可以选择时间戳作为随机变量。现在使用 URL、Appid、密码、时间戳四种进行加密生成 token。调用方在进行接口请求时将 token、Appid、时间戳随着 URL 一起传给微服务端。 
微服务端在接受到这些数据后会验证当前时间戳和传递过来的时间戳是否在一定的时间窗口内如一分钟。如果超过时间窗口则判定 token 过期拒绝接口请求。如果没有超过时间窗口则说明 token 没有过期就在通过同样的 token 生成算法在服务端生成新的 token和调用方的 token 对比。若一致则允许接口调用请求否则拒绝调用。 
优化后的认证流程如下 
客户端流程 
1.生成token SHA(http://www.test.com/user?id123appidabcpwddefts156152345)
2.生成新URLhttp://www.test.com/user?id123appidabcpwddeftokenxxxts156152345服务端流程 
3.解析出 URL、Appid、token
4.验证token是否失效。失效就拒绝访问否则执行5
5.从数据库中根据 Appid 取出 pwd
6.使用同样的算法生成服务端 token_s
7. token  token_s允许访问token ! token_s拒绝访问。第四轮分析优化 
不过你可能会说这样还是不够安全呀。未认证系统还是可以在一分钟的 token 失效窗口内通过截取请求来调用我们的借口OA。 
你说的不错。不过在攻与防之间本来就没有绝对的安全。我们能做的就是尽量提高攻击的成本。这个方案虽然还有漏洞但是实现起来足够简单而且不会过度影响接口本身的性能比如响应时间。 
实际上还有一个细节我们还没有考虑到那就是如何在微服务端存储每个授权调用方的 Appid 和密码。当然这个问题并不难。最容易想到的方案就是存储到数据库里比如 MySQL。不过像开发这样的非业务功能最好不要与具体的第三方系统有过度的耦合。 
针对 Appid 和密码的存储最后可以灵活支持不同的存储方式比如 Zookeeper、本地配置文件、自研配置中心、MySQL、Redis 等。我们不一定针对每种存储都去做实现但起码要留有扩展点保证系统足够的灵活性和扩展性能在我们切换存储方式的时候尽可能少的改动代码。 
最终确定需求 
调用方进行接口请求的时候将 URL、Appid、密码、时间戳拼接在一起通过加密算法生成 token并将 token、Appid、时间戳拼接在 URL 中一并发送到微服务端。微服务端在接收到调用方的请求后从请求中解析出 token、Appid、时间戳。微服务端首先检查传递过来的时间戳是否在 token 失效时间窗口内。若已失效则接口调用鉴权失败拒绝接口调用请求如果 token 没有过期失效微服务再从自己的存储中取出 Appid 对应的密码通过同样的 token 生成算法生成另一个 token与调用方的 token 进行比对。如果一致则鉴权成功允许接口调用否则就拒绝接口调用。 
这就是我们的需求分析的整个过程从最粗糙、最模型的需求开始通过“提出问题 - 再解决问题”的方式循序渐进的方式进行优化最后得到一个足够清晰、可落地的需求描述。 
2.如何利用面向对象设计和编程开发接口鉴权功能 
2.1如何进行面向对象设计OOD 
面向对象分析的产出是详细的需求描述面向对象设计的产出是类。在面向对象设计环节我们将需求描述转化成具体的类的设计。 
设计这一环节拆解细化主要包含以下几个部分 
划分职责而识别出有哪些类定义类及其属性和方法定义类之间的交互关系将类组装起来并提供执行入口 
划分职责而识别出有哪些类 
根据需求描述把其中涉及的功能点一个个罗列出来然后再去看看哪些功能职责相近操作同样的属性是否应该归为同一个类。 
我们来看下针对鉴权这个例子具体如何来做。之前我们依据确定了最终需求如下 调用方进行接口请求的时候将 URL、Appid、密码、时间戳拼接在一起通过加密算法生成 token并将 token、Appid、时间戳拼接在 URL 中一并发送到微服务端。微服务端在接收到调用方的请求后从请求中解析出 token、Appid、时间戳。微服务端首先检查传递过来的时间戳是否在 token 失效时间窗口内。若已失效则接口调用鉴权失败拒绝接口调用请求如果 token 没有过期失效微服务再从自己的存储中取出 Appid 对应的密码通过同样的 token 生成算法生成另一个 token与调用方的 token 进行比对。如果一致则鉴权成功允许接口调用否则就拒绝接口调用。 首先是逐字逐句地阅读上面的需求拆解成一个个小的功能点一条条罗列下来。注意拆解出来的每个功能点要尽可能小。每个功能点只负责一个很小的事情专业叫法是“单一职责”。下面是逐句拆解下来后得到的功能点罗列 
将 URL、Appid、密码、时间戳拼接为一个字符串对字符串通过加密算法加密得到 token将 token、Appid、时间戳拼接在 URL 中形成新的 URL解析得到 token、Appid、时间戳等信息根据时间戳判断 token 是否过期失效从存储中取出 Appid 对应的密码验证两个 token 是否匹配 
从上面的功能列表中我们发现 1、2、5、7 都是和 token 相关负责 token 的生成、验证。3、4 都是在处理 URL负责 URL 的拼接和解析6 是操作 Appid 和密码负责从存储中读取 Appid 和密码。所以我们可以粗略地得到三个核心类AuthToken 、URL、CredentialStorage。 
AuthToken 负责 1、2、5、7 这四个操作。URL 负责 3、4 这两个操作。CredentialStorage 负责 6 这个操作。 
当然这是一个初步的类划分其他一些不重要的类我们可能暂时没有办法一下子想全但这也没关系面向对象分析、设计、编程本来就是一个循环迭代、不断优化的过程。根据需求我们先给出一个粗糙版本的设计方案然后基于这样一个基础再去迭代优化会更加容易些思路也更加清晰一些。 
需要强调一点接口调用鉴权这个需求比较简单所以需求对应的面向对象设计并不复杂识别出来的类也不多。如果是面向的更加大型的软件开发、更加复杂的需求涉及的功能点可能会很多对应的类也会比较多像刚刚那样根据需求逐句罗列功能点的方法最后会得到一个很长的列表就会优点凌乱、没有规律。 
针对这种复杂的需求开发首先要做的是进行模块划分将需求先简单划分成几个小的、独立的功能模块然后再在模块内部应用刚刚的方法进行面向对象设计。而模块的划分和识别跟类的划分和识别是类似的套路。 
定义类及其属性和方法 
通过刚刚的需求分析识别出了三个核心类AuthToken 、URL、CredentialStorage。现在再来看下每个类有哪些属性和方法。我们还是从功能点列表中挖掘。 
AuthToken 类相关的功能点有四个 
将 URL、Appid、密码、时间戳拼接为一个字符串对字符串通过加密算法加密得到 token根据时间戳判断 token 是否过期失效验证两个 token 是否匹配 
对于方法的识别一般都是识别需求描述中的动词作为候选方法再进一步过滤筛选。类比下方法的识别可以把功能点中涉及的名词作为候选属性然后同样进行过滤筛选。 
借用这个思路识别出 AuthToken 类的属性和方法 
/****** AuthToken ******/
// 属性
private static final long DEFAULT_EXPIRED_TIME_INTERVAL  60000;
private String token;
private long createTime;
private long expiredTimeInterval  DEFAULT_EXPIRED_TIME_INTERVAL;// 构造函数
public AuthToken(String token, long createTime);
public AuthToken(String token, long createTime, long expiredTimeInterval);// 函数
public static AuthToken create(String baseUrl, long createTime, MapString, String params;)
public String getToken();
public boolean isExpired();
public boolean match(AuthToken authToken);从上面的类中我们可以返现这样三个小细节 
第一个细节 并不是所有出现的名词都被定义为类的属性比如 URL、Appid、密码、时间戳这几个名词我们把它作为了方法的参数。第二个细节我们还需要挖掘出一些没有出现在功能点描述中的属性比如 createTime、expiredTimeInterval它们用在 isExpired() 函数中用来判断 token 是否过期。第三个细节我们还给 AuthToken 类添加了一个功能点描述中没有提到的方法 getToken()。 
第一个细节高速我们从业务模型上来说不应该属于这个类的属性和方法不应该被放到这个类中。比如 URL、Appid 这些信息从业务模型上来说不应该属于 AuthToken 所以不应该放到这个类中。 
第二、第三个细节高速我们在设计类具体有哪些属性和方法的时候不能单纯地依赖当下的需求还要分析这个类从业务模型上来讲应该具有哪些属性和方法。这样一方面保证类定义的完整性另一方面不仅为当下的需求还为未来的需求做些准备。 
Url 类相关的功能点有两个 
将 token、Appid、时间戳拼接在 URL 中形成新的 URL解析得到 token、Appid、时间戳等信息 
虽然需求描述中都是以 URL 来代指接口请求但是接口请求并不一定是 URL 的形式来表达还可能是 Dubbo、RPC 等其他形式。为了让这个类设计的更加通用命名更加贴切我们接下来把它命名为 ApiRequest。下面是根据功能点描述设计的 ApiRequest。 
/****** ApiRequest ******/// 属性
private String baseUrl;
private String token;
private String appId;
private long timestamp;// 构造函数
public ApiRequest(String baseUrl, String token,String appId, long timestamp);// 函数
public static ApiRequest createFromUrl(String url);public String getBaseUrl();
public String getToken();
public String getAppId();
public long getTimestamp();CredentialStorage 类相关的功能点有一个 
从存储中取出 Appid 对应的密码 CredentialStorage 类很简单。为了做到抽象封装具体的存储方式我们将 CredentialStorage 设计成了接口基于接口而非实现编程。 
/****** CredentialStorage ******/// 接口函数
String getPasswordByAppId(String appId);定义类之间的交互关系 
类与类之间的关系有哪些 UML 统一建模语言定义了 6 种类之间的关系。分别是泛化、实现、关联、聚合、组合、依赖。 
泛化可以简单理解为继承关系。 
public class A {...}
public class B extends A {...}实现一般是指接口和实现类之间的关系。 
public interface A {...}
public class B implements A {...}聚合 是一种包含关系A 类对象包含 B 类对象B 类对象的生命周期可以不依赖 A 类对的生命周期也就是说可以单独销毁 A 类对象而不影响 B 类对象比如课程与学生的关系。 
public class A {private B b;public A(B b) {this.b  b;}
}组合也是一种包含的关系。A 类对象包含 B 类对象B 类对象的生命周期依赖 A 类对的生命周期B 类对象不可以单独存在比如鸟与翅膀的关系。 
public class A {private B b;public A() {this.b  new B();}
}关联 是一种比较弱的关系包含组合和聚合。如果 B 类对象是 A 类的成员变量那 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 类对象作为入参、返回值、局部变量只要 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();}
}或者public class A {public void func(B b) {...}
}个人觉得这样拆分的太细增加了学习的成本对指导编程没有太大的意义。所以我只保留了四个关系泛化、实现、组合、依赖。其中泛化、实现、依赖的定义不变组合关系替代 UML 中的组合、聚合、关联这三个概念。 相当于重命名关联关系为组合关系且不在区分组合和聚合这两个概念。 只要 B 类对象是 A 类的成员变量那就成 A 类和 B 类具有组合关系。 在看下我们定义的类之间有哪些关系因为目前只有三个核心类所以只用到了实现关系即 CredentialStorage 和 MySqlCredentialStorage 之间是实现关系。接下来讲到组装类的时候还会用到依赖关系、组合关系但是泛化关系暂时没有用到。 
将类组装起来并提供执行入口 
类定义好了类之间的泛化关系也设计好了接下来我们要将所有的类组装在一起提供一个执行入口。这个入口可能是 main() 函数也可能是一组给外部用的 API 接口。通过这个入口我们能触发整个代码跑起来。 
接口鉴权并不是一个独立运行的系统而是一个集成在系统上运行的组件所以我们封装所有的实现细节设计一个最顶层的 ApiAuthenticator 接口类暴露一组给外部调用者或者 API 接口作为触发执行鉴权逻辑的入口。 
/****** ApiAuthenticator ******/
// 接口函数
void auth(String url);
void auth(ApiRequest apiRequest);实现类 
/****** DefaultApiAuthenticatorImpl ******/
// 属性
private CredentialStorage credentialStorage;
// 构造函数
public DefaultApiAuthenticatorImpl();
public DefaultApiAuthenticatorImpl(CredentialStorage credentialStorage);
// 函数
void auth(String url);
void auth(ApiRequest apiRequest);2.2 如何进行面向对象编程OOP 
面向对象设计完成之后已经定义了清晰的类、属性、方法、类之间的交互并将所有的类组装起来提供了统一的执行入口。接下来面向对象编程的工作就是将这些设计思路翻译成代码实现。有了前面分析这部分工作相对来说就比较简单了。所以这里只给出比较复杂的 ApiAuthenticator 的实现。 
对于 AuthToken、ApiRequest、CredentialStorage 这三个类就不给出具体代码实现了。你可以自己试着把整个鉴权框架自己实现一遍。 
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;}Overridevoid auth(String url) {ApiRequest apiRequest  ApiRequest.createFromUrl(url);auth(apiRequest);}Overridevoid auth(ApiRequest apiRequest) {String appId  apiRequest.getAppId();String token  apiRequest.getToken();long timestamp  apiRequest.getTimestamp();String baseUrl  apiRequest.getBaseUrl();AuthToken clientAuthToken  new AuthToken(token, timestamp);if(clientAuthToken.isExpired()) {throw new RuntimeException(Token is exipred.);}String password  credentialStorage.getPasswordByAppId(appId);AuthToken serverAuthToken  AuthToken.generator(baseUrl, appId, password, timestamp);if(!serverAuthToken.match(clientAuthToken)) {throw new RuntimeException(Token verfication failed.);}}
}2.3 辩证思考与灵活应用 
之前讲解过面向对象分析、设计、编程每个环节的界限划分都比较清楚。而且设计和实现基本上是按照功能点的描述逐句照着翻译过来的。这样做的好处是先做什么后做什么都非常清晰、明确。 
不过在平时的工作中大部分程序员往往都是在脑子里或者草纸上完成面向对象分析和设计后然后就开始写了边写边思考重构并不会严格地按照刚刚的流程来执行。而且说实话即使在写代码之前花很多时间做分析和设计绘制出完美的类图、UML 图也不可能把每个细节、交互都想的很清楚。在落实到代码的时候还是要反复迭代、重构、打破重写。 
毕竟整个软件开发本来就是一个迭代、修修补补、遇到问题解决问题的过程是一个不断重构的过程。我们没法严格地按照顺序执行各个步骤。 
2.4 总结回顾 
面向对象分析的产出是详细的需求描述。面向对象设计的产出是类。在面向对象设计这一环节我们将需求描述转化为具体的类的设计。这个环节的工作可以分为四步 
划分职责进而识别出有哪些类 根据需求描述把其中涉及的功能点一个个罗列出来然后再去看哪些功能点职责相近操作同样的属性可否归为一个类。定义类的属性和方法 识别出功需求中的动词作为候选方法再进一步过滤筛选出真正的方法把功能点中涉及的名词作为候选属性然后再同样进行过滤筛选。定义类与类之间的关系 UML 统一建模语言定义了六种类之间的关系。分别是泛化、实现、关联、组合、聚合、依赖。从贴近编程的角度我们对类之间的关系做了调整保留四个关系泛化、实现、组合、依赖。将类封装起来并提供执行入口 将所有类组装在一起提供一个执行入口。这个入口可能是 main() 函数也可能是一组给外部调用的 API 接口。通过这个接口我们能触发整个代码跑起来。