2

谈谈OAuth(下)

作者微信号:code-n-more

oauth-tutorial-02

上一篇不惜笔墨,讲了一大通技术之外的故事。前戏很长,略显啰嗦,但是这绝非故弄玄虚,而是我一直坚信,在学习一种技术之前,我们一定要先弄清楚,为什么需要这种技术,这种技术究竟解决了什么问题。知其然并知其所以然,是一个优秀软件工程师的基本追求。二进制的世界只有0和1,技术的世界,要么懂,要么不懂,懵懂,其实就是不懂。

现在,我们正式进入OAuth。

OAuth的全称是“Open Authorization”,中文翻译为“开放授权”。标准定义为:“An open protocol to allow secure authorization in a simple and standard method from web, mobile and desktop applications”。经过十年的发展,OAuth协议从最初的1.0版,已经进化到目前的2.0版。本文讨论的OAuth是其最新版本,即OAuth 2.0。

与Authorization紧密相关的另一个概念,是所谓的Authentication,中文一般翻译为“认证”。授权和认证这两个概念,既相互联系又有本质区别,非常容易模糊。为了避免懵懂,在讨论OAuth之前,我们需要仔细澄清一下这两个概念。

Authentication

与其把Authentication翻译成“认证”,倒不如叫做“身份验证”,更加地通俗和接地气。现实世界中,需要进行身份验证的场景很多。比如,你到银行申请一笔贷款,银行需要验证你的身份;你到邮局取一封挂号信,邮局需要验证你的身份,等等。而在二进制世界里,身份验证则更是一件无处不在,稀松平常之事。比如,你登录QQ时,腾讯会对你进行身份验证;你使用支付宝付款时,阿里需要对你进行身份验证,等等。身份验证,说白了就是你跟对方声明你是谁,对方验证你是否真的就是谁。

对于如何验证“你就是你”这件事情,在技术上,有两种不同的方式。一种方式就是验证用户名和密码,这种形式非常直观,在计算机的世界里,绝大多数需要进行身份验证的场景,都是通过这种方式完成的,比如登录QQ要求你输入密码,使用支付宝付款要求你输密码或者按指纹,等等。这种身份验证机制背后的逻辑是:只有我和你知道你的密码,如果面前的这个人,能够准确地向我说出你的密码,那么我认为,面前的“你”就是真正的“你”。

还有一种身份验证的方式,并不验证用户名和密码,或者说根本没有用户名和密码可供验证,而是通过某个第三方权威机构签发的证明来完成。你在声明“你就是你”的同时,附带上某个权威机构的证明,我只需要验证这个证明是真实有效的,即可认为你说的是事实。这种方式在现实世界中广泛存在, 你到邮局去取挂号信,只需要向邮局出示你的身份证;你在银行申请贷款时,声称自己月薪百万,只需要向银行出示有效的收入证明。在计算机世界,这种身份验证方式虽不普遍,但也确实存在,一般用于某些特定的应用场景,比如说单点登录。

Authorization

再说说Authorization。所谓Authorization,或曰授权,就是某一方授予另一方在特定时间内对某特定事物进行某特定操作的权限。在现实生活中,授权的例子比比皆是。比如,一张电影票,实际上就包含了一份授权,它允许持有者在规定的时间,到规定的影院,进入规定的放映厅,在规定的座位,观看规定的电影。授权需要载体来承载,电影票承载了电影院的授权。类似地,汽车票,火车票,地铁票,等等,皆是授权在现实生活中的体现。

请求发起者发起动作请求时,携带事先获得的票据,被请求者对票据进行验证,从而决定对请求作何种响应:接受或者拒绝。如同现实世界中使用电影票,在计算机世界里,授权的基本套路是一样的。当你成功登录某个网站时(意味着完成了身份验证Authentication),一般的做法是,网站会给你签发一个票据 —— 一段特别构造的包含网站对你的访问授权的字符串,接下来,你对网站的任何访问请求,必须带上这个票据,网站对票据进行解读,并结合其他额外的信息,决定通过或者拒绝你的访问请求。这种数字票据,一般称之为ticket,或者token。具体实现时,为了对票据进行保护,可能会对其进行加密或签名。

虽然同为票据,但是电影票和火车票,却有着一个非常根本的不同,那就是,电影票是不记名的,但是火车票是记名的。电影票上没有使用者的任何信息,意味着它可以被任何人使用,影院并不会验证持票人的身份。而火车票则不一样,火车票上不仅标注了车次,发车时间等信息,还有乘车人的姓名和身份证号码。 在授权验证(检票)环节,不仅要验证车票是否真实,车次是否正确,还会对持票人身份进行核对。也就是说,对于记名票据,必须专人专用,验证授权(Authorization)的同时,还须对持票人进行身份验证(Authentication)。显然,相比于不记名票据,记名票据更加的安全。

