关于OAuth2.0的几个认识

OAuth2.0 是目前各大互联网公司主流使用的”单点登录+授权”协议,被各大开放平台所应用。

这里要注意,”单点登录”是SSO系统的职责,而”授权”则是OAuth系统的职责。但是,如果没有统一的用户帐号管理和登录认证能力,”授权”这个概念也无从谈起,因此现在OAuth2.0的实现普遍集成了两者。

OAuth2.0要解决的问题,一方面是统一的用户帐号体系,也就是一处登录处处使用;另一方面,通过”授权”机制避免用户帐号被第三方站点盗取的同时,又给予第三方获取用户基础信息的能力。

流程剖析

国内主流平台基本都是OAuth2.0,例如:新浪,腾讯,豆瓣,百度…等等,并且这个协议是有RFC的,也就是说通讯协议都是标准的,各家平台实现的大同小异,鲜有创新,下面我拿豆瓣为例来分析一下整个流程大概怎么实现,以及是如何避免安全隐患的。

获取 authorization_code

假设我的网站正在对接豆瓣,那么我首先发现用户在我的博客里没有登录状态,那么我会将用户重定向到豆瓣的登录+授权页,看一下豆瓣的文档:

通过在浏览器中访问下面的地址,来引导用户授权,并获得 authorization_code

参数:

参数名称 参数说明
client_id 必选参数,应用的唯一标识,对应于APIKey
redirect_uri 必选参数,用户授权完成后的回调地址,应用需要通过此回调地址获得用户的授权结果。此地址必须与在应用注册时填写的回调地址一致。
response_type 必选参数,此值可以为 code 或者 token 。在本流程中,此值为 code
scope 可选参数,申请权限的范围,如果不填,则使用缺省的 scope。如果申请多个 scope,使用逗号分隔。
state 可选参数,用来维护请求和回调状态的附加字符串,在授权完成回调时会附加此参数,应用可以根据此字符串来判断上下文关系。

注意:此请求必须是 HTTP GET 方式

例如:

返回结果:

  • 当用户拒绝授权时,浏览器会重定向到 redirect_uri,并附加错误信息
  • 当用户同意授权时,浏览器会重定向到 redirect_uri,并附加 autorization_code
  • 用户如果在豆瓣没有登录,那么他会输入帐号密码完成登录,豆瓣会维护全局会话状态,记录该用户为已登录状态。
  • 我们scope携带了希望授权的资源类型,我们通常想获取用户的基础信息(昵称等),这里可以传的scope豆瓣是明确列出的,最终需要用户在页面上点击接受按钮授予我们使用的权利,豆瓣才会给我的博客授权访问该用户的各类信息。
  • state这个值应该在用户访问我的博客之后(判定未登录),在重定向到豆瓣登录页之前,生成一个不可猜测的值保存在用户的会话里,后续豆瓣授权完成后会重定向用户到我的redirect_uri并携带state,我需要判断会话里的state和redirect参数里的state一致,目的如下:
    •  避免CSRF攻击:如果你不用state进行安全check,会发生这样的事情:黑客利用自己的帐号向豆瓣登录被重定向至:https://www.example.com/back?code=xxxx,但是黑客并不会去访问它,而是将这个链接发给了其他用户,其他用户不知情点击后实际上就在登录了黑客的帐号,如果用户接下来的行为是给账户充值,那么钱就进入了黑客的口袋。(其实黑客只需要在任意网站内埋藏一个iframe打开这个链接,就可以让你不知不觉的中招),有点偷梁换柱的意思。

获取 access_token

参数名称 参数说明
client_id 必选参数,应用的唯一标识,对应于 APIKey
client_secret 必选参数,应用的唯一标识,对应于豆瓣 secret
redirect_uri 必选参数,用户授权完成后的回调地址,应用需要通过此回调地址获得用户的授权结果。此地址必须与在应用注册时填写的回调地址一致
grant_type 必选参数,此值可以为 authorization_code 或者 refresh_token 。在本流程中,此值为 authorization_code
code 必选参数,上一步中获得的 authorization_code

