解决问题
OAuth2.0授权码模式主要解决了信任问题:一个第三方网站需要访问我们Github上的数据(例如用户头像),那Github为什么要信任该网站?该对网站信任到什么程度?
- 如果彻底信任该网站,那么将Github的用户名和密码直接交给该网站,由该网站直接登录即可。但这样使得该网站登录等同于用户登录,该网站将拥有与用户相同的权限,Github无法做权限的区分。
- 如果不再授权给该网站,则需要更改Github密码,这样也会影响到其他被授权的网站。
基于此,如果可以为该网站提供一个专门的access_token
,该access_token
有专门的权限和过期时间,且Github可随时清除access_token
的授权,这样问题就可以解决了。
为了提供这样一个access_token
,使用如下思路:
- 向Github登记一下该第三方网站,Github会给出一个凭据。记下该凭据。
- 第三方网站提供了Github授权按钮,通常是在登录页面提供。
- 用户点击授权按钮来同意Github授权,则携带凭据跳转到Github的管理页面,此时需登录。若已经登录则执行步骤4。
- 登录Github后,验证凭据,同意授权,则生成一个
access_token
返回。 - 第三方网站保存该
access_token
并向Github请求数据。 - Github返回数据,第三方网站使用这些数据匹配已有账号,匹配成功则登录;否则创建新账号并关联,然后登录。
流程
以Github授权给第三方网站为例,OAuth2.0的授权码模式流程如下:
这里将第三方网站的Web端和服务端分开,而非合并在一起。Github也是。这样更容易理解整个流程的细节及设计原因。
要使用授权码访问到服务器上的资源,有三个阶段:
- 登记应用。
- 获取
access_token
。 - 资源访问。
登记应用
假设我们是第三方网站的开发者,我们开发的网站要访问Github上的数据(例如用户头像),那Github为什么要信任我们的网站呢?
于是就需要进行应用的登记:我们明确地告知Github我们的应用的必要属性,Github登记后,就认可我们的网站了。
登记的流程为:
登录我们的Github账号,在其管理中找到Developer settings进入,在OAuth Apps下点击New OAuth App来创建一个新的应用。
新建应用时,需填入如下的属性:
- Application name: 应用名。
- Homepage URL:应用的首页地址。
- Application description: 应用描述。
- Authorization callback URL: 授权回调地址。默认地址。若授权请求中没有附带
redirect_uri
参数,则使用该地址。
点击Register application即可注册成功,Github会为该应用生成唯一的client_id
和client_secret
属性,这就是申请access_token
的凭据。
然后第三方网站需要将client_id
和client_secret
保存下来,通常是作为整个网站的全局配置。该网站所有的用户都将共享这2个属性。
获取access_token
流程
第三方网站的Web端登录页需要提供一个使用Github账号登录的按钮。点击该按钮,Web端向自己的服务端索要client_id
和state
,拿到后Web端拼接出Github的授权页面地址,直接以外链形式打开该授权页面。
这个外链地址虽然是Github的,但需要写在我们的第三方网站前端页面中,以作为外链来跳转。同时还需要在外连上直接以明文形式附加参数,主要包含:
- response_type: 模式类型。必须。授权码模式固定为
code
。 - client_id: 应用的id。必须。在Github登记后由Github生成,用于Github识别应用。
- redirect_uri: 重定向地址。可选。无论是否同意授权,Github都要调用该外链地址来跳转回第三方网站。如果不附带该参数,则Github会使用在注册应用时提交的
Authorization callback URL
。 - scope: 权限。可选。用于说明请求资源的范围,即Github允许第三方网站访问哪些资源。通常
scope
参数依赖于授权方的定义,这里是依赖Github的定义。 - state: 校验码。可选。当Github调用重定向地址时会再次传回该参数,用于第三方网站对请求进行合法校验,从而防止CSRF攻击。因此第三方网站需要将state保存在本地,以用于后续的校验。
外链到Github的页面打开后,访问的就已经是Github的Web端了,若Github没有登录则会提示登录Github账号。登录后,需要在页面上点击[确认授权]按钮。
点击后,Github的Web端向Github的服务端发确认授权请求,服务端生成授权码code
返回给Web端。然后Web端会将code
和跳转页面时传入的state
拼接到redirect_uri
后,以https
的外链形式在浏览器中进行跳转,这样浏览器页面就又跳转回第三方网站(跳回第三方网站是在回传code
时)。
浏览器页面跳转后,第三方网站的Web端将code
和state
发给自己的服务端,服务端对state
进行校验,确认是自己发出的。
然后第三方网站的服务端向Github的服务端发送申请access_token
的请求。参数主要包含:
- client_id: 应用的id。必须。登记时Github生成。
- client_secret: 应用的秘钥。必须。登记时Github生成。
- grant_type: 授权方式。必须。当前为授权码方式,传入
code
。 - code: 上一步Github发来的授权码。必须。
- redirect_uri: 必须。用于校验与请求code时传入的redirect_uri是否一致,不是用来做页面重定向的。
Github校验client_id
+client_secret
及redirect_uri
+code
无误后,生成access_token
,直接放入请求的Response中返回给第三方网站的服务端。为了处理access_token
过期问题,通常还会一起返回一个refresh_token
。
第三方网站的服务端收到返回,取出access_token
后,即可调用Github的接口来获取用户相关的信息。获取到信息后,首先判断是否已存在关联账户,若存在则直接登录;否则使用这些信息来创建新用户,然后登录。
由于授权码code
是跟Github的具体用户相关的,因此生成的access_token
也是跟用户相关的,多个用户的access_token
不同。
为什么要借助授权码code
来获取access_token
第三方网站发出第一次请求时获取到了一个授权码code
,使用授权码code
再次获取access_token
,然后才使用access_token
来获取用户信息。为何要加入一个授权码code
,而不是直接第一次请求就返回access_token
呢?
从上述流程分析,可知:
- 第一次请求时需要用户点击Github的[确认授权]按钮,因此页面必须跳转到Github网站的Web页面。由于需要用户点击操作,就无法将结果直接放在Response中返回,因为用户可以等一会再点授权按钮,这个等待时间可长可短,放在Response中很可能用户还没点按钮就请求超时了。
- 跳转到了Github的Web页面,授权后必须再跳回第三方网站的Web页面,这样就需要从Github的Web页面构造一个第三方网站的Web页地址以外链形式打开,同时将授权结果拼接到URL参数中,这意味着授权结果是暴露在公网上的。若直接返回
access_token
拼接在URL中显然会造成安全问题。 - 生成
access_token
需要校验client_secret
,但client_secret
是个需要保密的值,直接以外链形式传输该属性会直接暴露在公网上,同样有安全问题。
综上,先通过client_id
获取一个跟Github的具体用户相关的授权码code
,然后由第三方网站的服务端使用client_id
+client_secret
及redirect_uri
+code
这4个属性来请求Github获取access_token
。由于获取的请求交给了服务端,也就不用再担心client_secret
和access_token
暴露的问题。
某些网站为了更加安全,对code
设置了有效期,例如5分钟内有效,且只能使用一次。
实际上,OAuth2.0的简化模式就是取消掉了code
这一步,直接在第一次请求时的重定向地址中附加access_token
。 由于存在暴露风险,因此往往只用于安全性不高的场景,且另外有效期非常短。通常也不会发放refresh_token
。
资源访问
授权码模式用于Github授权账号登录到第三方网站。因此第三方网站拿到Github账号的信息后使用这些信息来创建新用户即完成了对Github必要的资源访问,只需要访问一次,不需要重复访问。
当然,第三方网站可以对access_token
与新创建的账号进行绑定存储,并使用refresh_token
进行有效期刷新,从而也可以重复地对Github进行资源访问。
但一般来说,这些访问请求都是由第三方网站的后端负责,第三方网站的前端是不接触access_token
的。