与一般授权的不同在于,OAuth的授权涉及到第三方,资源管理者不是为资源所有者本人,而是为另一方签发票据。要实现这一点,最简单的方式,就是让资源所有者把用户名密码交给第三方,第三方“冒充”其身份进行资源访问即可。这种方式注定是没有未来的,因为它问题重重,比如,用户的密码可能会被泄漏,无法控制权限授予的力度(粒度),一旦授权无法撤销(除非修改密码),等等,可以说除了简单一无是处。

在现实世界中,类似于OAuth的第三方授权也是存在的,装修钥匙就是一个很好的例子。把房子交给别人装修时,需要允许别人能够打开门锁进入屋子,但是又担心钥匙交出去之后的安全问题。在装修钥匙发明之前,这个问题只有通过装修完毕后,重新更换锁芯来解决,而装修钥匙则提供了更加优雅的解决方案。有过装修经验的人都知道,一般的防盗门锁,有两套钥匙,一套主钥匙,一套装修钥匙。装修期间,把装修钥匙交给装修人员使用,当装修完毕后,业主用正式钥匙开门,只要正式钥匙被使用一次,锁芯内的特殊机关便被触发,装修钥匙便不能再打开门锁。既解决了开放授权(装修人员能正常打开门锁),又解决了授权的控制与撤销问题(业主能随时将其作废),装修钥匙是个了不起的发明。

Access Token

“开放授权”也是一种授权,授权就需要有票据,OAuth也不例外,它的背后也有一张票据。具体而言,对于OAuth协议,所有的努力,最终都是为了得到一张名为Access Token的票据,第三方应用凭着这张船票,就可以顺利登上用户的小船(前提是没有过期)。协议背后的各种费尽心思,其根本目的,就是为了更加便捷地生成Access Token,并将其更加安全地交付到第三方应用手中。

安全与便捷是一个矛盾的问题,追求安全的同时,必然要损失一定的便捷性,反之亦然。在这个问题上,工业界和学术界持有不同的立场。为了尽快推向市场,降低开发人员门槛,工业界宁愿牺牲一定的安全性,换取使用的便捷性,而学术界则不愿意作此妥协。在OAuth这个问题上,最终,以Facebook为首的工业界取得了胜利。与此对应的是,OAuth 2.0从诞生之初,或者说还未诞生之时,就饱受争议,特别是它的Access Token,被设计为不记名Token,被认为是严重不安全的,因为这意味着,船票一旦泄漏,任何人都可以拿它来登船。船票本身不加密,不签名,其安全性全部交给传输层加密(TLS)来保证,这是OAuth Access Token的最重要的基本特点。

下面讨论OAuth协议的具体流程,协议中所涉及到的几个角色如下:

  • 用户 —— 作为资源的拥有者,我们称之为Resource Owner(RO),比如一个QQ用户,就是一个RO,其拥有头像、昵称、好友等各种资源。
  • 第三方应用 —— 可能是一个纯前端的网页应用,也可能包括后端服务,不管是什么样的形式,我们统称其为Client。Client期望获得授权,访问RO的资源。
  • 资源提供者 —— 存储并管控RO所拥有的资源,比如腾讯的QQ服务器,掌控所有QQ用户的资源。根据业务逻辑划分,将资源提供者解耦成两个部分:Authorization Server(AS)和Resource Server(RS),AS负责签发Access Token, RS负责响应资源请求。
  • 用户代理 —— User-Agent,来源于HTTP协议,OAuth沿用了这个名词,其实,代表的就是用户的Web浏览器。

为了配合不同的应用场景,OAuth协议提供了四种不同的授权方式,接下来一一讨论。

Resource Owner Password Credentials Grant

这种授权方式,基于Resource Owner的用户名和密码,交互流程如下图所示。

oauth-tutorial-03

  1. Resource Owner向Client提供其用户名和密码。
  2. Client携带RO的用户名和密码,向Authorization Server发起授权请求。
  3. AS对Client进行身份验证,同时对RO的用户名密码进行验证,如果验证双双通过,则同意授权,签发Access Token。

需要注意的是,这种方式并不是直接把用户名密码交给第三方,让第三方“冒充”RO进行资源访问,而是只在授权申请环节要求RO提供用户名和密码,用完即扔,Client并不会在背后存储RO的密码。但是即便如此,这种授权方式仍然简单粗暴。对于用户而言,将账户密码提供给第三方,是一件极其危险的事情,你无法知道别人拿到你的密码之后究竟会做什么,因此在绝大多数情况下,这种方式并不会被使用。

