设计模式之美学习-如何进行面向对象设计(三)

需求

实现一个接口鉴权的功能,实现思路

1.调用端下发一个appId和秘钥

2.调用端将每次传过来的参数url后面都要带上appId+时间戳同时根据url+appId+时间戳+参数+秘钥进行MD5加密后通过token参数传过来

3.获取时间戳判断是否过期(假定1分钟) 如果过期鉴权失败

4.服务端解析出url+appId+时间戳+参数+秘钥进行md5加密

5.根据token匹配 如果不匹配 则认为参数被篡改 则鉴权失败

方式一

把需求描述中的名词罗列出来,作为可能的候选类,然后再进行筛选。对于没有经验的初学者来说,这个方法比较简单、明确,可以直接照着做。

我想到的几个名词 囧

鉴权

解析

加密

方式二

类的大致拆解

将上面的需求拆分成一个一个尽可能小的功能点"单一职责"

1.解析url获取参数、token、时间戳

2.从存储中根据appId获取秘钥

3.将url+appId+时间戳+参数+秘钥+拼成一个串

4.对字符串进行加密生成一个token

5.根据时间戳判断是否token过期(防止接口重放)

6.对加密后的token和调用方穿过来的token判断是否一致(防止参数篡改)

 

3 4  5 6都是跟token相关 负责token的生成验证,1负责解析url,2.操作appid和密码

所以,我们可以粗略地得到三个核心的类:AuthToken、Url、CredentialStorage。AuthToken 负责实现 3 4  5 6这四个操作;Url 负责 1两个操作;CredentialStorage 负责 2 这个操作

类的定义

AuthToken

3.将url+appId+时间戳+参数+秘钥+拼成一个串

4.对字符串进行加密生成一个token

5.根据时间戳判断是否token过期

6.对加密后的token和调用方穿过来的token判断是否一致

对于方法的识别,很多面向对象相关的书籍,一般都是这么讲的,识别出需求描述中的动词,作为候选的方法,再进一步过滤筛选。类比一下方法的识别,我们可以把功能点中涉及的名词,作为候选属性,然后同样进行过滤筛选

 

动词: 1.拼接串 2.判断是否过期 3.生成token 4.是否匹配    但是拼接串和生成token其实都是为生成token服务所以实现了2个方法1.isExpired判断是否过期  2.match是否匹配

注意点:

  1. 从业务模型上来说,不应该属于这个类的属性和方法,不应该被放到这个类里。比如 URL、AppID 这些信息,从业务模型上来说,不应该属于 AuthToken,所以我们不应该放到这个类中。
  2. 在设计类具有哪些属性和方法的时候,不能单纯地依赖当下的需求,还要分析这个类从业务模型上来讲,理应具有哪些属性和方法。这样可以一方面保证类定义的完整性,另一方面不仅为当下的需求还为未来的需求做些准备。

Url

1.解析url获取参数、token、时间戳

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

 

 

CredentialStorage

2.从存储中根据appId获取秘钥

 

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

UML 统一建模语言中的关系 

泛化

泛化(Generalization)可以简单理解为继承关系。

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

实现

实现(Realization)一般是指接口和实现类之间的关系。

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

聚合

聚合(Aggregation)是一种包含关系,A 类对象包含 B 类对象,B 类对象的生命周期可以不依赖 A 类对象的生命周期,也就是说可以单独销毁 A 类对象而不影响 B 对象,比如课程与学生之间的关系

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

组合

组合(Composition)也是一种包含关系。A 类对象包含 B 类对象,B 类对象的生命周期跟依赖 A 类对象的生命周期,B 类对象不可单独存在,比如鸟与翅膀之间的关系。

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

关联

关联(Association)是一种非常弱的关系,包含聚合、组合两种关系。具体到代码层面,如果 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();
  }
}

依赖

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

关系实现

 

CredentialStorage为接口 可以设计MysqlCredentialStorage或者RedisCredentialStorage

ApiRequest也是接口可以为HttpApiRequest或者DubboApiRequest

所以这里只用到了实现关系

对外提供的入口方法可以通过main方法吧整个流程跑完 如mvc的拦截器的执行方法实现

public interface ApiAuthenticator {
  void auth(String url);
  void auth(ApiRequest apiRequest);
}

public class DefaultApiAuthenticatorImpl implements ApiAuthencator {
  private CredentialStorage credentialStorage;
  
  public DefaultApiAuthenticator() {
    this.credentialStorage = new MysqlCredentialStorage();
  }
  
  public DefaultApiAuthenticator(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.");
    }
  }
}

 

posted @ 2019-12-30 09:44  意犹未尽  阅读(357)  评论(0编辑  收藏  举报