「端到端的身份认证和访问控制」0x01:信任的传递

2021-03-07 • 预计阅读时间 7 分钟

有了明确的目标后,我们需要开始考虑整个系统如何建立起来。

@startuml
!include https://raw.githubusercontent.com/bschwarz/puml-themes/master/themes/bluegray/puml-theme-bluegray.puml
title [每个服务都进行身份认证]
actor 用户 as User
archimate #Application "接入层服务" as Boundary <<application-service>>
archimate #Technology "认证服务" as AuthServer <<technology-service>>
archimate #Business "后台服务" as Backend1 <<business-service>>
archimate #Business "后台服务" as Backend2 <<business-service>>
archimate #Business "后台服务" as Backend3 <<business-service>>
archimate #Business "后台服务" as Backend4 <<business-service>>
archimate #Business "后台服务" as Backend5 <<business-service>>
archimate #Business "后台服务" as Backend6 <<business-service>>
archimate #Business "后台服务" as Backend7 <<business-service>>
archimate #Technology "身份认证" as Auth <<component>>
User -Down-> Boundary
Boundary -Right-> AuthServer: 1. 身份认证
AuthServer -[dashed]Right-> Boundary: 认证结果
Boundary -Down-> Backend1: 2. 请求后台
Boundary -Down-> Backend2: 2. 请求后台
Backend1 -Down-> Backend3
Backend1 -Down-> Backend4
Backend2 -Down-> Backend5
Backend4 -Down-> Backend6
Backend5 -Down-> Backend7
Backend1 -[dotted]Down- Auth
Backend2 -[dotted]Down- Auth
Backend3 -[dotted]Down- Auth
Backend4 -[dotted]Down- Auth
Backend5 -[dotted]Down- Auth
Backend6 -[dotted]Down- Auth
Backend7 -[dotted]Down- Auth
@enduml

可以看到,其实在接入层应该已经做了身份认证,后面的服务还要做身份认证无非是因为无法确保请求真的做了这个认证,因此这个问题其实可以转化为「如何让后面的服务拿到认证的结果」。这是一种信任的传递,不依赖于任何服务的部署方式。我们只要将认证结果传递下去,只要后面的服务可以验证这个认证结果,就可以确保请求真的经过了身份认证。

@startuml
!include https://raw.githubusercontent.com/bschwarz/puml-themes/master/themes/bluegray/puml-theme-bluegray.puml
title [信任的传递]
actor 用户 as User
archimate #Application "接入层服务" as Boundary <<application-service>>
archimate #Technology "认证服务" as AuthServer <<technology-service>>
archimate #Business "后台服务" as Backend1 <<business-service>>
archimate #Business "后台服务" as Backend2 <<business-service>>
archimate #Business "后台服务" as Backend3 <<business-service>>
archimate #Business "后台服务" as Backend4 <<business-service>>
archimate #Business "后台服务" as Backend5 <<business-service>>
archimate #Business "后台服务" as Backend6 <<business-service>>
archimate #Business "后台服务" as Backend7 <<business-service>>
archimate #Technology "身份认证" as Auth <<component>>
User -Down-> Boundary
Boundary -Right-> AuthServer: 1. 身份认证
AuthServer -[dashed]Right-> Boundary: 认证结果
Boundary -Down-> Backend1: 2. 请求后台\n(认证结果)
Boundary -Down-> Backend2: 2. 请求后台\n(认证结果)
Backend1 -Down-> Backend3: (认证结果)
Backend1 -Down-> Backend4: (认证结果)
Backend2 -Down-> Backend5: (认证结果)
Backend4 -Down-> Backend6: (认证结果)
Backend5 -Down-> Backend7: (认证结果)
Backend1 -[dotted]Down- Auth
Backend2 -[dotted]Down- Auth
Backend3 -[dotted]Down- Auth
Backend4 -[dotted]Down- Auth
Backend5 -[dotted]Down- Auth
Backend6 -[dotted]Down- Auth
Backend7 -[dotted]Down- Auth
@enduml

