以实例说明 OAuth2
Table of Contents
1 概述
OAuth2 是互联网中广泛使用的授权标准,常用于实现单点登录、第三方授权。虽然当前有更完善的流程,但国内主要还是使用OAuth标准。国内一些服务商的OAuth是自己修改过的,没有依照标准文档实现,经常发生标准库没办法完成授权的情况,不知意欲何为,使用时还是需要依照服务商的开发文档。
OAuth2 解决的问题是第三方应用授权的问题,也可以用于一个庞大公司内多个系统使用统一账号的情况。它假设系统中授权服务器是独立的,对所有服务的访问必须首先经过授权服务器的授权。本文总是先提出一个系统结构,再考虑使用OAuth2标准解决其中授权的问题。会啰嗦一些,网上有许多简短精炼的介绍,或者直接看rfc6749,都可以满足想要迅速了解 OAuth2标准的人。
Google 有个 OAuth 2.0 Playgroud 可以模拟他家各服务的 oauth2 授权过程。我也做了个 类似的,尝试使用别家 OAuth2 API 时可以使用。
2 Authorization Code Grant
2.1 场景说明:第三方授权
这个场景是 OAuth2 最常出现的场景,以 Github 为例。假设现在有一个第三方服务,他的功能是帮用户定期检查 Github 账户上的 Repo 是否上传了敏感信息,如密钥,数据库口令等。
由于是第三方服务,且服务需要访问用户的 Repo 文件,属于个人信息,Github 和第三方服务都有义务征求用户同意。所以第三方服务需要获得用户授权,才能访问用户的 Repo,而 Github 需要获得用户授权,才能让第三方服务处理用户的资源。
顺序结构如下图。三方服务向 Github 的授权服务器请求授权,授权服务器征求用户同意后,允许三方应用访问资源服务器中用户的 Repo。
2.2 第三方授权解决方案
在解释如何使用 OAuth2 实现这个流程之前,先了解一些 OAuth2 中常见的几个请求/返回参数。
access_token
: OAuth2 流程最后产生的授权码,拥有授权码即可访问资源,通常是 JWT 格式。refresh_token
:access_token
是有时效的,可以使用refresh_token
请求新的access_token
。refresh_token
的生成和验证都在授权服务器上,所以一般不使用 JWT 格式,而是一个在授权服务器中可以查询到具体授权信息和状态的随机字符串。code
: OAuth2 的中间过程,表示用户不久前确认了这个授权,可以通过code
获取access_token
。
Github 的 OAuth2 管理三方登陆方案如下图。
看起来复杂,其实 OAuth2 过程只有两个步骤,这两个步骤最后,三方应用服务器将获得
access_token
。
- 授权请求
一般情况下,用户去往三方应用主页,这里假设主页是 https://thirdapplication.com
,上面写着说明应用功能如何强大有用的陈词滥调,并且有一个按钮或连接,点开后跳转到如下网址。
https://github.com/login/oauth/authorize? response_type=code&client_id=f41154b&state=xyz&redirect_uri=https://thirdapplication.com/examplecb
打开之后一般会询问是否允许三方应用访问你的账户,有时会要求用户登陆 Github 以确认身份。用户确认授权后,返回 302 令浏览器跳转到如下网址, thirdapplication.com
是三方应用服务器的地址。
https://thirdapplication.com/examplecb?code=WxSbIA&state=xyz
浏览器跳转后,实际是跳转到了三方服务器,并且把 code
告诉了三方应用服务器。
- 获取
access_token
第一步结束时,三方应用服务器获得了 code
,使用 code
作为参数请求
https://github.com/login/oauth/access_token
获得 access_token
, 这样就可以通过 access_token
访问 Github 的 API 获取用户信息,获取 Repo 信息了。
上述步骤涉及到几个参数,这里详细说明。
client_id 和 client_secret
这两个参数是 Github 提供的,去 创建一个应用 后即生成。授权请求步骤中,Github 需要 client_id 来标识现在请求的是哪个应用的访问权限。获取
access_token
步骤中, Github 需要client_secret
来验证这个三方服务器是不是伪造的。开发过程中千万注意保密client_secret
。redirect_uri
redirect_uri 出现在授权请求步骤和获取
access_token
步骤中。表示授权成功后,使用 302 跳转将code
发到哪个网址。标准要求验证这个参数,所以这个参数在认证服务器里是保存的。 Github 上创建应用时也要求输入一个 Authorization callback URL,也就是 redirect_uri。我觉得既然认证服务端保存了这个地址, 请求参数中的 redirect_uri 是没有必要的,但既然是标准就要遵守,大家都没法连了。state
状态参数,302跳转时原样设置到参数中,可以把 session 写在这里。
3 Implicit Grant
3.1 应用场景:不需要经过三方应用服务端访问资源
假设不需要通过三方服务器,那么三方服务器也不必获得 access_token
, 只要三方应用客户端获取 token 就可以了。
3.2 第三方授权解决方案
可以使用 OAuth2 的简化模式(Implicit Grant)。微软 提供类似于简化模式的授权,但实际他是 OpenID Connect。google 倒是有完整的简化模式,文档说是为客户端Web应用提供的授权方式。跟我们的需求匹配。
简化模式只有1个步骤就可以获得 access_token,请求地址:
https://accounts.google.com/o/oauth2/v2/auth?scope=https://www.googleapis.com/auth/drive.metadata.readonly&response_type=token&state=xyZk&redirect_uri=https://oauth2.example.com/code&client_id=client_id
然后返回跳转 302 地址中包含 access_token:
https://oauth2.example.com/callback#access_token=4/P7q7W91&token_type=Bearer&expires_in=3600
注意跳转地址的参数是 #
与路径分隔,而不是 ?
。这个 #
后面的参数是不会发往服务器的,所以只有客户端获得了 access_token
。
另外,这个 302 跳转中的参数是没有 refresh_token 的,access_token 超时时间是1天,这么说都够了吧。如果时间还是不够,可以尝试在页面上使用一个隐藏的 iframe 打开第一步的认证链接,由于 cookie 的存在,认证应该是直接通过并返回 access_token
的。但也需要看认证服务器是否这么实现了。
4 Extension Grants
4.1 应用场景: 用户自己开发的应用,不想登陆账号授权,就想直接用
我自己也经常开发这类应用,比如说扫描思想健康的网站的磁力连接,下载后将较大的视频文件压缩加密保存到 google drive 里。需要查看时再用自己开发的客户端下载解密播放。
4.2 授权解决方案
OAuth2 有直接拿用户名和密码当作参数去请求 access_token 的 Resource Owner Password Credentials Grant 模式。即使不提安全性,我觉得这个方案很不好用:我们公司之前使用一个公司邮箱账号发送运维告警邮件,就是设置的 smtp 账号和密码,后来 ISO 27001 审核组要求三个月修改一次邮箱密码,密码改了之后没改运维系统,我们的告警邮件发不出去,挂了几个月都没人发现。我读书少,这个 Resource Owner Password Credentials Grant 模式还没见人用过。
OAuth2 也有 Client Credentials Grant 模式,只拿 client_id 和 client_secret 来验证。比账号密码强一点点,我也没见人用过。国内常用的方式是直接提供token并要求三方服务对请求作一系列密码学操作,比如 阿里云API 就提供AccessKeyID 和 AccessKeySecret,构造签名后再直接请求业务服务器。
Google 使用 OAuth2 的 Extension Grants 模式,他们称作
Service Accounts, 使用他们提供的密钥,构造JWT来请求 access_token
,再用
access_token
来访问资源服务器。例如:
POST /token HTTP/1.1 Host: oauth2.googleapis.com Content-Type: application/x-www-form-urlencoded grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&assertion=JWT-XXXXXXXXXX
返回
{ "access_token": "1/8xbJqaOZXSUZbHLl5EOtu1pxz3fmmetKx9W8CV4t79M", "scope": "https://www.googleapis.com/auth/prediction" "token_type": "Bearer", "expires_in": 3600 }
这样就可以使用 access_token
访问自己账户的资源了。
OAuth2 流程虽然标准并且不复杂,但具体实现还是挺繁琐的。每个 token 都有时效,需要在时效过期之前使用 refresh_token
刷新 access_token
,给本来可以简单 curl
或者 Postman 的流程增加了不少步骤,有条件的公司基本都会提供 SDK,在SDK中帮忙实现了认证流程,不需要重复实现。没有条件提供多种语言 SDK 的话,说服一些固执的合作伙伴按照
OAuth2 的文档实现授权再调用接口,并定期刷新 token ,极难。最后经常把数据结果保存到独立的数据库实例中,再把数据库账号给合作伙伴,这是他们专家提供的方案,大概也很靠谱吧。