简介

本文适合对OAuth2已经有一定了解的开发人员,没有说到几种具体的模式流程,仅仅写下我自己对授权oauth2的理解,还有一些疑问和解答,希望对阅读的人有所帮助,如果有什么建议可以直接联系笔者。关于具体的模式讲解和实例我就留到后续文章中一一道来。

了解授权

什么是授权

简单来说就是将自己的权限给其他人,让他可以完成相应的工作任务。
这里实际有个主动被动的关系,一般开发中的授权都是说的被动式的(请求用户授予权限)。

为什么需要授权

互联网作为一个的庞大网络,其中有海量的服务提供方,而我们个人作为这些服务的使用者,通常需要对不同的服务提供相同的信息(比如身份、学历、联系方式等等),而这不就相当于自己把个人信息从一个已有的服务系统拷贝另一个新的服务系统中吗?不想自己做,嫌麻烦了也可以委托其他人来拷贝,但再此之前得找自己信得过帮手让他代替自己(你肯定不希望随便一个陌生人盗用自己得信息),为了不同服务间资源数据的安全共享,所以需要授权!

怎么授权

注:怎么授权换言之就是授权方式,先思考有哪些授权方式,它们可能会遇到的问题和处理方式,至于它们具体实现细节这里不做过多描述。

在了解怎么授权的之前,先思考授权这个过程涉及到了哪些角色(自己的理解,比较片面):

  • 用户:资源数据的所有方
  • 服务提供方:需要共享资源数据给外部的服务
  • 服务使用方:第三方服务、外部服务、接入方、调用方

授权行为:用户将自己在服务提供方的权限(部分或全部)给服务使用方,使服务使用方可进行相关权限的操作。

最简单的授权无非是服务提供方将用户的帐号密码给第三方系统(服务使用方),但这是建立在非常信任情况下(应该没有正常人会把自家大门钥匙给一个从未见面的陌生人吧)

共享帐号密码实现确实简单直接,但这真的没有弊端吗?

试想自己作为服务提供方采用这种方案,将会有哪些问题?

  1. 要是哪天用户的密码换了,又怎么让第三方系统能继续访问资源?(同步帐号密码必然遇到更多问题!)
  2. 又或者是哪天第三方其中的一个不再可信,不能再给他权限了,如何保证在不影响用户和其他第三方系统的情况下对其限制?
  3. 再说如果给了帐号密码,就相当于用户在操作,又怎么知道用户是否真的出于意愿来进行操作?(难以保证第三方不会滥用帐号密码)
  4. 更需要注意的是权限,比如第三方系统已知用户帐号密码,那他是否可以修改用户的密码?

以上只是想到的一部分问题,估计要解决这些问题又得牺牲为数不多的头发。

如果站在被授权方(服务使用方)的角度,这些问题都不算事,只要你给密码,我能完成工作,那就没问题!
但是站在授权方(服务提供方)的角度,那就不得不重视了,不仅是为了用户,也是为了本系统的安全!

从服务提供方的角度来考虑的话,怎么授权才能避免上面的问题?

  1. 当然是不与第三方共享密码,或者直接由用户提供帐号密码授权
  2. 需要验证第三方服务的身份,不是任何人都可以随便接入授权服务
  3. 让用户参与到过程中(提供帐号密码、确认授权范围、确认人为操作)

所以授权是最好三方(服务提供方、服务使用方、用户)一起参与才最真实可靠,简单的说就是

  1. 不止校验用户是否有意愿来完成授权
  2. 还要看服务使用方是否是可信的

以共享用户帐号密码的方式为例,首先只有完全可信的第三方(服务使用方)才能共享帐号密码,但用户是否真的有意愿使用功能那就不得而知了(问题3)。

那么如何做到这些呢?

从OAuth2 授权码模式中可以总结出了这样的答案:

  1. 服务使用方在服务提供方注册自己的身份信息用于验证是否可信(client_idclient_sercet等)
  2. 通过重定向页面让用户确认授权信息(用户信息通过用户登录或者已登录的会话获得,从而关联用户信息和服务使用方)