身份认证显然是访问控制的前提,而当这样的信任传递建立起来以后,后面的服务就可以拿着认证结果进行端到端的访问控制了。比如前文举的坐飞机的例子,实际上只有柜台换登机牌的人进行了身份认证(拿身份证对着看人脸、查行程),生成了认证结果(登机牌),后面的工作人员并不太关心你拿的是不是你的登机牌(虽然理论上最好也关心一下),登机口的工作人员验证了一下登机牌的真假,同时确认登机牌上写的航班是不是本次航班,再后面的工作人员就只看登机牌上写的航班是不是本次航班了。这就是一个信任传递的过程。

可想而知,如果每个地方的工作人员都拿着身份证和相应的联网设备去验证身份证真假、你是不是身份证上的人、你的身份证有没有绑定当次航班这几项信息,虽然确认更安全了,但是成本也明显上升了。前文也着重提到了,安全没有100%,适合当前情况才是最好的。

将认证方式统一起来

一个系统中可能有很多种身份认证的方式,认证可能用到了各种各样的因子,认证的具体方法也不尽相同,但本质上都是一样的(确认请求来源身份),因此利用「认证结果」这个概念,我们可以将各种不同的认证方法产生的结果统一成一种表现形式,对后台服务屏蔽细节,这样不管系统中增加多少种认证因子和认证方式,后台服务都可以自动兼容,大大降低了认证的工作量。

@startuml
!include https://raw.githubusercontent.com/bschwarz/puml-themes/master/themes/bluegray/puml-theme-bluegray.puml
title [认证]
认证方式 "1" -- "0..*" 认证结果
认证方式 o-- 认证因子
@enduml

从这个关系可以看出,认证结果中应该存放认证方式的类型,而认证因子实际上未必真的要放在认证方式或者认证结果中,甚至用「A认证方式是先认证因子X然后认证因子Y」这种方法约定好即可。

票据

至此,我们可以把「认证结果」称为「票据」,这跟实际生活中的各类票据(车票、记名门票、登机牌等等)起到的作用一样,用于标记身份和权限。票据中应该起码存放认证方法身份信息。票据的整个生命周期大概是这样的:

@startuml
!include https://raw.githubusercontent.com/bschwarz/puml-themes/master/themes/bluegray/puml-theme-bluegray.puml
title [票据的生命周期]
start
:在认证服务中生成;
note left
  接入层服务带着身份材料来认证,认证通过后在**本地**生成
  ----
  生成后返回给接入层服务,由接入层服务向下调用时自动带上
end note
(传)
note right
  放在传输协议中,由框架全程自动传输,对业务层透明
end note
:在后台服务中验证;
note left
  从协议中取出,完成验证
end note
stop
@enduml

几个需要注意的地方:

  1. 票据必须在认证服务本地生成,因为这时认证结果在内存中,很难被篡改或伪造——如果身份认证成功后再调用别的服务去生成,那又回到了最开头的问题——这个服务如何确保请求经过了身份认证呢?
  2. 「接入层服务」是指用户在前端直接访问的后台服务,外部访问接入层一般都是通过HTTP(s)协议(如果前端是App,也有可能是私有协议)。从接入层开始,对于微服务体系的系统来说,内部的协议是统一的,形式上可能是HTTP(s)或者私有的RPC协议,框架需要在协议中自定义一个字段用来存放票据。
  3. 票据需要在每次请求时都生成,而不是生成后缓存下来;同样地,请求链终止的时候票据也就消失了,不需要缓存或者落地。这么做显然是出于安全考虑,因为理论上每个请求进入系统的时候都会在接入层进行身份认证,因此完全可以每次都生成出来,给一个很短的有效期(比如几秒),有效地避免票据被截取下来干别的事情。
  4. 票据本身只携带信息,具体的验证还需要业务参与进来,因为只有业务方自己才知道如何去验证身份。比如某请求要查询用户基本信息,则访问获取用户基本信息的服务时,一定会在协议中携带用户的ID,同时请求中的票据里也有一个用户的ID,当两者不一致时即认为需要拦截。

票据类型