但是凡事都有特例,有些应用场景下,如果没有其他授权方式可供选择,同时第三方又被普遍认为是值得信赖的,那么这个时候,这种授权方式也许也是能被接受的。比如,支付宝内置的信用卡账单查询服务,它要求用户提供自己的邮箱账户和密码,支付宝据此进入用户的邮箱进行查询。这种授权模式,需要第三方强大的自身信用作为背书,如果你不是马云之类的人,就不要在你的应用中使用这种授权方式,否则你一定会很尴尬。

oauth-tutorial-04

注:这只是个例子,也许并不完全贴切,很有可能支付宝是存储了用户的邮箱密码的。

Implicit Grant

这种授权方式下,Client不再向Resource Owner索要用户名密码,交互流程因此变得复杂起来,如下图所示:

oauth-tutorial-05

值得说明的是,上面的流程图,实际上略去了一个重要的环节,这个环节是触发上图所有交互流程的第一步。那就是,Resource Owner访问Client的某个页面,或者从用户角度看,在Client的网页上进行某种操作,比如,在京东网页上,点击“使用QQ登录”,就是这样一个典型的操作。

oauth-tutorial-06

接下来,所有的事情开始按部就班地往下走了。

  1. 通过浏览器跳转,Client将Resource Owner带到Authorization Server的授权页面。这个授权页面一般会告诉用户,这个Client是谁,它希望获得什么样的权限,同时提供一个登录框,一个典型的授权页面如下,这是在京东网站点击“使用QQ登陆”时,跳转到的页面,即QQ授权服务器提供的授权页面:
    oauth-tutorial-07
    这个页面的URL可能是这样的:https://xui.ptlogin2.qq.com/cgi-bin/xlogin?appid=716027609&pt_3rd_aid=100273020&daid=383&pt_skey_valid=0&style=35&s_url=http://connect.qq.com&refer_cgi=authorize&which=&response_type=code&client_id=100273020&redirect_uri=https://plogin.m.jd.com/cgi-bin/m/qqcallback?sid=rzjuh9gynoa4fb8bimbjl80ogaat1zvt&state=qhax6kdt。这里有很多参数,其中最为重要的就是redirect_uri,Authorization Server最终需要通过这个redirect_uri将签发的Access Token回传给Client。
  2. 在授权页面,Resource Owner输入用户名和密码,点击登录,向Authorization Server确认同意授权。
  3. Authorization Server生成Access Token,在redirect_uri后加上“#”号,带上Access Token,请求浏览器跳转至该地址。
  4. 浏览器遵从跳转指令, 访问redirect_uri,此页面为内置特定js代码的静态HTML文件。
  5. HTML页面连同js代码被加载到本地。
  6. js代码执行,提取Access Token。
  7. Client获得Access Token。

以上步骤走完,Access Token到手,接下来Client就可以拿着它去向Resource Server请求Resource Owner的资源了。

这种授权方式已经从蛮荒走入了文明,因为用户不再需要向第三方提供其用户名和密码,但是安全性仍然不够,主要体现在对Access Token的保护上。作为一种不记名token,Access Token可以被任何持有者利用,因此必须进行妥善保护。虽然有TLS作为保证,我们认为传输过程中Access Token不会被窃取,但是在Implict Grant授权方式下,Access Token被直接回传到了客户端(浏览器),而客户端的环境是不安全的,暴露在浏览器的历史记录或日志文件中的Access Token,对于恶意者而言唾手可得。当然,正如第一种基于Resource Owner的密码授权一样,Implicit Grant虽安全性不足,但是并非毫无用武之地。Implicit Grant的一个极大优势是,他不要求Client拥有自己的后端应用,也就是说,如果你的应用是一个纯前端网页应用,Implicit Grant是你的不二选择。

Authorization Code Grant

这种授权方式,安全性更进一步提升,同时,交互流程也变得更加复杂。

oauth-tutorial-08

同样,这张流程图略去了Resource Owner访问Client这一步,这一步是所有后续动作的起点。

  1. 通过浏览器跳转,Client将Resource Owner带到Authorization Server的授权页面。
  2. 在授权页面,Resource Owner输入用户名和密码,点击登录,向Authorization Server确认同意授权。
  3. Authorization Server生成Authorization Code,添加到redirect_uri到参数中,请求浏览器跳转至该地址。
  4. 浏览器遵从跳转指令,访问redirect_uri(带着Authoriazation Code参数),Client后端接收到此访问请求后,提取出Authoriazation Code,调用Authorization Server接口,用Authoriazation Code申请Access Token。
  5. Authorization Server收到授权请求后,进行必要验证,如果验证通过,签发Access Token。