前面说到直接与多个外部系统共享用户帐号密码的方式将面临的问题,正好可以基于以上两点来解决:

  1. 用户更改密码,并不影响第三方系统(被授权方)在服务提供方已注册的身份信息,两者是独立的
  2. 各第三方系统(被授权方)在服务提供方注册的信息也是相互独立的,限制其中一个并不影响其他第三方系统
  3. 不止第三方系统,用户也参与了授权(重定向页面验证用户身份)
  4. 用户可以根据自己的意愿来确认是否授权(比如授予修改密码的权限)

当然不能避免的是引入需要考虑的新问题:

  1. 如何避免第三方系统身份信息的泄露、被盗用的情况(类似用户帐号密码泄露的情况)
  2. 用户直接参与授权验证,交互会更加复杂,给外部的可乘之机就越多
  3. 用户的每次授权的时效性(只有一段时间内可用,避免一次授权一直使用,当然也存在特殊场景)
  4. 如何避免用户被钓鱼授权(用户在本系统已登录认证过,然后在互联网点击攻击者放置的钓鱼链接以获取其他正规网站的授权)

等等一系列问题,不过现在不需太过深入在意。

以上是个人学习OAuth2.0授权码模式同时思考的总结,总而言之授权过程的需要考虑:

  1. 服务使用方是否是可信的
  2. 用户信息是否真实
  3. 用户是否有意愿来完成授权
  4. 互联网的一些安全问题

注:如果已经了解过OAuth2.0的密码模式,或许会注意到该模式并没有验证服务使用方的信息(client_idclient_sercet等),可以说是从传统的用户帐号密码验证演变来的,能提供帐号密码既是可信的(被盗和泄露另说),官方文档上说明(需保密就用授权码模式):

If the client type is confidential or the client was issued client
credentials (or assigned other authentication requirements), the
client MUST authenticate with the authorization server as described
in Section 3.2.1.

OAuth2.0授权

首先,和上面思考怎么授权之前一样,OAuth2.0对涉及的角色给出了相关定义

resource owner: 资源拥有者,可以理解成常见的用户。
client:客户端,请求被授权的应用。可以理解授权服务的使用方。
resource server:实际提供资源数据的服务器
authorization server: 授权服务器

在我的理解中,resource server+authorization server可以合称为服务提供方,因为这两者相对服务使用者来说是一方的。只是这里根据各自的职责定义更加详细具体。

再看OAuth 2.0的抽象运行流程如下图,摘自RFC 6749

Figure 1: Abstract Protocol Flow

这个图我在我学习OAuth2.0的整个过程中至少看到过五次,然而就算一遍又一遍其中的每一步的意义,始终感觉自己哪里不明白(不知道有人跟我一样不)

起初我有些困惑这个图为啥和自己印象中的OAuth2.0授权流程不一样,几经思考还是暂时放在一边

后来才逐渐反应过来这不过是抽象的授权协议流程(印象中的是几个模式具体的流程)

但紧接着我又疑惑了:为什么抽象的授权协议流程是这样的?与几个模式具体的流程差异这么大?这样的流程有什么好处?

抽象的授权流程换言之意味着最符合理想效果的流程,所以我反过来思考什么是理想的授权效果?通常的授权方式又有哪里不尽人意?该怎样授权?

设想一个情景:找人装修

角色:

1. 我们:高大上小区的物业和门卫
2. 张三:他在我们小区刚买了套新房,平时996
3. 李四:老实巴交专门接私活干的装修工

事件:张三请李四帮他装修房子

星期一早上10点,李四拎着他的麻袋来给张三干活。

我一瞧,这么大一个麻袋干嘛的,难不成是人口贩子?不行,得好好盘问盘问:“你谁?来这干嘛得?”。
李四憨厚一笑道:“我是来装修得,张三请我来的,就是1栋2单元3楼4户的业主”。

1234号业主不刚好是物业上面那户程序猿吗,确实是刚买房子还没装,最近常来物业转悠还算挺熟的,我一皱眉,刚想开口再问时,就见他放下麻袋,抽裤兜里掏出一张对半折的整整齐齐的纸条递给我。

“这是张三他给我的委托书,你给看看呢。”他憨笑着摸了摸后脑勺说到。

我接过纸条展开一看眉头一展,这歪七拐八的字确实是那苦逼程序猿张三的真迹,而且这人老实巴交的看也不像假话。“行吧,这委托书我就收下了,你跟我去物业来办个临时工作证,后面就用它进出吧。”

于是李四老老实实跟着我办了证,天天拿着他的临时出入证勤勤恳恳去帮张三装房子去了。

