OAuth
OAuth 定义
OAuth 2.0(开放授权)是一个开放标准,允许用户让第三方应用通过access_token(令牌)来访问该用户在某一网站上(微信、微博、Github)存储的私密的资源(如用户名、邮箱、联系人列表),而无需将账号和密码提供给第三方应用。
因为使用账号和密码登录时,账号和密码等信息都会保存在第三方应用后台(可能被盗取、泄露),所以当我们不信任第三方应用时,我们可以选择以令牌授权的形式让第三方应用获取个人信息,登录第三方应用平台。
OAuth 的角色及流程
OAuth 中的角色
为了方便说清楚OAuth的运行流程,需要先了解 OAuth 流程中的这几个角色 :
Resource Owner:资源所有者,本文中又称"用户"(user)。Client:客户端,既“第三方应用”。Authorization server:授权服务器,即服务提供商专门用来处理授权的服务器。Resource server:资源服务器,即服务提供商存放用户生成的资源的服务器,它与授权服务器,可以是同一台服务。
OAuth的作用就是让"客户端"安全可控地获取"用户"的授权,从“资源服务器”中获取用户的基本信息。
OAuth 运行流程

图:OAuth授权流程
如上图所示:
- 客户端向用户请求权限。
- 用户点击授权后,客户端收到授权许可。
- 客户端使用上一步获得的授权向授权服务器获取令牌。
- 授权服务器将令牌发送给客户端。
- 客户端获得令牌后,使用令牌向资源服务器获取用户资源。
- 资源服务器返回受保护的资源。
应用场景
为了更好的理解OAuth的运行原理,我们可以通过一个常见的例子来对其进行说明。如下图:

leetcode登录
当使用leetcode刷题时,我们除了可以通过注册使用账号密码登录外,还可以使用Github进行登录。当点击图中Github的Logo时,页面会跳转指向 Github 的 OAuth 授权网址,其URL如下:
https://github.com/login/oauth/authorize?
client_id = 6efe458dfe2230acceea&
redirect_uri = https%3A%2F%2Fleetcode.com%2Faccounts%2Fgithub%2Flogin%2Fcallback%2F&
scope = user%3Aemail&
response_type = code&
state = dIqOqdw4WChR
其中:
client_id参数让Github授权服务器知道是谁在请求。redirect_uri参数是授权后跳转的网址(leetcode)。scope参数表示要求的授权范围(用户个人信息)。response_type参数表示要求返回授权码(code)。state参数会在重定向时作为Query Parameter,开发者可以通过该参数验证请求有效性。state还可以用于防止跨站请求伪造(CSRF)。
第一步,当我们点击Github图标时,网页会跳转到上面URL对应的授权页面,客户端通过授权页面向用户请求权限。如下图所示:

授权页面
第二步,当我们点击Authorize oj-leetcode按钮后,leetcode将获得授权,这时网站就会重定向到redirect_uri参数指定的网址。跳转时,会传回一个授权码,就像下面这样:
https://leetcode.com/accounts/github/login/callback/?code=9275236b97affb3c6cc&state=dIqOqdw4WChR
第三步,leetcode网站拿到授权码以后,就可以在后端向Github网站请求令牌。
https://github.com/login/oauth/authorize?
client_id = cc568d196569c732159c&
client_secret = ddddsssss&
grant_type = authorization_code&
code = 9275236b97affb3c6cc&
redirect_uri = https://leetcode-cn.com/
其中:
client_id和client_secret参数用来确认客户端(leetcode)身份(client_secret参数是保密的,因此只能在后端发请求)。grant_type参数的值是AUTHORIZATION_CODE,表示采用的授权方式是授权码。code参数是上一步拿到的授权码。redirect_uri参数是令牌颁发后的回调网址。
第四步,当Github网站收到请求以后,如果通过验证,就会发送令牌给leetcode。具体做法是向redirect_uri指定的网址,发送一段 JSON 数据。
{
"access_token":"2YotnFZFEjr1zCsicMWpAA",
"token_type":"bearer",
"expires_in":3600,
"refresh_token":"tGzv3JOkF0XG5Qx2TlKWIA",
......
}
其中:
access_token:表示访问令牌,必选项。token_type:表示令牌类型,不区分大小写,必选项,可以是 bearer 类型或 mac 类型。expires_in:表示过期时间,单位为秒。如果省略该参数,必须其他方式设置过期时间。refresh_token:表示更新令牌,用来更新access_token,过期时间长于access_token,可选项。
生成 access_token 的时候也会生成一个 refresh_token,refresh_token 过期时间长于access_token。当令牌过期时,客户端不用再重复上面的步骤,可以调用对应的API用 refresh_token更新 access_token 。
第五步,客户端(leetcode)通过令牌向Github资源服务器发出请求。
第六步,Github资源服务器将Personal user data(用户数据)传回给leetcode。
授权模式
OAuth 2.0 一共分成四种授权模式,四种模式分别应用于不同场景,四种模式为:
- 授权码模式
- 密码模式
- 简化模式
- 客户端模式
授权码模式
授权码模式是最常用的流程,安全性也最高,它适用于那些有后端的 Web 应用。授权码通过前端传送,令牌则是储存在后端,而且所有与资源服务器的通信都在后端完成。这样的前后端分离,可以避免令牌泄漏。
授权码模式获取令牌的流程如下:

