客户端

  • client_id: 客户端标识字符串,由授权服务器分配,形式可采用开发者门户,动态客户端注册机等

  • client_secret: 保密客户端的共享密钥,用于与授权服务器交互时对自身进行身份认证,由授权服务器分配

其它配置:

  • 重定向 url
1
/callback
  • 授权端点

  • 令牌端点

  • 资源服务器, 受保护的资源端点

授权码许可方式获取访问令牌

  1. 将用户重定向至授权服务器的授权端点,并在请求的 URL 中包含适当的查询参数,如请求权限范围,重定向 URL (回调 URL, 即授权服务器返回授权码给客户端的端点),client_id 等。
1
2
3
4
5
6
7
8
# `code` 授权码许可类型
response_type: 'code',

# 客户端标识 ID
client_id: client.client_id,

# 客户端接收授权码的端点
redirect_uri: client.redirect_uris[0]
  1. 在授权服务器授权端点,用户对客户端进行授权(可能包含必要用户身份认证),授权结束,根据客户端传过来的重定向 URL 重定向至客户端端点, 并且携带了授权服务器生成的授权码
  1. 客户端拿到授权码,使用 POST 请求,并使用 Http Basic 认证(client_id作为用户名,client_secret 作为密码),将授权码以表单形式放入请求体中,发送至服务器令牌端点
1
2
3
4
5
6
// 请求头部,使用表单,使用 Http Basic 认证
var headers = {
'Content-Type': 'application/x-www-form-urlencoded',
'Authorization': 'Basic ' + encodeClientCredentials(client.client_id,
client.client_secret)
};
1
2
3
4
5
6
7
8
9
10

# 授权码许可类型
grant_type: 'authorization_code',

# 授权码
code: code,

# 此处不需要执行重定向
# 这里是 OAuth 规范约定必须指定和请求授权时相同的重定向 url
redirect_uri: client.redirect_uris[0]
  1. 请求成功时,授权服务器返回一个包含访问令牌等信息的 JSON 对象
1
2
3
4
{
"access_token": "987tghjkiu6trfghjuytrghj",
"token_type": "Bearer" // Bearer 令牌,持有该类型的令牌,就可以向受保护资源出示
}
  1. 客户端解析 JSON 对象,获取访问令牌值,并将其保存起来,以便以后使用

使用 state 参数添加跨站保护

为了避免恶意调用客户端重定向 URL 端点 (client.redirect_uris[0]),破解令牌,请求授权端点前,客户端生成一个随机字符串,并添加在请求授权端点的请求参数中 (state),授权端点在返回授权码时,会带着这个字符串,客户端进行比较,如果相同,表明是自己之前发送的授权请求,否则,应向客户提示异常。

使用令牌访问受保护资源

bearer 令牌,它意味着无论是谁,只要持有该令牌就可以向受保护资源出示

使用令牌向受保护资源发出调用请求,令牌的使用的方式有三种:

  1. HTTP Authorization 头部(OAuth 规范推荐)
1
'Authorization': 'Bearer ' + access_token
  1. 使用表单格式的请求体参数 (必须使得资源端点支持 POST 请求和表单)

  2. 使用 URL 编码的查询参数 (可能使得令牌暴漏在请求日志中)

刷新访问令牌

无需用户参与下获取新访问令牌的方法

步骤

1.授权服务器的令牌端点会将刷新令牌与访问令牌一起返回给客户端

1
2
3
4
5
{
"access_token": "987tghjkiu6trfghjuytrghj",
"token_type": "Bearer",
"refresh_token": "j2r3oj32r23rmasd98uhjrk2o3i"
}

2.客户端发送访问令牌失效时(使用令牌获取资源失败),使用刷新令牌重新请求访问令牌

1
2
3
# POST 请求, 同时头部也要添加 Basic 认证信息,并使用表单形式
grant_type: 'refresh_token',
refresh_token: refresh_token

如果刷新令牌有效,返回新的访问令牌和刷新令牌

1
2
3
4
5
{
"access_token": "IqTnLQKcSY62klAuNTVevPdyEnbY82PB",
"token_type": "Bearer",
"refresh_token": "j2r3oj32r23rmasd98uhjrk2o3i"
}

如果刷新令牌无效,提示错误, 回到最开始没有获取访问令牌的情况,提示用户重新进行获取与令牌的授权

受保护的资源端

使用 OAuth 保护 Web API

  1. 从传入的请求中解析出令牌
  2. 通过授权服务器验证令牌
  3. 根据令牌的权限范围做出响应,令牌的权限范围有多种

资源服务器如何对请求中传过来的令牌进行验证?

  1. 授权服务器与资源服务器共享令牌数据库
  2. 使用令牌内省(token introspection)的 Web 协议,由授权服务器提供接口,让资源服务器能在运行时检查令牌的状态
  3. 令牌内包含资源服务器能直接解析并理解的信息,如 JWT, 使用受加密保护的 JSON 对象携带声明信息

