... views
微服务架构的引入为软件应用带来了诸多好处:包括小开发团队,缩短开发周期,语言选择灵活性,增强服务伸缩能力等。与此同时,也引入了分布式系统的诸多复杂问题。其中一个挑战就是如何在微服务架构中实现一个灵活,安全,高效的认证和鉴权方案。本文将尝试就此问题进行一次比较完整的探讨。
在单体架构下,整个应用是一个进程,在应用中,一般会用一个安全模块来实现用户认证和鉴权。
用户登录时,应用的安全模块对用户身份进行验证,验证用户身份合法后,为该用户生成一个会话(Session),并为该 Session 关联一个唯一的编号(Session Id)。Session 是应用中的一小块内存结构,其中保存了登录用户的信息,如 User name, Role, Permission 等。服务器把该 Session 的 Session Id 返回给客户端,客户端将 Session Id 以 cookie 或者 URL 重写的方式记录下来,并在后续请求中发送给应用,这样应用在接收到客户端访问请求时可以使用 Session Id 验证用户身份,不用每次请求时都输入用户名和密码进行身份验证。
备注:为了避免 Session Id 被第三者截取和盗用,客户端和应用之前应使用 TLS 加密通信,session 也会设置有过期时间。
客户端访问应用时,Session Id 随着 HTTP 请求发送到应用,客户端请求一般会通过一个拦截器处理所有收到的客户端请求。拦截器首先判断 Session Id 是否存在,如果该 Session Id 存在,就知道该用户已经登录。然后再通过查询用户权限判断用户能否执行该此请求,以实现操作鉴权。
在微服务架构下,一个应用被拆分为多个微服务进程,每个微服务实现原来单体应用中一个模块的业务功能。应用拆分后,对每个微服务的访问请求都需要进行认证和鉴权。如果参考单体应用的实现方式会遇到下述问题:
一个完整的微服务应用是由多个相互独立的微服务进程组成的,对每个微服务的访问都需要进行用户认证。如果将用户认证的工作放到每个微服务中,应用的认证逻辑将会非常复杂。因此需要考虑一个 SSO(单点登录)的方案,即用户只需要登录一次,就可以访问所有微服务提供的服务。 由于在微服务架构中以 API Gateway 作为对外提供服务的入口,因此可以考虑在 API Gateway 处提供统一的用户认证。
HTTP 是一个无状态的协议,对服务器来说,用户的每次 HTTP 请求是相互独立的。互联网是一个巨大的分布式系统,HTTP 协议作为互联网上的一个重要协议,要考虑到大量应用访问的效率问题。无状态意味着服务端可以把客户端的请求根据需要发送到集群中的任何一个节点,HTTP 的无状态设计对负载均衡有明显的好处,由于没有状态,用户请求可以被分发到任意一个服务器,应用也可以在靠近用户的网络边缘部署缓存服务器。对于不需要身份认证的服务,例如浏览新闻网页等,这是没有任何问题的。但很多服务如网络购物,企业管理系统等都需要对用户的身份进行认证,因此需要在 HTTP 协议基础上采用一种方式保存用户的登录状态,避免用户每发起一次请求都需要进行验证。
传统方式是在服务器端采用 Cookie 来保存用户状态,由于在服务器是有状态的,对服务器的水平扩展有影响。在微服务架构下建议采用 Token 来记录用户登录状态。
Token 和 Seesion 主要的不同点是存储的地方不同。Session 是集中存储在服务器中的;而 Token 是用户自己持有的,一般以 cookie 的形式存储在浏览器中。Token 中保存了用户的身份信息,每次请求都会发送给服务器,服务器因此可以判断访问者的身份,并判断其对请求的资源有没有访问权限。
Token 用于表明用户身份,因此需要对其内容进行加密,避免被请求方或者第三者篡改。 JWT(Json Web Token) 是一个定义 Token 格式的开放标准(RFC 7519),定义了 Token 的内容,加密方式,并提供了各种语言的 lib。
JWT Token 的结构非常简单,包括三部分:
{
"alg": "HS256",
"typ": "JWT"
}
{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
这三部分使用 Base64 编码后组合在一起,成为最终返回给客户端的 Token 串,每部分之间采用".“分隔。下图是上面例子最终形成的 Token 采用 Token 进行用户认证,服务器端不再保存用户状态,客户端每次请求时都需要将 Token 发送到服务器端进行身份验证。Token 发送的方式 rfc6750 进行了规定,采用一个 Authorization: Bearer HHTP Header 进行发送。
Authorization: Bearer mF_9.B5f-4.1JqM
采用 Token 方式进行用户认证的基本流程如下图所示:
单点登录的理念很简单,即用户只需要登录应用一次,就可以访问应用中所有的微服务。API Gateway 提供了客户端访问微服务应用的入口,Token 实现了无状态的用户认证。结合这两种技术,可以为微服务应用实现一个单点登录方案。
用户的认证流程和采用 Token 方式认证的基本流程类似,不同之处是加入了 API Gateway 作为外部请求的入口。
用户登录
用户请求
用户权限控制有两种做法,在 API Gateway 处统一处理,或者在各个微服务中单独处理。
客户端发送的 HTTP 请求中包含有请求的 Resource 及 HTTP Method。如果系统遵循 REST 规范,以 URI 资源方式对访问对象进行建模,则 API Gateway 可以从请求中直接截取到访问的资源及需要进行的操作,然后调用 Security Service 进行权限判断,根据判断结果决定用户是否有权限对该资源进行操作,并转发到后端的 Business Service。这种实现方式 API Gateway 处统一处理鉴权逻辑,各个微服务不需要考虑用户鉴权,只需要处理业务逻辑,简化了各微服务的实现。
如果微服务未严格遵循 REST 规范对访问对象进行建模,或者应用需要进行定制化的权限控制,则需要在微服务中单独对用户权限进行判断和处理。这种情况下微服务的权限控制更为灵活,但各个微服务需要单独维护用户的授权数据,实现更复杂一些。
对于第三方应用接入的访问控制,有两种实现方式:
第三方使用一个应用颁发的 API Token 对应用的数据进行访问。该 Token 由用户在应用中生成,并提供给第三方应用使用。在这种情况下,一般只允许第三方应用访问该 Token 所属用户自身的数据,而不能访问其他用户的敏感私有数据。
例如 Github 就提供了 Personal API Token 功能,用户可以在 Github 的开发者设置界面 中创建 Token,然后使用该 Token 来访问 Github 的 API。在创建 Token 时,可以设置该 Token 可以访问用户的哪些数据,如查看 Repo 信息,删除 Repo,查看用户信息,更新用户信息等。
使用 API Token 来访问 Github API
curl -u zhaohuabing:fbdf8e8862252ed0f3ba9dba4e328c01ac93aeec https://api.github.com/user
使用 API Token 而不是直接使用用户名/密码来访问 API 的好处是降低了用户密码暴露的风险,并且可以随时收回 Token 的权限而不用修改密码。
由于 API Token 只能访问指定用户的数据,因此适合于用户自己开发一些脚本或小程序对应用中自己的数据进行操作。
某些第三方应用需要访问不同用户的数据,或者对多个用户的数据进行整合处理,则可以考虑采用 OAuth。采用 OAuth,当第三方应用访问服务时,应用会提示用户授权第三方应用相应的访问权限,根据用户的授权操作结果生成用于访问的 Token,以对第三方应用的操作请求进行访问控制。
同样以 Github 为例,一些第三方应用如 Travis CI,GitBook 等就是通过 OAuth 和 Github 进行集成的。 OAuth 针对不同场景有不同的认证流程,一个典型的认证流程如下图所示:
备注:
- OAuth 中按照功能区分了资源服务器和认证服务器这两个角色,在实现时这两个角色常常是同一个应用。将该流程图中的各个角色对应到 Github 的例子中,资源服务器和认证服务器都是 Github,客户端程序是 Travis CI 或者 GitBook,用户则是使用 Travis CI 或者 GitBook 的直接用户。
- 有人可能会疑惑在该流程中为何要使用一个授权码(Authorization Code)来申请 Token,而不是由认证服务器直接返回 Token 给客户端。OAuth 这样设计的原因是在重定向到客户端 Callback URL 的过程中会经过用户代理(浏览器),如果直接传递 Token 存在被窃取的风险。采用授权码的方式,申请 Token 时客户端直接和认证服务器进行交互,并且认证服务期在处理客户端的 Token 申请请求时还会对客户端进行身份认证,避免其他人伪造客户端身份来使用认证码申请 Token。 下面是一个客户端程序采用 Authorization Code 来申请 Token 的示例,client_id 和 client_secret 被用来验证客户端的身份。
POST /oauth/token HTTP/1.1 Host: authorization-server.com grant_type=authorization_code &code=xxxxxxxxxxx &redirect_uri=https://example-app.com/redirect &client_id=xxxxxxxxxx &client_secret=xxxxxxxxxx
另外在谈及 OAuth 时,我们需要注意微服务应用作为 OAuth 客户端和 OAuth 服务器的两种不同场景:
在实现微服务自身的用户认证时,也可以采用 OAuth 将微服务的用户认证委托给一个第三方的认证服务提供商,例如很多应用都将用户登录和微信或者 QQ 的 OAuth 服务进行了集成。
第三方应用接入和微服务自身用户认证采用 OAuth 的目的是不同的,前者是为了将微服务中用户的私有数据访问权限授权给第三方应用,微服务在 OAuth 架构中是认证和资源服务器的角色;而后者的目的是集成并利用知名认证提供服务商提供的 OAuth 认证服务,简化繁琐的注册操作,微服务在 OAuth 架构中是客户端的角色。
因此在我们需要区分这两种不同的场景,以免造成误解。
除了来自用户和第三方的北向流量外,微服务之间还有大量的东西向流量,这些流量可能在同一个局域网中,也可能跨越不同的数据中心,这些服务间的流量存在被第三方的嗅探和攻击的危险,因此也需要进行安全控制。
通过双向 SSL 可以实现服务之间的相互身份认证,并通过 TLS 加密服务间的数据传输。需要为每个服务生成一个证书,服务之间通过彼此的证书进行身份验证。在微服务运行环境中,可能存在大量的微服务实例,并且微服务实例经常会动态变化,例如随着水平扩展增加服务实例。在这种情况下,为每个服务创建并分发证书变得非常困难。我们可以通过创建一个私有的证书中心(Internal PKI/CA)来为各个微服务提供证书管理如颁发、撤销、更新等。