可以代入不同角色去思考,这里从物业门卫的角度考虑,是为了确保小区内的所有居民的安全,不只是特例张三。

整理下几者之间是如何交互的:

  1. 张三、李四早就知道李四是没法直接进小区的,所以张三事先给李四整了一张能证实张三身份并说明李四事由的委托书。
  2. 李四将委托书交由交由门卫进行验证真实性,并为其办理临时出入证。
  3. 李四使用临时身份证开始自己的工作

当然这里只是举个例子,要是思考:为啥张三不直接带李四一起进去?又或者为啥要整个委托书,不直接电话?…等等类似问题,那就说明考虑的很全面。不过这里简单抽象就够了(你可以把张三自己看着证明,或者张三的电话回答是证明),到最后也就是上面的OAuth2.0抽象流程图。

话说回OAuth2.0抽象流程图具体步骤:

A:Client找Resource Owner要一个凭据,为了完成他委托自己办的事
B:Resource Owner给Client一个凭据,这个凭据同时记录了委托人、被委托人、委托事项等
C:Client将凭据给Authorization server进行验证(这里只是抽象流程,验证真实性的方式不需太过深入)
D:Authorization server觉得凭据没啥问题,再给了Client整个临时访问的令牌用
E:Client拿着令牌到Resource server确认,并告知他想干嘛
F:Client完成自己的工作,并从Resource server得到想要的结果数据

这不就像:

  1. 张三是Resource Owner
  2. 李四是Client
  3. 物业门卫是Authorization server

或许你会在意Resource server体现在哪,E、F步骤怎么体现?可以理解就是小区门禁。

这个流程的难点无疑在于B步骤(授权方式):

  1. Resource Owner如何授予权限给Client
  2. Authorization Server如何知道B步骤是真实的?也就是C步骤Client提供的授权凭据是真实的

答案是重定向,也就是OAuth2中说到的引导用户到指定页面(密码、客户端模式下不需要)。

授权码模式下,Client引导Resource OwnerAuthorization Server的授权页面,同时会附带自己的身份信息,而Authorization Server的授权页面还需要知道是谁来赋权,所以通常还需要Resource Owner登录一次(或者从已登录的会话中获取用户信息)来确定。

从交互上来看,Authorization Server就像是Resource OwnerClient赋予权限这件事的见证者、记录者。以前面的例子来说,就像是:李四拿的委托书新来的门卫不认,需要到物业处申请专门规定的表单,他签字登记身份信息和事由,然后找来张三签字确认授权,并由物业盖章才能证明。

关于具体的授权方式就这里写就太长了,先留个新坑吧~