权限范围 scope

  1. 不同的权限范围对应不同的操作,如 GET\POST\DELETE 分别对应的 read write delete 权限

  2. 不同的权限范围对应不同的数据结果,例如,同一个 API 对权限范围不同的客户端返回不同的子集,如有一个销售统计 API, 每个大区只能请求自己大区的数据

  3. 不同的用户对应不同的数据结果

资源服务器可以根据令牌及其附属信息(如权限范围)直接做出授权决策。资源服务器还可以将访问令牌中的权限范围与其他访问控制信息结合起来,用于决定是否响应 API 调用以及响应什么内容。

资源服务器如何设计权限是比较灵活的,而且可以和其他控制信息结合,也是属于灵活的一种体现。客户端绝不会知道授权的用户与具体的权限范围,令牌本身就代表了资源拥有者的授权与权限范文,在资源服务器端,通过验证令牌以及获取令牌对应的访问权限,用户,以及其它控制信息,做出是否有要相应的决策以及相应什么内容。

授权服务器

授权服务器对用户进行身份认证、注册并管理客户端、颁发令牌。

授权

  1. 客户端将用户重定向至授权服务器授权端点 /authorize,并携带了必要的查询参数
1
2
3
4
5
6
7
https://api.weibo.com/oauth2/authorize?
client_id=3129654709& //客户端 id
redirect_uri=https://www.douyu.com/member/oauth/signin/weibo& //重定向客户端 uri
state=96b2a0b5a31566c5f63b2322f987736a& //state 防止恶意调用客户端重定向 uri
response_type=code& //授权码许可类型
approval_prompt=force& //授权提示?
scope=foo bar //客户端期望的权限范围
  1. 授权服务器验证客户端身份(client id),检查传入的 redirect_url 与注册时的是否一致;验证请求的权限范围是否符合要求(提前为每个客户端限定了权限范围),如果都满足要求,生成一个随机字符串 , 并将其与客户端请求的查询参数保存为一个映射;渲染一个授权表单,提供授权或拒绝授权,以及权限范围的选择, 并将随机字符串 ,嵌入到表单中(表示未完成授权,为授权页面提供了简单的防跨站请求伪造保护,防止对服务器端授权端点的恶意请求)
1
2
3
4
5
6
7
8
9
10
11
12
13
// 提交的表单数据
POST /approve HTTP/1.1
Host: localhost:9001
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:39.0)
Gecko/20100101 Firefox/39.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Referer: http://localhost:9001/authorize?response_type=code&scope=foo&client_id=
oauth-client-1&redirect_uri=http%3A%2F%2Flocalhost%3A9000%2Fcallback&state=
GKckoHfwMHIjCpEwXchXvsGFlPOS266u
Connection: keep-alive

//reqid 即授权服务器生成的随机值,approve 表示用户的授权许可
reqid=tKVUYQSM&approve=Approve
  1. 用户同意授权,表单提交到授权码生成端点 /approve,对提交表单的请求验证 (随机值),权限范围进行验证(表单请求可能会被篡改,所以需要再次验证),生成授权码 code,存储在授权服务器上,并保存与之对应的权限范围(在令牌端点可以取出来,与访问令牌形成对应关系),客户端请求,并将其通过客户端请求授权时的重定向 uri 重定向到客户端,并且在查询参数上应用了客户端的 state 值(对客户端提供跨站保护,虽然不要求客户端传递该参数,但是要求授权服务器只要收到该参数就返回它)

令牌颁发

客户端身份认证,client_id, client_secret

授权许可类型 grant_type 检查, 如 authorization_code

如果是授权许可码类型,取出授权许可码 code, 并查询之前保存的 code 与客户端请求信息缓存,验证它确实是为该客户端生成的(对比 client_id)

客户端匹配成功,生成一个访问令牌,并将其保存

向客户端返回访问令牌,令牌类型

1
2
access_token: access_token,
token_type: 'Bearer'

支持刷新令牌,刷新令牌只在授权服务器上使用,访问令牌只在受保护的资源上使用

1
2
refresh_token: refresh_token,
client_id: clientId

授权许可类型

除了授权码许可类型外的许可类型

1 隐式许可类型

直接运行在浏览器内的客户端,此时,客户端没有必要再通过授权码获取访问令牌,因为授权码是通过浏览器(前端信道)发送给客户端的,对于浏览器来说是可见的(如果是 web 应用,是直接发送给客户端服务器的)。直接请求授权端点, 而不是令牌端点来获取访问令牌,其它流程与授权码许可类型相似,客户端 id 检查,授权端点检查权限范围,资源拥有者身份认证,并验证对请求的批准,生成令牌,在授权端点响应中,将令牌附在客户端重定向 URI 片段中 。

1
2
3
4
5
# 隐式许可类型下,客户端访问授权端点时的 response_type
response_type=token

# 授权端点响应重定向时的 URI, 附件了 access_token 和 token_type
GET /callback#access_token=987tghjkiu6trfghjuytrghj&token_type=Bearer

