OAuth2.0授权码模式详解

2023-03-01

OAuth 2.0一词中的“Auth“表示“授权Authorization,字母 “O” 表示 “开放Open”,连在一起就表示“开放授权”。这也是为什么我们使用OAuth的场景,通常发生在开放平台的环境下。

OAuth 2.0提供了4种模式:

  • 资源拥有者凭据许可(Resource Owner Password Credentials)、
  • 隐式许可(Implicit)
  • 客户端凭据许可(Client Credentials)
  • 授权码许可(Authorization Code)

本文将介绍OAuth2.0授权码许可的模式,授权码许可是应用最为广泛的开放授权模式,广泛被应用,比如微信开放平台、阿里开放平台等都是用这个模式做开放授权。

开放授权出现的意义

我们需要区分区分下认证和授权:

  • 认证(Authentication):用来验证某个用户是否具有访问系统的权限。如果认证通过,该用户就可以访问系统,从而创建、修改、删除、查询平台支持的资源。认证的方式就是我们登录,比如账密登录、手机验证码登录等。
  • 授权(Authorization):用来验证某个用户是否具有访问某个资源的权限,如果授权通过,该用户就能对资源做增删改查等操作。

简单而言,认证就是证明访问者的身份,决定他是否能进入系统;授权则是决定访问者能做哪些内容。一般而言,都是先认证后检查授权。

OAuth 诞生之初是为了解决 Web 浏览器场景下的授权问题。比如有一个网站A,在新用户注册时是支持QQ账号注册的,需要拿到QQ账号和头像作为网站A用户注册的基本资料,这样新注册用户流程就简单多了,很多信息都不需要用户自己填写,直接套用QQ号的信息就可以了。

这个就涉及到授权问题,我的QQ信息为什么可以给网站A使用?我是否有同意且我的授权是否限定在头像昵称等基础信息?我的QQ空间的信息能否被网站A访问呢?这就涉及到授权范围的内容了。

注册时授权获取QQ昵称等信息是一次性的,需要考虑的事情可能没那么多,也许一个请求回复就能拿到这些信息了,以后不会再需要。但如果网站A还有个业务,支持管理QQ空间里的照片,此时需要向用户申请授权:请允许网站A可以访问你的QQ空间的照片。这种资源的访问就不是一次性了,是反复多次的访问,这个就更为复杂,首先我们不可能网站A每一次访问我们的空间信息都要求用户点一次【同意授权】,如果我们当时没有登陆QQ,还得先账密登录QQ才可以进行授权。这样对用户体验很不友好。因此需要一种安全便捷的方式去处理这种频繁访问受限资源的场景。这个就是开放授权出现的意义:把私有资源有限地开放给第三方访问和使用。OAuth 2.0能安全且友好地解决了这个问题。

OAuth2.0 定义的角色类型

根据 OAuth 2.0 协议规范,主要有四个主体:

  • 授权服务器,负责颁发 Access Token,比如微信开放平台授权服务器。
  • 资源所有者,你的应用的用户是资源的所有者,授权其他人访问他的资源。比如微信用户是资源所有者。
  • 调用方,调用方请求获取 Access Token,经过用户授权后,微信开放平台 为其颁发 Access Token。调用方可以携带 Access Token 到资源服务器访问用户的资源。比如调用方是删上文说的网站A。
  • 资源服务器,接受 Access Token,然后验证它的被赋予的权限项目,最后返回资源。比如微信开放平台资源服务器。

接下来,我们将以微信用户授权第三方网站A的案例,介绍OAuth2.0授权码模式下的授权流程。

OAuth2.0授权码模式流程

我们先看下业界标杆微信开放平台是怎么做开放授权的。网站应用微信登录是基于OAuth2.0协议标准构建的微信OAuth2.0授权登录系统,微信OAuth2.0授权登录目前支持authorization_code模式。

我们以第三方应用example app为例,向阿里云请求资源的流程步骤:

  1. example需要在阿里云注册(授权服务器上),填写回调地址,并获得相应的client_id和client_secret,将该信息存储在example的后台服务器。
  2. 用户通过手机登录了example APP。
  3. 此时example需要访问用户的阿里云相关资源,这里需要获得用户的授权。example弹出一个页面引导重定向到阿里云去授权。
  4. 用户点击【去授权】按钮,example向阿里云授权服务器发起请求,申请获取授权码code,请求携带的参数有:client_id、重定向URL(用于授权后回调,带上example的uid), 授权范围scope(比如头像、账户名等资源)。
  5. 用户跳转到阿里云APP,看到授权确认页面(如果用户还未登录阿里云,需先登录):页面展示:example 申请访问你的阿里云以下资源:1. 头像 2. 账号名
  6. 用户点击【同意授权】,阿里云授权服务器将授权码code将拼接到重定向UR返回给用户,用户APP跳转回example,example后台拿到了uid对应的授权码code。
  7. example后台向阿里云授权服务器发起请求,换取assess_token和refresh_token,请求参数是授权码code、client_id。请求后得到用户访问阿里云资源的access_token和refresh_token,存储于example数据库。
  8. 用户访问阿里云资源服务器获取头像、昵称资源时,带上这个用户对应的access_token即可。