注意:此请求必须是 HTTP POST 方式

例如:

返回结果:

使用 access_token

  • 首先,为了证明我的博客是合法的,我需要将client_id+client_secret一起提交用于认证。
  • code是临时票据,用于换取长期可用的access_token,一旦兑换成功code当即失效。
  • 为了防止client_secret泄露,同时确保豆瓣的身份不是伪造的,该接口是要求Https访问的。
  • 豆瓣对回调code的地址不要求https,因为即便code被中间人拦截,中间人也不知道client_secret,是无法获得access_token的。
  • 个人认为这里redirect_uri其实不是很必要,因为client_secret已经表明了身份,重要的是此前获取code环节提交的redirect_uri:通常OAuth服务都会要求在平台提前配置redirect_uri,在获取code环节通过强校验client_id,redirect_uri对应的关系来保障不会有攻击者替换redirect_uri从而盗取code。
  • 获得的access_token应该保存在我的用户会话里,用于后续享受我此前申请的各类scope权利,例如获取用户的昵称。

access_token 有效期 与 refresh_token

在 OAuth 2.0 中,access_token 不再长期有效。在授权获取 access_token 时会一并返回其有效期,也就是返回值中的 expires_in 参数。

在 access_token 使用过程中,如果服务器返回106错误:“access_token_has_expired ”,此时,说明 access_token 已经过期,除了通过再次引导用户进行授权来获取 access_token 外,还可以通过 refresh_token 的方式来换取新的 access_token 和 refresh_token。

通过 refresh_token 换取 access_token 的处理过程如下:

参数名称 参数说明
client_id 必选参数,应用的唯一标识,对应于 APIKey
client_secret 必选参数,应用的唯一标识,对应于豆瓣 secret
redirect_uri 必选参数,用户授权完成后的回调地址,应用需要通过此回调地址获得用户的授权结果。此地址必须与在应用注册时填写的回调地址一致
grant_type 必选参数,此值可以为 authorization_code 或者 refresh_token。在本流程中,此值为 refresh_token
refresh_token 必选参数,刷新令牌

注意:此请求必须是 HTTP POST 方式,refresh_token 只有在 access_token 过期时才能使用,并且只能使用一次。当换取到的 access_token 再次过期时,使用新的 refresh_token 来换取 access_token

例如:

返回结果:

级别 access_token 有效期 refresh_token 有效期 说明
L1 7天 14天
L2 30天 60天
L3 90天 180天
  • 每当发现access_token过期,都应利用refresh_token重新申请延期。
  • 之所以频繁的变更access_token和refresh_token,是因为攻击者完全可以猜测access_token进行用户信息盗取。

获取用户信息

通过访问以下url来获取授权用户的信息

例如:

返回结果为json,包含当前授权用户的各种信息。具体见用户Api V2

  • 无论通过哪种方式,将access_token提交,豆瓣可以找到对应的用户以及用户的信息,具体权限是通过此前scope申请,由用户同意授权的。
  • 可见,豆瓣的接口仅仅要求传入access_token而不再校验client_id,client_secret的做法实际安全性很低,攻击者枚举access_token即可轻松获得很多用户的信息,即便access_token会定时的过期,新浪微博在这方面更加严谨。

最后

要实现OAuth服务端,有几个重要的组成部分,总结一下:

  • 用户帐号密码和用户信息。-mysql
  • OAuth全局登录会话。-session(memcached/redis)
  • 第三方网站的认证信息(client_id,client_secret,redirect_uri)-mysql
  • 授权code与用户id,client_id,scope之间的关系,缓存几分钟。 -redis
  • 令牌access_token与用户id,client_id,scope之间的关系,缓存一段时间。-redis
  • 刷新令牌refresh_token与用户id,client_id,scope之间的关系,缓存较长一段时间。-redis

有上述结构,通过code兑换access_token,通过refresh_token兑换access_token,通过access_token获取用户信息就可以轻松实现了。

如果文章帮助您解决了工作难题,您可以帮我点击屏幕上的任意广告,或者赞助少量费用来支持我的持续创作,谢谢~