相对于第二种方式,Authorization Code授权方式在安全上又进了一步,与Implicit Grant最大的区别是它将Access Token从前端藏到了后端。保护鸡蛋不被摔坏的方法有很多种,前提是鸡蛋一定要在自己手中,如果鸡蛋在别人手中,一切都是空谈。Access Token放在用户的机器上,第三方应用即使希望对其进行保护,也是心有余而力不足。而一旦Access Token被藏到了后端,第三方应用便可通过加密等手段,最大程度保护其不被泄露。

流程越复杂,往往就越容易出问题,Authorization Code授权方式精心设计的“完美”流程背后,却隐藏着种种杀机,任何一个环节,如果处理的不好,就能被找到漏洞,造成严重后果。授权码模式面临的安全挑战及应对措施,主要有以下几个方面。

  • Authorization Code泄露:虽然Access Token不再暴露给客户端,但是同样的问题转移到了Authorization Code身上 —— 它是通过浏览器跳转回传给客户端的。如果Authorization Code被泄露了,一样会有风险,比如恶意使用者会重复利用它来申请Access Token。这个问题,OAuth从以下几个方面着手解决:1. 第三方应用提供的redirect_uri必须使用TLS,保证传输过程中Authorization Code不会被窃取;2. Authorization Code的有效期必须很短,比如10分钟或者更短;3. Authorization Server要保证同一个Authorization Code只能被用来换一次Access Token。4. Authorization Server需要对Client进行身份验证,保证只有合法者才能拿Authorization Code来换Access Token。
  • 篡改redirect_url:redirect_uri是授权服务器向第三方应用回传Authorization Code的通道,这个通道必须加密以防窃听,然而仅仅加密是不够的。攻击者有可能会这样做,他将授权页面网址中的redirect_uri部分进行篡改,改成属于他自己控制的地址,然后诱使受害者进入授权页进行授权,一旦受害者同意授权,本来应该发送给正常的第三方应用的Authorization Code,却经过被篡改的回调地址被发给了攻击者,进而被用来换取Access Token。这个问题的解决方法是,Client的reirect_url必须事先向Authorization Server注册,后者会对授权请求页面的参数进行检查,如果redirect_uri和事先注册的不匹配,则拒绝请求。
  • Cross-Site Request Forgery(CSRF):CSRF,伪造客户请求,是OAuth面临的另一个挑战。攻击场景是这样的,攻击者构造一种特殊的redirect_uri,地址上带着攻击者自己的有效的Authorization Code,然后诱使受害者访问该地址,实施诱使的方式有很多,比如诱使对方主动点击恶意链接,或者让对方打开Email中的一副恶意图片,等等。一旦实施成功,结果就是,受害者接下来的所有资源请求,实际上操作的是攻击者的资源,而非受害者本人的资源。极端点的例子,比如说,受害者向帐号进行转账操作,他以为背后操作的是自己的帐号,而实际上却是在给攻击者的帐号打钱。为了应对CSRF攻击,OAuth引入了一个特殊的state参数。state参数由第三方应用生成,在跳转到授权服务器之时一并传递过去,授权服务器跳转回redirect_uri的时候,将收到的state原封不动传回。在这个机制下,如果第三方应用生成的state足够随机无法被轻易猜测,并且保证一个state只被使用一次,那么攻击者就很难构造出合法的redirect_uri,从而规避了可能的CSRF攻击。

Client Credentials Grant

这是OAuth协议提供的最后一种授权方式,基本思想是,Client本身也是一个Resource Owner,比如说它是一个QQ用户,只不过这个QQ用户比较特殊,腾讯事先给了它特殊权限,它可以访问一些其他QQ用户的资源。所以这种授权方式,就是一个普通用户的登录授权过程,已经偏离了开放授权的本意,意义不大,就此略过。

Bearer Token

殊途同归,无论什么样的授权方式,无论交互流程有何差异,最终的目的都是得到Access Token。OAuth的Access Token是一种不记名token,所谓不记名token,英文叫Bearer Token。“Bearer”这个单词,在金融领域有着特定的含义,意为:“the person who is in possession of a check or note or bond or document of title that is endorsed to him or to whoever holds it”。其实,我们日常使用的纸币,就是一种典型的不记名票据,只要不是假钞,没有人会在意付款者(持票人)的身份,正是由于这一特点,才保证了纸币在社会中的正常流通。

其实,“Bearer”这个词真的存在某些国家的纸币上,下面是一张50英镑纸币局部。

oauth-tutorial-09

纸币上印着这么一句话,“I promise to pay the bearer on demand the sum of fifty pounds”。在金本位的时代,持币人可以随时用纸币向银行兑换相应份额的黄金,而银行必须承诺“见票即付” —— pay the bearer on demand。金本位时代早就过去,银行早已不再行使这项承诺,然而这句话却一直保留在了英格兰银行发行的纸币上。

OAuth漫谈,到此结束。

 



王 剑

2 Comments

发表评论

电子邮件地址不会被公开。 必填项已用*标注