授权码模式流程
上文介绍的GitHub便是采用了授权码模式,第三方应用leetcode先申请一个授权码code,然后再用该授权码获取令牌。
除了授权码模式外,还有密码模式、简化模式和客户端模式,这三种模式在本文中将不再介绍,也不推荐使用这三种授权模式。
如果对其它三种授权模式感兴趣,可以通过参考资料中的1和2来学习。
另外,阮一峰在他的博客--GitHub OAuth 第三方登录示例教程中对授权码模式进行了代码实践,建议实践一下。
refresh_token
refresh_token即“更新令牌”,当授权服务器在返回access_token的同时会将refresh_token一起返回,并且refresh_token的过期时间晚于access_token。当用户使用的access_token过期时,可以使用refresh_token来重新获取令牌。
为什么使用 refresh_token
access_token是用户访问服务器资源进行身份认证的凭证,一般设置的过期时间都比较短。较短的过期时间主要是从安全方面考虑,一方面,较短的生命周期可以限制攻击者盗取access_token,另一方面,较短的生命周期可以在access_token改变时,及时的更新access_token。
当access_token过期需要更新时,只需要向授权服务器发送一个携带refresh_token的Post请求就可以实现access_token的更新(web应用的更新还需要携带client secret)。
refresh_token 存储
由于refresh_token设置的过期时间较长,所以存储refresh_token不被泄露至关重要。refresh_token应该和client secret一样保存在应用后端,只有在更新access_token的时候才需要离开后端。
此外,将token和refresh_token存储在客户端,服务器就不用专门的维护令牌状态,这样无疑会很好的减轻服务器的压力。
OAuth 安全
在使用授权码模式时,使用Github、微信等网站进行授权登录时,可能会因为Github用户授权页面的URL不包含state参数,导致 CSRF 攻击。
假设,当“黑客”使用leetcode网站时,通过自己的Github账号,获取了自己的code(授权码)。然后“黑客”精心打造了一个和leetcode网页高度相似的网站,如果有人通过Github账号登录该钓鱼网站,网站将会把黑客的授权码返回给用户。用户获取黑客的授权码后向Github服务器请求了“黑客”的令牌。此时黑客便可以跟任何使用该“钓鱼网站”的用户共享信息。
为了解决授权码模式存在的CSRF攻击问题,通常需要在Github用户授权页面的URL中添加state参数,如下所示:
https://github.com/login/oauth/authorize?
client_id = 6efe458dfe2230acceea&
redirect_uri = https%3A%2F%2Fleetcode.com%2Faccounts%2Fgithub%2Flogin%2Fcallback%2F&
scope = user%3Aemail&
response_type = code&
state = dIqOqdw4WChR
其中state参数是个随机数,每次刷新页面state参数都会改变,state会保存在用户本地。
当用户点击授权按钮后,页面会重定向到用户权限认证的redirect_uri(leetcode)页面,授权服务器(Github)返回的code和state会作为查询参数返回,如下所示:
https://leetcode.com/accounts/github/login/callback/?code=9275236b97affb3c6cc&state=dIqOqdw4WChR
因为两次state都是Github给出,此时通过对比用户本地保存的state和授权服务器返回的state,来判断是否是异常请求。这样就可以避免CSRF攻击。