2019独角兽企业重金招聘Python工程师标准>>>
OAuth2.0协议原理与实现系列
- (一) 协议原理
- (二) TOKEN生成算法
- (三) 协议实现
OAuth2.0协议定义了授权详细流程,并最终以token的形式作为用户授权的凭证下发给客户端,客户端后续可以带着token去请求资源服务器,获取token权限范围内的用户资源。
对于token的描述,OAuth2.0协议只是一笔带过的说它是一个字符串,用于表示特定的权限、生命周期等,但是却没有明确阐述token的生成策略,以及如何去验证一个token。RFC6749对于access token的描述:
The client obtains an access token -- a string denoting a specific scope, lifetime, and other access attributes.
协议不去详细阐述token的生成和验证过程,个人觉得是因为这一块各个业务都有自己的特点,无法完全做到抽象,并且在这一块去做详细的规定,其意义并不大。Token本质上就是对用户授权这一操作在时间和权限范围两个维度上的一个表征,协议可以对token的传递和基本验证做相应规定,但是具体的一个token包含哪些元素,采用什么样的生成算法还是需要由自己去把握。
本文主要讲解自己对于token生成的一些思考,以及介绍两种类型的token:BEARER类型和MAC类型。
一. TOKEN的基本构成
Token表征了用户授权这一操作,授权服务器通过下发token来给客户端颁发获取用户受保护资源的资格,且不会因此而泄露用户的登录凭证信息。Token对于客户端应该是非透明的,客户端只知道这是一个字符串,能够用它来获取用户的受保护资源,对于字符串内部所含的信息应该无从知晓,也不能通过其它方法去解密其中的信息。所以token应该是一类对称加密得到的字符串,并且只有授权服务器持有对称密钥,用于对生成的token进行加密和验证。
对于构成token的元素,各个业务都有自己的需求,不过仍然存在一些基本通用的元素,比如:
clientId:客户端ID,当前token隶属的客户端
userId:用户的ID,表示当前token来自哪个用户授权
scope: 权限范围,该token允许换取的用户受保护资源范围
issueTime: 下发时间,用于控制token的生命周期
tokenType: token的类型,不同类型可能会采用不同的验证措施
以上是我个人根据经验总结的一些基础的token组成元素,具体业务还可以根据具体的需求添加一些其他的元素。
二. Bearer Type Access Token
BEARER类型的token是在RFC6750中定义的一种token类型,OAuth2.0协议RFC6749对其也有所提及,算是对RFC6749的一个补充。BEARER类型token是建立在HTTP/1.1版本之上的token类型,需要TLS(Transport Layer Security)提供安全支持,该协议主要规定了BEARER类型token的客户端请求和服务端验证的具体细节。
2.1 客户端请求
客户端在携带token请求用户的受保护资源时,需要保证token的安全性,以防止token被窃取或篡改,从而损害用户数据安全。BEARER类型token定义了三种token传递策略,客户端在传递token时必须使用其中的一种,且最多一种。
2.1.1 放在Authorization
请求首部
Authorization首部说明
Authorization首部是由客户端发送,以向服务器回应自己的身份验证信息,客户端在收到服务器的401 Authentication Required响应之后,需要在请求中包含该首部。
基本用法:Authorization:
在传输时,Authorization
首部的authentication-scheme
需要设置为Bearer
,请求示例:
GET /resource HTTP/1.1
Host: server.example.com
Authorization: Bearer mF_9.B5f-4.1JqM
2.1.2 放在请求实体中
Token需放置在access_token
参数后面,且Content-Type
需要设置为application/x-www-form-urlencoded
,请求示例如下:
POST /resource HTTP/1.1
Host: server.example.com
Content-Type: application/x-www-form-urlencoded
access_token=mF_9.B5f-4.1JqM
协议推荐使用第一种方式,对于该请求方式,必须在满足如下条件时才允许使用:
The HTTP request entity-header includes the "Content-Type" header field set to "application/x-www-form-urlencoded".
The entity-body follows the encoding requirements of the "application/x-www-form-urlencoded" content-type as defined by HTML 4.01.
The HTTP request entity-body is single-part.
The content to be encoded in the entity-body MUST consist entirely of ASCII characters.
The HTTP request method is one for which the request-body has defined semantics. In particular, this means that the "GET" method MUST NOT be used.
2.1.3 放在URI请求参数中
该方式通过在请求URl后面添加access_token
参数来传递token,请求示例如下:
GET /resource?access_token=mF_9.B5f-4.1JqM HTTP/1.1
Host: server.example.com
客户端在请求时需要设置Cache-Control: no-store
,服务端在成功响应时也需要设置Cache-Control: private
。
由于很多服务都会以日志方式去记录用户的请求,此类方式存在较大的安全隐患,所以一般不推荐使用,除非前两种方案均不可用。
2.2 服务端验证
如果服务端拒绝客户端的访问请求,则需要在响应中添加WWW-Authenticate
首部,响应示例如下:
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="example"
WWW-Authenticate首部说明
WWW-Authenticate首部用于401 Unauthorized响应,用于向客户端发送一个质询认证方案。
基本用法:WWW-Authenticate:
这里的响应,其中auth-scheme
必须设置为Bearer
,如果客户端携带了无效的token,那么按照上一篇《OAuth2.0协议原理与实现:协议原理》讲解的,OAuth2.0协议要求错误响应中必须携带error
字段,并选择性携带error_description
和error_uri
,具体释义请参考上一篇,响应示例如下:
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="example",
error="invalid_token",
error_description="The access token expired"
三. MAC Type Access Token
前面介绍了BEARER类型的token,RFC6750明确说明该类型token需要TLS(Transport Layer Security)提供安全支持。虽然现今大部分站点都已经或正在由HTTP向HTTPS迁移,但是仍然会有站点继续在使用HTTP,在这类站点中BEARER类型的token存在安全隐患,这个时候MAC类型的token正是用武之地,MAC类型的token设计的主要目的就是为了应对不可靠的网络环境。
MAC类型相对于BEARER类型对于用户资源请求的区别在于,BEARER类型只需要携带授权服务器下发的token即可,而对于MAC类型来说,除了携带授权服务器下发的token,客户端还要携带时间戳,nonce,以及在客户端计算得到的mac值等信息,并通过这些额外的信息来保证传输的可靠性。
3.1 下发MAC类型令牌
OAuth2.0协议在规定下发accessToken时,包含access_token
,token_type
,expires_in
、refresh_token
,以及scope
字段,其中部分字段可选,具体参见上一篇《OAuth2.0协议原理与实现:协议原理》,示例如下:
HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
Cache-Control: no-store
Pragma: no-cache
{
"access_token":"2YotnFZFEjr1zCsicMWpAA",
"token_type":"example",
"expires_in":3600,
"refresh_token":"tGzv3JOkF0XG5Qx2TlKWIA",
"example_parameter":"example_value"
}
响应字段是可扩展的,对于MAC类型token则增加了mac_key
和mac_algorithm
两个字段,mac_key
是一个客户端和服务端共享的对称密钥,mac_algorithm
则指明了加密算法(比如hmac-sha-1,hmac-sha-256),示例响应如下:
HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: no-store
Pragma: no-cache
{
"access_token":"SlAV32hkKG",
"token_type":"mac",
"expires_in":3600,
"refresh_token":"8xLOxBtZp8",
"mac_key":"adijq39jdlaska9asud",
"mac_algorithm":"hmac-sha-256"
}
3.2 构造MAC类型请求
一些开放API接口可能会强制要求以MAC类型令牌来请求,这个时候就需要在客户端构造合法的请求,一个标准的请求示例如下:
GET /resource/1?b=1&a=2 HTTP/1.1
Host: example.com
Authorization: MAC id="h480djs93hd8",
ts="1336363200",
nonce="dj83hs9s",
mac="bhCQXTVyfj5cmA9uKkPFx1zeOXM="
请求参数说明:
参数名 | 必须 | 描述信息 |
---|---|---|
id | 必须 | 访问令牌 |
ts | 必须 | 时间戳 |
nonce | 必须 | 客户端生成的字符串,对于相同token和timespan的请求nonce必须相同 |
ext | 可选 | 扩展信息 |
mac | 必须 | 根据MAC key和MAC algorithm计算出来的值 |
通过添加id、ts、nonce、mac
字段到Authorization
请求首部以发起对用户资源的请求,这里的id
就是授权服务器下发的accessToken;ts
则是时间戳,由客户端生成,以秒为单位;nonce
是客户端生成的一个字符串形式的签名,是对ts和id两个维度的唯一、可重复性标识;而mac
则是整个客户端构造最核心和复杂的部分,可以看做是对本次请求参数的一个签名,1.2.3小节专门讲解。此外客户端还以用ext
字段来携带一些扩展数据。
3.3 mac值算法
mac值可以看作是对本次请求参数的一个签名,通过对请求数据进行本地加密计算得到,用于防止请求过程中参数被更改。服务器端收到请求之后,会以相同的算法和密钥重新计算一遍mac值,并与客户端传递过来的作比较,如果不一致则拒绝该请求。因为密钥仅保存在客户端和服务端本地,所以无需担心mac值被更改或伪造,从而确保在没有TLS保证的环境下可靠传输,实际上这里可以看做是MAC类型请求自己实现了一遍TLS。
mac值对于相同的请求参数必须是一致和可再计算的,对于参与计算元素的选择,协议选取了如下元素:
The timestamp value calculated for the request.
The nonce value generated for the request.
The HTTP request method in upper case. For example: HEAD, GET, POST, etc.
The HTTP request-URI as defined by RFC2616 section 5.1.2.
The hostname included in the HTTP request using the Host request header field in lower case.
The port as included in the HTTP request using the Host request header field. If the header field does not include a port, the default value for the scheme MUST be used (e.g. 80 for HTTP and 443 for HTTPS).
The value of the ext Authorization request header field attribute if one was included in the request, otherwise, an empty string.
通过对这些元素按照顺序组织,并以换行符\n
作分隔(最后一行也需要包含一个\n
),利用mac_algorithm
指定的算法和mac_key
指定的密钥对组织好的数据进行加密计算得到mac值。
计算示例:
假设有一个请求:
POST /request?b5=%3D%253D&a3=a&c%40=&a2=r%20b&c2&a3=2+q HTTP/1.1
Host: example.com
Hello World!
其中ts=264095:7d8f3e4a
,nonce=7d8f3e4a
,ext=a,b,c
。
对该请求按照之前的说明进行组织,以\n
分隔得到:
264095\n
7d8f3e4a\n
POST\n
/request?b5=%3D%253D&a3=a&c%40=&a2=r%20b&c2&a3=2+q\n
example.com\n
80\n
a,b,c\n
其中\n
仅仅是为了展示,实际中以ASCII码%x0A
的意义表示,不要忘了最后一行的\n
。假设授权服务器指定的mac_algorithm
为hmac-sha-1,令text
表示上面的字符串,那么最后的mac值得计算方式如下:
mac = hmac-sha-1(mac_key, text)
3.4 服务端验证
服务器端在收到客户端的请求之后,需要做如下验证:
重新计算mac值,并与客户端传递的值进行比较
确保(timestam, nonce, token)三个维度之前没有被请求过,以防止重放攻击
验证scope,以及token
如果服务端拒绝客户端的请求,则需要指定WWW- Authenticate
响应首部,例如客户端携带了无效的授权信息,则服务器响应示例如下:
HTTP/1.1 401 Unauthorized
WWW-Authenticate: MAC error="The MAC credentials expired"
四. 本篇小结
本篇主要介绍了两种token类型,基本可以覆盖实际应用中的各种场景。Token是对用户授权操作的一类凭证,一旦下发到客户端,其安全性就需要客户端去保证,为了尽量在保护用户数据和提升用户体验上寻找一个平衡点,token的生命周期不应该设置的太短或太长,一般可以以月为单位,并推荐走授权码授权,请求下发刷新令牌,刷新令牌的生成和验证也可以参考本文的两种类型。
本篇和上一篇《OAuth2.0协议原理与实现:协议原理》介绍了OAuth2.0协议涉及到的理论知识,如果你已经看得手痒了,那么请期待下一篇吧,在下一篇中我将以java语言述写OAuth2.0的具体实现!
文中不免有错误之处,欢迎批评指正,如果您觉得写得还不错,让您有所收获,欢迎打赏~
一分钱也是钱,一分钱也是爱,哈哈~
参考文献
- RFC5849 - The OAuth 1.0 Protocol
- RFC6749 - The OAuth 2.0 Authorization Framework
- RFC6750 - The OAuth 2.0 Authorization Framework: Bearer Token Usage
- HTTP Authentication: MAC Authentication (draft-hammer-oauth-v2-mac-token-02)
鉴于作者水平有限,文中不免有错误之处,欢迎大家批评指正~
同步更新,我的个人小站:www.zhenchao.org