由于票据中需要标记票据的具体认证方式,我们可以将不同的认证方式称为票据类型。根据不同产品或功能的情况,接入层的接口的具体形式并不固定,认证方式也可能会根据实际的业务来定。例如:

  • 桌面版网页上用户通过用户名密码来登录,小程序里用户通过微信客户端完成授权,这两者在系统中的登录态一定是不同的(起码会有不同的标识),此时每次请求的身份认证就是对这个登录态的校验。两种不同的渠道虽然都是验证登录态,但理应算作不同的认证方式(即不同的票据类型),这样不仅在安全上做到了隔离,在突发情况下避免全局一刀切,而且对业务也更为友好,在业务需要区分不同客户端的功能时可以天然支持区分验证。
  • 如果有某个敏感操作(比如修改用户登录密码),要求用户额外进行短信验证,验证通过后才能执行的这一次操作,那这次操作对应对应了新的认证方式,是「先验证登录态,再验证短信」,因此后台接口要验证的就是新的票据类型。相应地,生成这种票据的服务要先验证前一种类型的票据合法后才能生成。
@startuml
!include https://raw.githubusercontent.com/bschwarz/puml-themes/master/themes/bluegray/puml-theme-bluegray.puml
sprite $aService jar:archimate/application-service
sprite $bService jar:archimate/business-service
title [票据类型]
actor 用户 as User
rectangle "桌面版网页CGI服务" <<$aService>> #Application {
  archimate #Application GetInfoWeb as "查询用户信息" <<application-interface>>
  archimate #Application ModifyPasswordWeb as "修改用户密码" <<application-interface>>
}
rectangle "微信小程序CGI服务" <<$aService>> #Application {
  archimate #Application GetInfoMiniapp as "查询用户信息" <<application-interface>>
  archimate #Application ModifyPasswordMiniapp as "修改用户密码" <<application-interface>>
}
rectangle "用户信息服务" <<$bService>> #Business {
  archimate #Business GetInfo as "查询用户信息" <<business-interface>>
  archimate #Business ModifyPassword as "修改用户密码" <<business-interface>>
}
note left of GetInfo
  要求票据的认证方式:
  * 验证了**桌面版网页登录态**或者**小程序登录态**
end note
note right of ModifyPassword
  要求票据的认证方式:
  * 先验证了**桌面版网页登录态**或者**小程序登录态**
  * 再验证了**短信**
end note
archimate #Technology WebAuth as "验证桌面版网页登录态" <<technology-interface>>
archimate #Technology MiniappAuth as "验证微信小程序登录态" <<technology-interface>>
archimate #Technology MessageAuth as "验证短信" <<technology-interface>>
note left of MessageAuth
  要求票据的认证方式:
  * 验证了**桌面版网页登录态**或者**小程序登录态**
end note
User -Down-> GetInfoWeb
User -Down-> GetInfoMiniapp
User -Down-> ModifyPasswordWeb
User -Down-> ModifyPasswordMiniapp
GetInfoWeb -[dotted]Left-> WebAuth: A1
GetInfoMiniapp -[dotted]Right-> MiniappAuth: C1
ModifyPasswordWeb -[dotted]Left-> WebAuth: B1
ModifyPasswordWeb -[dotted]Down-> MessageAuth: B2
ModifyPasswordMiniapp -[dotted]Right--> MiniappAuth: D1
ModifyPasswordMiniapp -[dotted]Down--> MessageAuth: D2
GetInfoWeb -Down-> GetInfo: A2 查询用户信息
GetInfoMiniapp -Down-> GetInfo: C2 查询用户信息
ModifyPasswordWeb -Down-> ModifyPassword: B3 修改用户密码
ModifyPasswordMiniapp -Down-> ModifyPassword: D3 修改用户密码
@enduml

现在我们根据信任的传递原则,推导出了票据和票据类型的概念,实际上已经可以从理论上解决身份认证的问题。但是到目前为止,整个系统是「浮于表面」的,因为身份认证只能说是一个手段或者必要条件,我们最终的目标还是要实现端到端的、精细化的访问控制。不过在这之前,可能你已经要问了:就这?纯理论分析简单,但这「信任的传递」怎么保证可靠?

技术安全
知识共享许可协议
本作品采用知识共享署名-相同方式共享 4.0 国际许可协议进行许可。

「端到端的身份认证和访问控制」0x00:问题的提出

comments powered by Disqus