OAuth 2.0 简介
OAuth 2.0 的设计背景,在于允许用户在不告知第三方自己的帐号密码情况下,通过授权方式,让第三方服务可以获取自己的资源信息。协议的详细信息,可以参考 RFC 6749 , RFC 6749 中文 。
OAuth 2.0 相对于 1.0 版本在授权模式上做了更多的细化,已定义的授权模式分为四种:
- 授权码模式(Authorization Code Grant)
- 隐式授权模式(Implicit Grant)
- 资源所有者密码凭证模式(Resource Owner Password Credentials Grant)
- 以及客户端凭证模式(Client Credentials Grant)
简单说明 OAuth2 中最经典的 Authorization Code Grant 模式,流程如下:
流程图中,包含四个角色。
- ResourceOwner 为资源所有者,即为用户
- User-Agent 为浏览器
- AuthorizationServer 为认证服务器,可以理解为用户资源托管方,比如企业微信服务端
- Client 为第三方服务
调用流程为:
- 用户访问第三方服务,第三方服务通过构造 OAuth 2.0 链接(参数包括当前第三方服务的身份 ID,以及重定向 URI),将用户引导到认证服务器的授权页。
- 用户查看获取授权的第三方信息,以及需要被授权的资源,选择是否同意授权。
- 若用户同意授权,则认证服务器将用户重定向到第一步指定的重定向 URI,同时附上一个授权码。
- 第三方服务收到授权码,带上授权码来源的重定向 URI,向认证服务器申请凭证。
- 认证服务器检查授权码和重定向 URI 的有效性,通过后颁发 AccessToken(调用凭证)。
4 与 5 的调用为后台调用,不通过浏览器进行。
OAuth 2.0 接入流程标准
测试环境地址:https://uis-sit.cmsk1979.com/security
生产环境地址:
授权码授权模式(Authorization Code Grant)
授权码模式在整个授权流程上与 1.0 版本最贴近,但是整个流程还是要简化了许多,也是 OAuth 2.0 中最标准,应用最广泛的授权模式。
这类授权模式非常适合于具备服务端的应用,当然现在大多数 APP 都有自己的服务端,所以大部分 APP 的 OAuth 授权都可以采取授权码模式,下图为授权码各个角色之间的交互时序(这里让用户直接参与其中,省略了用户代理):
整个授权流程说明如下(具体参数释义见下文):
- 客户端携带 client_id, scope, redirect_uri, state 等信息引导用户请求授权服务器的授权端点下发 code。
- 授权服务器验证客户端身份,验证通过则询问用户是否同意授权(此时会跳转到用户能够直观看到的授权页面,等待用户点击确认授权)。
- 假设用户同意授权,此时授权服务器会将 code 和 state(如果客户端传递了该参数)拼接在 redirect_uri 后面,以 302 形式下发 code。
- 客户端携带 code, redirect_uri, 以及 client_secret 请求授权服务器的令牌端点下发 access_token(这一步实际上中间经过了客户端的服务器,除了 code,其它参数都是在应用服务器端添加,下文会细讲)授权服务器验证客户端身份,同时验证 code,以及 redirect_uri 是否与请求 code 时相同。验证通过后下发 access_token,并选择性下发 refresh_token。
获取授权码
授权码是授权流程的一个中间临时凭证,是对用户确认授权这一操作的一个暂时性的证书,其生命周期一般较短,协议建议最大不要超过 10 分钟,在这一有效时间周期内,客户端可以凭借该暂时性证书去授权服务器换取访问令牌。
请求参数说明:
名称 | 是否必须 | 描述信息 |
---|---|---|
response_type | 必须 | 对于授权码模式 response_type=code |
client_id | 必须 | 客户端ID,用于标识一个客户端,等同于 appId,在注册应用时生成 |
redirect_uri | 可选 | 授权回调地址,传输的回调地址必须和授权服务保存的回调地址一致(参数部分可不一致) |
scope | 可选 | 权限范围,用于对客户端的权限进行控制,如果客户端没有传递该参数,那么服务器则以该应用的所有权限代替 |
state | 推荐 | 用于维持请求和回调过程中的状态,防止 CSRF 攻击,服务器不对该参数做任何处理,如果客户端携带了该参数,则服务器在响应时原封不动的返回 |
请求参数示例:
GET /oauth/authorize?response_type=code&client_id=s6BhdRkqt3&state=xyz&redirect_uri=https://example.com/oauth HTTP/1.1
Host: example.com
客户端携带上述参数请求授权服务器的令牌端点,授权服务器会验证客户端的身份以及相关参数,并在确认用户登录的前提下弹出确认授权页询问用户是否授权。
如果用户同意授权,则会将授权码(code)和 state 信息(如果客户端传递了该参数)添加到回调地址后面,以 302 的形式下发。
成功响应参数说明:
名称 | 描述信息 |
---|---|
code | 授权码,授权码代表用户确认授权的暂时性凭证,只能使用一次,推荐最大生命周期不超过10分钟 |
state | 如果客户端传递了该参数,则会原封不动返回 |
成功响应示例:
HTTP/1.1 302 Found
Location: https://example.com/oauth?code=SplxlOB&state=xyz
下发访问令牌
授权服务器的授权端点在以 302 形式下发 code 之后,用户 User-Agent,比如浏览器,将携带对应的 code 回调请求用户指定的 redirect_url,这个地址应该能够保证请求打到应用服务器的对应接口。该接口可以由此拿到 code,并附加相应参数请求授权服务器的令牌端点,授权端点验证 code 和相关参数,验证通过则下发 access_token。
请求参数说明:
名称 | 是否必须 | 描述信息 |
---|---|---|
grant_type | 必须 | 对于授权码模式 grant_type=authorization_code |
code | 必须 | 上一步获取的授权码 |
redirect_uri | 必须 | 授权回调地址,传输的回调地址必须和授权服务保存的回调地址一致 |
client_id | 必须 | 客户端 ID,用于标识一个客户端,等同于 appId,在注册应用时生成 |
如果在注册应用时有下发客户端凭证信息(client_secret),那么客户端必须携带 Authorization 信息以让授权服务器验证客户端的有效性。针对客户端凭证,不能将其传递到客户端,客户端无法保证凭证的安全,凭证应该始终留在应用的服务器端,当下发 code 回调请求到应用服务器时,在服务器端携带上凭证再次请求下发令牌。
请求参数示例:
POST /oauth/token HTTP/1.1
Host: server.example.com
Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code&code=SplxlOB&redirect_uri=https://client.example.com/oauth&client_id=client_test
添加 Authorization 示例:
import org.apache.http.client.methods.HttpPost; import java.util.Base64; public class Test { private void addHeader() { //向授权服务器申请时提供的client_id String userName = "09853e59-e240-450f-b732-001af3207149"; //向授权服务器申请时提供的client_secret String password = "c7492ba5-f749-480f-bec5-55ab6bdc7161"; HttpPost httpPost = new HttpPost("http://127.0.0.1:8080/oauth/token"); //添加http头信息 httpPost.addHeader("Authorization", "Basic " + Base64.getUrlEncoder() .encodeToString((userName + ":" + password).getBytes())); } }
授权服务器需要验证客户端的有效性,以及是否与之前请求授权码的客户端是同一个(请求授权时的信息可以记录在 code,或以 code 为 key 建立缓存),授权服务器还要保证 code 处于生命周期内(推荐 10 分钟内有效),且只能被使用一次。
授权服务器验证通过之后,生成 access_token,并选择性下发 refresh_token,OAuth 2.0 协议明确了 token 的下发策略,对于生成策略没有做太多说明。
成功响应参数说明:
名称 | 描述信息 |
---|---|
access_token | 访问令牌 |
token_type | 访问令牌类型,比如 bearer,mac 等等 |
expires_in | 访问令牌的生命周期,以秒为单位,表示令牌下发后多久时间过期,如果没有指定该项,则使用默认值 |
refresh_token | 刷新令牌,选择性下发 |
scope | 权限范围,如果最终下发的访问令牌对应的权限范围与实际应用指定的不一致,则必须在下发访问令牌时用该参数指定说明 |
openid | 授权用户唯一标识 |
最后访问令牌以 JSON 格式响应,并要求指定响应头部 Cache-Control: no-store
和 Pragma: no-cache
。
成功响应示例:
HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
Cache-Control: no-store
Pragma: no-cache
{
"access_token":"2YotnFZFEjr1zCsicMWpAA",
"token_type":"bearer",
"expires_in":7200,
"refresh_token":"tGzv3JOkF0XG5Qx2TlKWIA",
"scope":"all",
"openid":"123456"
}
刷新令牌
access_token 是调用授权关系接口的调用凭证,由于 access_token 有效期(目前为 2 个小时)较短,当 access_token 超时后,可以使用 refresh_token 进行刷新,refresh_token 拥有较长的有效期(30 天),当 refresh_token 失效的后,需要用户重新授权。
另外,请求刷新令牌后,原令牌失效。
请求参数说明:
名称 | 是否必须 | 描述信息 |
---|---|---|
grant_type | 必须 | 刷新令牌时 grant_type=refresh_toekn |
refresh_toekn | 必须 | 返回的refresh_token |
client_id | 必须 | 客户端id |
请求参数示例,注意这里和获取令牌一样,如果有下发 client_secret,必须附带 Authorization 信息:
POST /oauth/token HTTP/1.1
Host: server.example.com
Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW
Content-Type: application/x-www-form-urlencoded
grant_type=refresh_toekn&refresh_toekn=tGzv3JOkF0XG5Qx2TlKWIA&client_id=client_test
成功响应参数说明:
名称 | 描述信息 |
---|---|
access_token | 访问令牌 |
token_type | 访问令牌类型,比如 bearer,mac 等等 |
expires_in | 访问令牌的生命周期,以秒为单位,表示令牌下发后多久时间过期,如果没有指定该项,则使用默认值 |
refresh_token | 刷新令牌,选择性下发 |
scope | 权限范围,如果最终下发的访问令牌对应的权限范围与实际应用指定的不一致,则必须在下发访问令牌时用该参数指定说明 |
openid | 授权用户唯一标识 |
成功响应示例:
HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
Cache-Control: no-store
Pragma: no-cache
{
"access_token":"2YotnFZFEjr1zCsicMWpAA",
"token_type":"bearer",
"expires_in":7200,
"refresh_token":"tGzv3JOkF0XG5Qx2TlKWIA",
"scope":"all",
"openid":"123456"
}
资源所有者密码凭证授权模式(Resource Owner Password Credentials Grant)
用户高度信任当前应用,也可以给出自己的用户名/密码,快速获取令牌,本系统不推荐使用这种方式。
下发访问令牌
直接提供用户名密码,获取令牌。
请求参数说明:
名称 | 是否必须 | 描述信息 |
---|---|---|
grant_type | 必须 | 对于授权码模式 grant_type=password |
username | 必须 | 用户名 |
password | 必须 | 用户的密码 |
client_id | 必须 | 客户端ID,用于标识一个客户端,等同于appId,在注册应用时生成 |
scope | 可选 | 权限范围,用于对客户端的权限进行控制,如果客户端没有传递该参数,那么服务器则以该应用的所有权限代替 |
如果有下发 client_secret,必须附带 Authorization 信息。
请求参数示例:
POST /oauth/token HTTP/1.1
Host: server.example.com
Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW
Content-Type: application/x-www-form-urlencoded
grant_type=password&username=abcd&password=123456&client_id=client_test
成功响应参数说明:
名称 | 描述信息 |
---|---|
access_token | 访问令牌 |
token_type | 访问令牌类型,比如 bearer,mac 等等 |
expires_in | 访问令牌的生命周期,以秒为单位,表示令牌下发后多久时间过期,如果没有指定该项,则使用默认值 |
refresh_token | 刷新令牌,选择性下发 |
scope | 权限范围,如果最终下发的访问令牌对应的权限范围与实际应用指定的不一致,则必须在下发访问令牌时用该参数指定说明 |
openid | 授权用户唯一标识 |
最后访问令牌以 JSON 格式响应,并要求指定响应头部 Cache-Control: no-store
和 Pragma: no-cache
。
成功响应示例:
HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
Cache-Control: no-store
Pragma: no-cache
{
"access_token":"2YotnFZFEjr1zCsicMWpAA",
"token_type":"bearer",
"expires_in":7200,
"refresh_token":"tGzv3JOkF0XG5Qx2TlKWIA",
"scope":"all",
"openid":"123456"
}
刷新令牌
同授权码模式刷新令牌。
隐式授权模式(Implicit Grant)
略。(本系统不支持此类授权模式)
客户端凭证授权模式(Client Credentials Grant)
略。(本系统不支持此类授权模式)
发表回复