解惑

  • Q:为什么要分别定义Authorization ServerResource Server

    两者是抽象概念,实际可以是不同服务器,也可以是同一服务器甚至同一服务。抽象主要是依据职责,而且由于微服务和分布式等的概念,单一职责更易于复用,从而达到一个授权服务器发布的授权也可以用于多个资源服务器目的。

  • Q:为什么大量交互都是使用重定向的,而不是后台直接调用接口?

    首先,后台调用接口方式也是有的,但仅限于Client请求Authorization Server获取Token时(授权码模式下),剩余交互都是通过重定向,为什么如此?为什么在用户确认授权时Authorization Server不直接将Token通过调用接口方式返回给Client,这样不是更简单高效?
    其实原因主要在于Authorization Server可能无法访问Client给的重定向地址,比如不同域下、重定向地址需要当前已登录的用户信息等等。一个简单的例子在局域网内对接外网服务认证(比如Github),或者使用带有localhost的地址作为结果处理端点,这样授权服务器是无法连接客户端的,但是用户代理(通常指浏览器)一定知道,所以重定向更加适用,而且也可减轻授权服务的压力。

  • Q:为什么授权码模式需要有一步Code,再换取Token

    与简化模式不同,授权码模式多出一步授权Code请求,而不是直接返回Token,这样做的目的肯定是为了更安全!
    上一个问题说到为什么使用重定向,然而这样也会带来困难,直接返回Token会暴露给外部、甚至攻击者(实际上简化模式就是这样做的,但迫不得已才会采用这种授权方式),所以中间添加了一步Code,但Code就不会暴露吗?会,同样的!但是没关系,因为只拿到Code是没有用的,还得需要一个东西,也就是client_sercet
    在授权码模式中Client身份实际有两个东西结合认证的:client_idclient_sercetclient_id通过用户代理的url参数早已经暴露在网络中,只能表示是谁,而client_sercet不同,它只有在Client主动调用请求Authorization ServerToken接口时才使用,也就是能使用client_sercet一定是他自己(或者类似集群的),Code结合client_sercet一起使用在授予Token时先确认Client的身份准确无误,这就是Code的意义,确保Token只被对的人获取。
    而在简化模式中不能使用client_sercet(一般没有后端服务程序,但前端传输会暴露,所以不能使用),这样就不能校验Client的身份,那么Code在简化模式中也就没有意义,还不如直接请求Token来的实在。

  • Q:授权权码模式下,获取Tokenredirect_uri需要和获取Code是相同的?

    一开始我以为两个redirect_uri都会用来重定向,但实际上只有请求Code后才会发生一次重定向(Authorization Server -> Client),获取Token的接口参数redirect_uri是为了校验前面的请求,保持一致性,防止外部攻击

  • Q:为什么简化模式不校验client_sercet直接获取到Access token?

    校验不了,参考问题:为什么授权码模式需要有一步Code,再换取Token
    更多文章:stackoverflow

  • Q:简化模式的token获取脚本(web-hosted client resource)指的是从授权服务器、Client的Web服务器还是资源服务器?

    我觉得时Client的Web服务器上的一个脚本,但图例中既有web-hosted client resource又有Client,实在令人混淆,但实际的重定向目标地址时之前Client提供的redirect_uri,也就是Client的资源路径之一(专门用于获取url上hash片段里的Token的页面)。
    更多文章:stackoverflow等。

  • Q:关于参数state有什么意义

    保持请求设备一致性,也就是防止CSRF攻击,简单的说就是攻击者用他已有的帐号和他的state向授权服务器申请Authorization Grant后,用其伪造一个链接或图片让用户点击,这样用户以为他授权的是自己帐号,但实际是攻击者专门的帐号,之后用户的所有操作都可被攻击者看见,如果向该帐号推送数据(比如保存文件,帐号密码,转账等等),那就完蛋了!所以说对state进行校验是有必要的,要是传出的和传入的state不等,那直接拒绝就完事了!
    state 如果为固定值的话意义不大,攻击者知道了就形同摆设,最好是动态的(例如会话的hash值)!
    可以从以下文章中了解更多

    1. 关于 OAuth2.0 安全性你应该要知道的一些事
    2. 移花接木:针对OAuth2的CSRF攻击
    3. 技术干货 | OAuth2.0的安全解析
  • Q:资源服务器在接受每个请求时,是怎么判断令牌的真实有效性的?

    这部分实际不在OAuth2协议规范里。通常验证需要资源服务器与授权服务器交互协调,也可以通过其他方式,例如中间件(Redis)或者JWT实现。

  • Q:认证和授权的区别?

    二者实际不一样,但又有一定联系,一时间有些混淆。
    认证:辨认证实身份。通常指的是程序和用户二者之间的交互,程序需要知道用户到底是谁?登录认证便是一种。
    授权:授予权限访问。通常指的是三者交互(用户、服务提供方、服务使用方),服务使用方请求用户将他(用户)在服务提供方的部分或全部权限授予自己(服务使用方)。
    区别:认证更倾向于用户是谁!授权更倾向于能替哪个用户干什么事!一般认证后,拥有对整个帐号的功能数据的所有权力,但授权后仅限于被已认证的用户授予的部分。
    联系:授权时一般涉及到用户身份认证。

参考

  1. OAuth 2.0 的一个简单解释 - 阮一峰
  2. 理解OAuth2.0 - 阮一峰
  3. OAuth 2.0 的四种方式 - 阮一峰
  4. OAuth2.0 详解 - 知乎
  5. The OAuth 2.0 Authorization Framework - rfc6749
  6. 从“黑掉Github”学Web安全开发
  7. OAuth 2.0 Tutorial
  8. 关于 OAuth2.0 安全性你应该要知道的一些事 - chrisyue
  9. 移花接木:针对OAuth2的CSRF攻击
  10. 技术干货 | OAuth2.0的安全解析
  11. OAuth2授权码模式详细流程(一)——站在OAuth2设计者的角度来理解code