各位读者朋友鼠年大吉,祝各位新的一年身体健康,万事如意!javascript
最近疫情严重,是一个特殊时期,你们必定要注意防御。不少省份推迟了企业开工的时间,大部分的互联网公司也都是下周开始远程办公。你们能够利用在家的几天时间学习充电,反正也出不去(🙂🙂🙂)。html
今天笔者要写得是 Go 微服务相关的组件实践,笔者在好几年前就接触 Go 语言,去年开始从事 Go 微服务相关的开发,在过程当中也和小伙伴联合编写了一本 《Go 高并发与微服务实战》书籍,即将出版上市。本文是截取其中的抢先版阅览,介绍微服务统一认证与受权的 Go 语言实现。java
统一认证与受权是微服务架构的基础功能,微服务架构不一样于单体应用的架构,认证和受权很是集中。当服务拆分以后,对各个微服务认证与受权变得很是分散,因此在微服务架构中,将集成统一认证与受权的功能,做为横切关注点。web
常见的认证与受权方案有 OAuth、分布式 Session、OpenID 和 JWT 等,下面咱们将分别介绍这四种方案。算法
OAuth2 相关理论的介绍主要来自于OAuth2官方文档,相关地址为https://tools.ietf.org/html/rfc6749
。数据库
OAuth 协议的目的是为了为用户资源的受权提供一个安全的、开放而简易的标准。官网中的介绍以下:json
An open protocol to allow secure API authorization in a simple and standard method from web, mobile and desktop applications.浏览器
OAuth1 因为不被 OAuth2 兼容,且签名逻辑过于复杂和受权流程的过于单一,在此不过多谈论,如下重点关注OAuth2认证流程,它是当前Web应用中的主流受权流程。缓存
OAuth2是当前受权的行业标准,其重点在于为Web应用程序、桌面应用程序、移动设备以及室内设备的受权流程提供简单的客户端开发方式。它为第三方应用提供对HTTP服务的有限访问,既能够是资源拥有者经过受权容许第三方应用获取HTTP服务,也能够是第三方以本身的名义获取访问权限。安全
角色
OAuth2 中主要分为了4种角色
在不少时候,资源服务器和受权服务器是合二为一的,在受权交互的时候是受权服务器,在请求资源交互是资源服务器。可是受权服务器是单独的实体,它能够发出被多个资源服务器接受的访问令牌。
协议流程
首先看一张来自官方提供的流程图:
+--------+ +---------------+
| |--(1)- Authorization Request ->| Resource |
| | | Owner |
| |<-(2)-- Authorization Grant ---| |
| | +---------------+
| |
| | +---------------+
| |--(3)-- Authorization Grant -->| Authorization |
| Client | | Server |
| |<-(4)----- Access Token -------| |
| | +---------------+
| |
| | +---------------+
| |--(5)----- Access Token ------>| Resource |
| | | Server |
| |<-(6)--- Protected Resource ---| |
+--------+ +---------------+
复制代码
这是一张关于OAuth2角色的抽象交互流程图,主要包含如下的6个步骤:
为了获取访问令牌,客户端必须获取到资源全部者的受权许可。OAuth2默认定了四种受权类型,固然也提供了用于定义额外的受权类型的扩展机制。默认的四种受权类型为:
下面对经常使用的受权码类型和密码类型进行详细的介绍。
受权码类型
受权码类型(authorization code)经过重定向的方式让资源全部者直接与受权服务器进行交互来进行受权,避免了资源全部者信息泄漏给客户端,是功能最完整、流程最严密的受权类型,可是须要客户端必须能与资源全部者的代理(一般是Web浏览器)进行交互,和可从受权服务器中接受请求(重定向给予受权码),受权流程以下:
+----------+
| Resource |
| Owner |
| |
+----------+
^
|
(2)
+----|-----+ Client Identifier +---------------+
| -+----(1)-- & Redirection URI ---->| |
| User- | | Authorization |
| Agent -+----(2)-- User authenticates --->| Server |
| | | |
| -+----(3)-- Authorization Code ---<| |
+-|----|---+ +---------------+
| | ^ v
(1) (3) | |
| | | |
^ v | |
+---------+ | |
| |>---(4)-- Authorization Code ---------' |
| Client | & Redirection URI |
| | |
| |<---(5)----- Access Token -------------------'
+---------+ (w/ Optional Refresh Token)
复制代码
密码类型
密码类型(resource owner password credentials)须要资源全部者将密码凭证交予客户端,客户端经过本身持有的信息直接向受权服务器获取受权。在这种状况下,须要资源全部者对客户端高度可信任,同时客户端不容许保存密码凭证。这种受权类型适用于可以获取资源全部者的凭证(credentials)(如用户名和密码)的客户端。受权流程以下:
+----------+
| Resource |
| Owner |
| |
+----------+
v
| Resource Owner
(1) Password Credentials
|
v
+---------+ +---------------+
| |>--(2)---- Resource Owner ------->| |
| | Password Credentials | Authorization |
| Client | | Server |
| |<--(3)---- Access Token ---------<| |
| | (w/ Optional Refresh Token) | |
+---------+ +---------------+
复制代码
令牌刷新
客户端从受权服务器中获取的访问令牌(access token)通常是具有失效性的,在访问令牌过时的状况下,持有有效用户凭证的客户端能够再次向受权服务器请求访问令牌,可是若是不持有用户凭证的客户端能够经过和上次访问令牌一同返回的刷新令牌(refresh token)向受权服务器获取新的访问令牌。
HTTP 协议是无状态的协议。一旦数据交换完毕,客户端与服务器端的链接就会关闭,再次交换数据须要创建新的链接。这就意味着服务器没法从链接上跟踪会话。
会话,指用户登陆网站后的一系列动做,好比浏览商品添加到购物车并购买。会话(Session)跟踪是 Web 程序中经常使用的技术,用来跟踪用户的整个会话。经常使用的会话跟踪技术是 Cookie 与 Session。
Cookie 其实是一小段的文本信息。客户端请求服务器,若是服务器须要记录该用户状态,就使用 response 向客户端浏览器颁发一个 Cookie。客户端会把 Cookie 保存起来。
当浏览器再请求该网站时,浏览器把请求的网址连同该 Cookie 一同提交给服务器。服务器检查该 Cookie,以此来辨认用户状态。服务器还能够根据须要修改 Cookie 的内容。
Session 是另外一种记录客户状态的机制,不一样的是 Cookie 保存在客户端浏览器中,而 Session 保存在服务器上。客户端浏览器访问服务器的时候,服务器把客户端信息以某种形式记录
在服务器上。这就是 Session。客户端浏览器再次访问时只须要从该 Session 中查找该客户的状态就能够了。
每一个用户访问服务器都会创建一个 session,那服务器是怎么标识用户的惟一身份呢?事实上,用户与服务器创建链接的同时,服务器会自动为其分配一个 SessionId。
简单来讲,Cookie 经过在客户端记录信息肯定用户身份,Session经过在服务器端记录信息肯定用户身份。
某些站点看到容许以 OpenID 的方式登录,如使用 Facebook 帐号或者 Google 帐号登录站点。
OpenID 和 OAuth 很像。但本质上来讲它们是大相径庭的两个东西:
JWT,JSON Web Token,做为一个开放的标准,经过紧凑(compact,快速传输,体积小)或者自包含(self-contained,payload中将包含用户所需的全部的信息,避免了对数据库的屡次查询)的方式,定义了用于在各方之间发送的安全JSON对象。
为何要介绍JWT,由于JWT能够很好的充当在上一节介绍的访问令牌(access token)和刷新令牌(refresh token)的载体,这是Web双方之间进行安全传输信息的良好方式。当只有受权服务器持有签发和验证JWT的secret,那么就只有受权服务器能验证JWT的有效性以及发送带有签名的JWT,这就惟一保证了以JWT为载体的token的有效性和安全性。
JWT的组成
JWT格式通常以下:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiY2FuZyB3dSIsImV4cCI6MTUxODA1MTE1NywidXNlcklkIjoiMTIzNDU2In0.IV4XZ0y0nMpmMX9orv0gqsEMOxXXNQOE680CKkkPQcs
它由三部分组成,每部分经过.
分隔开,分别是:
接着咱们对每一部分进行详细的介绍。
Header
头部一般由两部分组成:
一个简单的头部例子以下:
{
"alg": "HS256"
"typ": "JWT"
}
复制代码
而后这部分JSON会被Base64Url编码用于构成JWT的第一部分:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
Playload
有效负载是JWT的第二部分,是用来携带有效信息的载体,主要是关于用户实体和附加元数据的声明,由如下三部分组成:
通常不建议在payload中添加任何的敏感信息,由于Base64是对称解密的,这意味着payload中的信息的是可见的。
一个简单的有效负荷例子:
{
"name": "cang wu",
"exp": 1518051157,
"userId": "123456"
}
复制代码
这部分JSON会被Base64Url编码用于构成JWT的第二部分:
eyJuYW1lIjoiY2FuZyB3dSIsImV4cCI6MTUxODA1MTE1NywidXNlcklkIjoiMTIzNDU2In0
Signature
要建立签名,必须须要被编码后的头部、被编码后的有效负荷、一个secret,最后经过在头部的定义的加密算法alg加密生成签名,生成签名的伪代码以下:
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
复制代码
用到的加密算法为HMACSHA256
secret
是保存在服务端用于验证JWT以及签发JWT,因此必须只由服务端持有,不应流露出去。
一个简单的签名以下:
IV4XZ0y0nMpmMX9orv0gqsEMOxXXNQOE680CKkkPQcs
这将成为JWT的第三部分。
最后这三部分经过.分割,组成最终的JWT,以下所示:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiY2FuZyB3dSIsImV4cCI6MTUxODA1MTE1NywidXNlcklkIjoiMTIzNDU2In0.IV4XZ0y0nMpmMX9orv0gqsEMOxXXNQOE680CKkkPQcs
通过以上的简单介绍,咱们已经了解了目前常见的统一认证与鉴权的方案,接下来咱们将基于 OAuth2 协议和 JWT 实现一套简单的认证和受权系统。系统主要由两个服务组成,受权服务器和资源服务器,它们之间的交互图 11-4 所示:
客户端想要访问资源服务器中用户持有的资源信息,首先须要携带用户凭证向受权服务器请求访问令牌。受权服务器在验证过客户端和用户凭证的有效性后,它将返回生成的访问令牌给客户端。接着客户端携带访问令牌向资源服务器请求对应的用户资源,在资源服务器经过受权服务器验证过访问令牌有效后,将返回对应的用户资源。
不少时候,受权服务器和资源服务器是合二为一,便可以颁发访问令牌,也对用户资源受限访问;也能够将它们的职责划分得更加详细,受权服务器主要负责令牌的颁发和令牌的验证,而资源服务器负责对用户资源进行保护,仅容许持有有效访问令牌的请求访问受限资源。
受权服务器的主要职责有颁发访问令牌和验证访问令牌,对此咱们须要对外提供两个接口:
通常来说,每个客户端均可觉得用户申请访问令牌,所以一个有效的访问令牌是和客户端、用户绑定的,这表示某一用户授予某一个客户端访问资源的权限。
咱们接下来实现的受权服务器主要包含如下模块,如图 11-5 所示:
鉴于篇幅所限,咱们的受权服务器仅提供密码类型获取访问令牌,可是提供了简便的可扩展的机制,读者能够根据本身的须要进行扩展实现。
用户服务和客户端服务的做用类型,都是根据对应的惟一标识加载用户和客户端信息,用于接下来的用户信息和客户端信息的校验。咱们定义的用户信息和客户端信息结构体以下:
type UserDetails struct {
// 用户标识
UserId int
// 用户名 惟一
Username string
// 用户密码
Password string
// 用户具备的权限
Authorities []string
}
// 验证用户名和密码是否匹配
func (userDetails *UserDetails)IsMatch(username string, password string) bool {
return userDetails.Password == password && userDetails.Username == username
}
type ClientDetails struct {
// client 的标识
ClientId string
// client 的密钥
ClientSecret string
// 访问令牌有效时间,秒
AccessTokenValiditySeconds int
// 刷新令牌有效时间,秒
RefreshTokenValiditySeconds int
// 重定向地址,受权码类型中使用
RegisteredRedirectUri string
// 可使用的受权类型
AuthorizedGrantTypes []string
}
// 验证 clientId 和 ClientSecret 是否匹配
func (clientDetails *ClientDetails) IsMatch(clientId string, clientSecret string) bool {
return clientId == clientDetails.ClientId && clientSecret == clientDetails.ClientSecret
}
复制代码
除了它们具有的基本信息,还提供了 #IsMatch 方法用于验证帐号信息和密码是否匹配的 方法。因为咱们的信息都是明文存储的,因此直接比较信息是否相等便可,也能够根据项目的需求,在其中使用一些加密算法,避免敏感信息明文存储。
UserDetailsService 和 ClientDetailService 服务都仅提供一个方法,用于根据对应的标识加载信息,接口定义以下所示:
type UserDetailsService interface {
// 根据用户名加载用户信息
GetUserDetailByUsername(username string)(*UserDetails, error)
}
type ClientDetailService interface {
// 根据 clientId 加载客户端信息
GetClientDetailByClientId(clientId string) (*ClientDetails, error)
}
复制代码
用户信息和客户端信息能够来源多处,咱们能够从数据库中、缓存中甚至经过 RPC 的方式从其余用户微服务中加载。
本文主要介绍了微服务架构中的统一认证与受权相关概念,以及受权服务器实现涉及到的结构体和服务接口。TokenGrant 令牌生成器和 TokenService 令牌服务以及其余的实现将会在下篇介绍。