限使用隐式许可类型的限制

  • 无法持有客户端密钥,无法对浏览器隐藏密钥
  • 只使用授权端点,而不是用令牌端点 (不要求客户端在授权端点的身份认证),影响安全等级
  • 不可用于获取刷新令牌,浏览器内的应用运行的特点:短暂,没有必要

2 客户端凭据许可类型

后端系统通信的场景,并不代表某个特定用户,没有明确的资源拥有者。这种场景下,相当于没有用户对客户端授权。

客户端凭据许可类型,只使用后端信道,客户端代表自己从令牌端点获取令牌,此时客户端向授权服务器的令牌端点发出令牌请求。

1
2
3
4
5
6
7
# 客户端身份认证 head:客户端 ID 和密钥
Authorization: Basic b2F1dGgtY2xpZW50LTE6b2F1dGgtY2xpZW50LXNlY3JldC0x

# 客户端凭据许可类型的授权类型
grant_type: 'client_credientials'
# 也可以使用 scope 指定请求的权限范围,与授权码或隐式许可流程在 授权端点 上使用的 scope 参数一样
scope: foo

客户端直接向授权服务器进行身份认证,授权服务器向客户端颁发访问令牌,客户端能随时获取新令牌,无需单独的资源拥有者参与,因此也没有必要使用刷新令牌

特点

  • 客户端凭据类型没有任何直接的用户交互,是为可信的后端系统直接访问服务准备的

    因此对于交互式与非交互式客户端,最好进行区分,指定不同的权限范围

3 资源拥有者凭据类型

客户端通过后端信道使用用户名和密码换取访问令牌, 资源拥有者直接与客户端进行交互, 而不是授权服务器, 只使用令牌端点. 授权服务器验证客户端的身份, 从收到的请求中取出用户名和密码,并与本地存储的用户信息对比, 验证权限范围, 如果都匹配,则授权服务器向客户端颁发令牌, 刷新访问令牌。

1
2
3
4
5
6
7
Authorization: Basic b2F1dGgtY2xpZW50LTE6b2F1dGgtY2xpZW50LXNlY3JldC0x

# 客户端收集用户的用户名和密码,发送至授权服务器的令牌端点
grant_type=password
scope=foo bar
username=alice
&password=secret

虽然这种模式下客户端直接获取了用户的认证信息, 但是后续的请求受保护资源使用的是访问令牌, 比每次请求都直接使用用户凭据要好的多, 但这仍然是一种不不好的许可类型, 请不要在现实中使用.

4 断言许可类型

官方扩展许可类型,客户端得到一条结构化的且被加密保护的信息-断言, 使用断言向授权服务器换取令牌。断言必须由信任的认证机构提供. 这种许可类型只使用后端信道, 没有明确的资源拥有者参与. 断言一般来自第三方, 客户端可以不知道断言本身的含义. 客户端向授权服务器的令牌端点发送 HTTP POST 请求, 将断言作为参数传递给服务器. 客户端一样要进行身份认证

两种标准化的断言: 安全断言标记语言 (SAML); JSON Web Token (JWT)

1
2
3
4
5
6
Authorization: Basic b2F1dGgtY2xpZW50LTE6b2F1dGgtY2xpZW50LXNlY3JldC0x

grant_type: urn:ietf:params:oauth:grant-type:jwt-bearer

# 示例断言
assertion=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6InJzYS0xIn0.eyJpc3MiOiJodHRwOi8vdHJ1c3QuZXhhbXBsZS5uZXQvIiwic3ViIjoib2F1dGgtY2xpZW50LTEiLCJzY29wZSI6ImZvbyBiYXIgYmF6IiwiYXVkIjoiaHR0cDovL2F1dGhzZXJ2ZXIuZXhhbXBsZS5uZXQvdG9rZW4iLCJpYXQiOjE0NjU1ODI5NTYsImV4cCI6MTQ2NTczMzI1NiwianRpIjoiWDQ1cDM1SWZPckRZTmxXOG9BQ29Xb1djMDQ3V2J3djIifQ.HGCeZh79Va-7meazxJEtm07ZyptdLDu_Ocfw82F1zAT2p6Np6Ia_vEZTKzGhI3HdqXsUG3uDILBv337VNweWYE7F9ThNgDVD90UYGzZN5VlLf9bzjnB2CDjUWXBhgepSyaSfKHQhfyjoLnb2uHg2BUb5YDNYk5oqaBT_tyN7k_PSopt1XZyYIAf6-5VTweEcUjdpwrUUXGZ0fla8s6RIFNosqt5e6j0CsZ7Eb_zYEhfWXPo0NbRXUIG3KN6DCA-ES6D1TW0Dm2UuJLb-LfzCWsA1W_sZZz6jxbclnP6c6Pf8upBQIC9EvXqCseoPAykyR48KeW8tcd5ki3_tPtI7vA

授权服务器进行断言解析,检查加密保护, 确定生成何种令牌. 断言可以表示很多不同的信息: 资源拥有者的身份,被允许的权限范围. 授权服务器决定接受哪些断言并为断言的含义制定解释规则. 最终生成访问令牌返回.

基于 OAuth 的身份认证协议:OpenID Connect