这里强调几个关键点:

  1. 用户的access_token是存储在example后台,取用户阿里云资源时是从example后台发起到阿里云资源服务器。
  2. 用户refresh_token用于做续签access_token,一般在access_token快过期时或者已过期时触发续签。
  3. 授权码code只能兑换access_token和refresh_token一次,兑换后失效。
  4. refresh_token有效期比较长,一般30天;access_token较短,一般2小时。
  5. access_token用于访问阿里云资源服务器时的身份验证和权限验证;refresh_token用于请求阿里云授权服务器续签access_token。

问题思考

access_token和refresh_token的关系是什么?

access_token是调用授权关系接口的调用凭证,由于access_token有效期(目前为2个小时)较短,当access_token超时后,可以使用refresh_token进行刷新,access_token刷新结果有两种:

  1. 若access_token已超时,那么进行refresh_token会获取一个新的access_token,新的超时时间;
  2. 若access_token未超时,那么进行refresh_token不会改变access_token,但超时时间会刷新,相当于续期access_token(有的做法是原来的access_token上做延时,有的开放平台是直接换一个新的access_token)。

refresh_token拥有较长的有效期(30天),当refresh_token失效后,需要用户重新授权。

为什么需要双token机制?

首先探讨access_token和refresh_token都放在哪里存储。

第一个方案:access_token和refresh_token都放在example后台存储。这种方案安全性更高。

第二个方案:access_token放在用户手机APP存储,refresh_token都放在example后台存储。这种方案效率更好,不需要转一次到example后台。

如果是用这二个方案,就好解释了。使用单个token续签方案,需要考虑离线续签的问题。在线情况下,当access_token将要过期时,客户端会向服务器请求更换access_token。当离线情况下(比如客户端进程没了),此时access_token即将过期时,因为没有客户端进行续签,因此离线情况下,access_token的续签是失败的,用户下次访问服务器就要重新登录(授权)了。

而双token方案可以很好地处理离线续签的场景,假设access_token的过期时间是2小时,refresh_token过期时间是30天。客户端在线时,当检查到access_token距离过期时间还剩15分钟时,请求服务器进行续签,得到新的access_token;客户端离线了一段时间(比如12小时),此时access_token已经过期了,客户端重新上线时,请求example后台拿着refresh_token去服务器去重新申请access_token,因为refresh_token还没到过期时间(没到30天),因此可以顺利拿到新的access_token,完成离线的续签操作。

除此之外,更重要的是,access_token是用于资源服务器,refresh_token用于授权服务器,这是两条不同的请求链路。在生产实践中很多系统往往简化了OAuth2的认证,把资源服务器和授权服务器合并了,导致初学者无法区分。在复杂的环境中资源服务器和授权服务器是由不同团队负责,甚至是不同的公司维护以至于相互并不信任,所以需要两个token来分别访问。

为什么获取access_token和refresh_token之前一定要先获取code,然后再用code去获取access_token和refresh_token?

这是基于安全性的考虑。首先如果没有code这一步骤,用户同意授权后的重定向地址拼接上了access_token和refresh_token返回到手机APP里,再此时refresh_token是有机会被被窃听到的,而且这个refresh_token时效性较长,所以refresh_token一旦泄漏就会长时间处于风险阶段。如果是code模式,因为code只能兑换一次,因此在APP侧恶意程序窃听code且先私自兑换了refresh_token,example再次兑换就会报异常,提醒用户你的code已被兑换,要重新授权,提醒用户风险。如果是example后台拿着code先兑换了refresh_token, APP侧恶意程序后兑换refresh_token,会提醒code已失效,规避了风险。

Token的续签是系统自动完成的吗?

续签是客户端写代码来实现自动更换的,比如客户端本地周期检查access_token是否即将过期或者已经过期,是的话就拿refresh_token去换新的access_token。

授权码被盗取后,人家不能也模拟服务器请求获取access_token吗?

授权码存在有效期,一般很短只有几分钟,过期了就没法用了;授权码只有一次使用机会,如果该授权码已经被人兑换过了,后面就没法兑换了。
如果是无appSecret的模式,授权码被盗取后是有机会被模拟盗窃access_token的。

在appSecret这种模式下,兑换access token,如果只有授权码是没法兑换access token,还得有appSecret+授权码来兑换。所以appSecret这种模式更为安全。