一. 什么是Oauth2授权框架?
Oauth2是一种通用的开放式的面向个人(即用户)网络授权方案,它的前身是Oauth1。但是由于Oauth1设计复杂,易用性差,所以Oauth2与Oauth1并不兼容。
二. Oauth2.0的应用场景
我们拿csdn登录来举个栗子,来看csdn的登录页面下方:
其中的第三方登录(qq, 微信, 微博 ,github等),就是一个Oauth2的授权应用场景。 假设我们在没有csdn账号的情况下需要访问csdn的评论功能那么传统的情况下我们需要创建一个csdn账号然后在登录, 在有了Oauth2授权方案之后。我们可以在不需要登录csdn的情况下通过Github来登录csdn. 那么Github是怎么授权允许给csdn进行登录的呢? 首先我们排除以下可能的几点:
一: 授权不是将Github的账号密码发送过去比对.
二: 授权只是针对csdn而不是博客园或者某论坛.
三 : 授权是某个时间段到某个时间段的授权.
四: 授权是有范围的授权,而不是将所有资料都公开给csdn
那么搞清楚以上几个要点,我们在来谈Oauth2.0的专业术语.
三. Oauth2专业术语定义:
角色(Roles)
Oauth2.0当中共定义了四种角色:
一: client
这里的“客户端"不包含任何特定的实现特性,比如在上方的栗子中csdn就是一个客户端.
二: resource owner
资源拥有者, 比如在上方中,你点击csdn的第三方登录(Github),你拥有Github的账号,并且已经授权让csdn访问你在Github中的账号(通过点击第三方按钮). 此时你就是资源拥有者.
三: authorization server
授权服务器,当你点击了csdn登录页面下方的Github登录按钮之后就会跳转到Github提供的授权服务器
四:resource server
资源服务器, 当你点击同意授权之后,资源服务器会将你的信息发往client,在上面的栗子中等于Github的资源服务器将你信息发给csdn.
如果还不理解请见下图:
当你点击第三方登录时(点击Github),你作为resource owner, csdn为client. 接着跳转到如上的页面,此时我们可以认为上面的页面就是Github提供的authorization server. 当你点击同意授权后(Authorize passprotcsdn)资源服务器会将你的信息发送给csdn.
将上面的东西在文档化一点,就变成了如下图(图片引用RFC 6749):
(A): 客户端请求资源所有者的授权。(csdn请求你使用第三方登录)
(B):资源所有者同意授权。(你点击了第三方登录(假设为Github) )
(C): 客户端获得了资源所有者的授权之后,向授权服务器申请授权令牌(你点击了同意授权按钮)
(D): 授权服务器验证客户端无误后发放授权令牌(Github验证客户端是csdn)
(E): 客户端拿到授权令牌之后请求资源服务器发送用户信息
(F): 资源服务器验证令牌无误后将用户信息发放给客户端(将你的Github的昵称,图像等,发送给csdn)
经过上面的步骤就完成一套Oauth2的授权流程,不难看出在上面的步骤中关键是如何同意授权获取Token,那么下面就
讲Oauth2.0的几种授权方式.
笔者注: 上面流程图中的authorization server应该为授权服务器而非认证服务器。同样Oauth2是授权框架而非认证框架
从表现形式和专业定义的角度来讲都应该是 authorization 而非 authentication.
授权流程可以理解为客户端在经过资源拥有者同意之后从授权服务器中获取授权令牌的几种过程。
4.1 授权码模式获取授权过程(Authorization Code):
笔者注:User-Agent在本文章中表示为浏览器(以上图片引用自RFC 6749)
(A) 客户端初始化参数,并且通过浏览器将用户引导至授权服务器
(B) 用户同意或者拒绝客户端的授权请求.(通过浏览器)
(C)用户通过浏览器同意了授权,那么授权服务器将会发送一个授权码给客户端(二个C步骤同时完成,并且对用户不可见)
(D) 客户端拿到了授权服务器给的授权码之后将授权码(Authorization Code)发送给授权服务器,请求授 权令牌
(E)授权服务器验证授权码,若授权码无误发放授权令牌(Access Token)和刷新令牌
将上面的图和流程对应到csdn和github为:
1. csdn通过浏览器下方的第三方登录引导用户至github的授权服务器(大致对应A步骤)
2. 用户点击同意授权(Authorize passprotcsdn)(对应步骤B)
3.用户在浏览器中点击了同意授权后,Github将他的授权码发送给csdn后台(对应步骤C)
4.csdn拿到了授权码之后将授权码发送给Github的授权服务器(对应步骤 D)
5.Github授权服务器验证csdn发送过来的授权码是否是第三步中发送过来的授权码,是则发放允许授权令牌(对应步骤E)
由此可见A和B是需要资源所有者进行操作的,C D E都是客户端和服务器在各自的后台进行完成的。那么完成这些
步骤它们所需要的参数是什么呢?
4.1.1 授权码模式请求参数(对应步骤A):
客户端请求授权服务器需要携带的请求参数
一. response_type(必须携带)
在授权码模式中response_type的参数值必须为"code".(为了区分和其他三种请求模式的区别, 而 且 从字面上也方便理解, "code" 意思是先拿到授权码在拿token)
二. client_id(必须携带)
客户端标识符(如栗子当中的csdn, 此参数可以理解为区分是哪个客户端)
三. redirect_uri(可选参数)
表示重定向的uri(可以理解为当授权服务器返回code时的返回地址)
四.scope(可选参数)
表示申请权限的范围
五.state(推荐参数)
state参数通常是一个客户端随机值,发送给授权服务器,授权服务器在原封不动的返回给客户端。
这样做是为了预防CSRF攻击。
客户端向授权服务器的请求以"application/x-www-form-urlencoded" 方式提交,下面是一个授权码提交的栗子:
GET /authorize?response_type=code&client_id=s6BhdRkqt3&state=xyz &redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb HTTP/1.1 Host: server.example.com
4.1.2 授权码模式响应参数(对应步骤C ):
授权服务器在接收到客户端的请求参数后,对客户端提交的请求参数进行验证,验证成功后返回
如下参数:
一. code(必须):
返回给当前请求授权的客户端的授权码,通常设置为10分钟,并且只能使用一次
二. state(可选):
如果客户端包含这个值,那么授权服务器将值原封不动的返回给客户端
下面是一个返回code的栗子:
HTTP/1.1 302 Found Location: https://client.example.com/cb?code=SplxlOBeZQQYbYS6WxSbIA &state=xyz
4.1.3 请求授权令牌(对应步骤D ):
客户端在拿到授权码之后验证state值,然后向授权服务器请求授权令牌:
一. grant_tpype(必须):
参数值必须为"authorization_code",表示使用的授权码模式请求授权令牌
二. code(必须):
授权服务器返回的授权码
三. redirect_uri(可选):
授权服务器回调给客户端的回调地址,地址于请求code中的地址保持一致
四. client_id(可选):
客户端标识符,于请求code中的客户端标识符保持一致
授权码模式请求Token的栗子如下:
POST /token HTTP/1.1 Host: server.example.com Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW Content-Type: application/x-www-form-urlencoded grant_type=authorization_code&code=SplxlOBeZQQYbYS6WxSbIA &redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fc
4.1.4 授权码模式返回授权令牌:
授权服务器验证code和client_id信息,验证无误后返回如下参数:
一. access_token(必须):
授权令牌的值
二. token_type(必须):
token_type的值大小写不敏感,通常是bearer类型或mac类型。
三. expires_in(推荐):
token令牌的过期时间。
四. refresh_token(可选):
用来获取新的授权令牌。
授权服务器以json的格式返回给客户端,栗子如下:
HTTP/1.1 200 OK Content-Type: application/json;charset=UTF-8 Cache-Control: no-store Pragma: no-cache { "access_token":"2YotnFZFEjr1zCsicMWpAA", "token_type":"example", "expires_in":3600, "refresh_token":"tGzv3JOkF0XG5Qx2TlKWIA", "example_parameter":"example_value" }
4.2 简化模式(Implicit)获取授权过程
在简化模式(又称隐式模式)获取授权令牌中,省去了获取code的步骤. 在浏览器中(JavaScript)完成获取授权令牌的过程,简化模式中不需要验证客户端身份,同时也不支持刷新令牌。授权服务器将授权令牌进行uri_hash返回给浏览器,所以授权令牌是可见的.简化模式适用于客户端运行在浏览器或者flsh中, 同时也要求浏览器必须绝对可信,因为授权令牌可以被泄露给恶意用户或程序.
简化模式流程图如下(图片引用 RFC 6749 ):
(A): 客户端初始化参数并通过浏览器将用户引导至授权服务器(二个A同步)
(B):用户给予客户端授权
(C):假设用户同意授权,授权服务器将访问令牌以uri hash的方式重定向至A步骤指定的回调地址
(D):浏览器向资源服务器发送一个请求(不包括收到的hash值)
(E):资源服务器返回一个web页面(通常是嵌入脚本的html文档)
(F):浏览器在获得资源服务器提供脚本后提取其中的授权令牌
(G):浏览器将授权令牌发送给客户端
4.2.1 简化模式中客户端请求的参数:
一. response_type(必须):
该参数表示请求授权的类型,该值固定为"token"
二. client_id(必须):
客户端标识符
三. redirect_uri(可选):
授权服务器返回token后的重定向地址
四. scope(可选):
请求授权的范围
五. state(推荐):
state参数通常是一个客户端随机值,发送给授权服务器,授权服务器在原封不动的返回给客户端。这样做是为了预防CSRF攻击。
一个简化授权请求的参数栗子如下:
在js中可以用如下代码组装请求参数,示范如下:GET /authorize?response_type=token&client_id=s6BhdRkqt3&state=xyz &redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb HTTP/1.1 Host: server.example.com
var tokenparams = { client_id: "your clientid", client_secret: "your client_secret", redirect_uri:"your redirecturi", scope:"your scope", response_type:"token", } function formatParams( params ){ return "?" + Object .keys(params) .map(function(key){ return key+"="+encodeURIComponent(params[key]) }) .join("&") }
4.2.2 授权服务器响应授权令牌参数:一. access_token(必须):
返回的授权令牌值
二. token_type(必须):
token_type的值大小写不敏感,通常是bearer类型或mac类型。
三.expires_in(推荐):
token令牌的过期时间。
四.scope(可选):
请求的授权范围.
五. state(必须):
如果客户端包含这个值,那么授权服务器将值原封不动的返回给客户端.
简化模式返回授权令牌示范如下:
Location是4.2.1步骤中指定的redirect_uri,授权令牌等相关信息包含在uri_hash中.HTTP/1.1 302 Found Location: http://example.com/cb#access_token=2YotnFZFEjr1zCsicMWpAA &state=xyz&token_type=example&expires_in=3600
4.2.3 后续操作.
浏览器访问指定的回调地址,不带上hash(#键后面的值)后面的值(D步骤),资源服务器返回一个脚本
(对应E步骤). 脚本为返回授权令牌. 示范代码如下:
浏览器拿到token之后,将token令牌返回给客户端(对应F,G步骤)var hash = location.hash.substring(1); var scope= new Object(); if (hash.length > 0) { var regex = /([^&=]+)=([^&]*)/g; var keyValue; while (keyValue = regex.exec(hash)) { var key = keyValue[1]; var value = keyValue[2]; if ('access_token' == key) { scope.accessToken = value; } else if ('token_type' == key) { scope.tokenType = value; } else if ('expires_in' == key) { scope.expiresIn = value; } else { scope.tokenError = value; } }
笔者注: 简化模式中的资源服务器就是授权服务器回调token的地址, 浏览器去请求之前回调用的地址。 然后在解析hash后面的值, 得到 token.
4.3 密码模式(Resource Owner Password Credentials)获取授权过程:
使用此模式时,用户直接向客户端提供账号密码. 但是客户端不保存账号密码。这种模式通常是用户高度信任
客户端,如操作系统或高权限的程序.
4.3.1 密码模式流程图如下(图片引用 RFC 6749):
(A):用户将账号密码发送给客户端.
(B):客户端将账号密码发送给授权服务器,并请求授权令牌.
(C):授权服务器验证账号密码无误后,将令牌发送给客户端.
4.3.1 密码模式请求授权参数:
一. grant_type(必须):
参数值必须为"password",表示使用的密码模式请求授权令牌
二.username(必须):
表示用户名.
三. password(必须):
表示密码.
四. scope(可选):
请求授权范围
密码模式请求授权示范如下:
POST /token HTTP/1.1 Host: server.example.com Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW Content-Type: application/x-www-form-urlencoded grant_type=password&username=johndoe&password=A3ddj3w
4.3.2 授权服务器响应参数:
授权服务器响应参数同授权码模式中相应参数一样.下面是一个示范:
HTTP/1.1 200 OK Content-Type: application/json;charset=UTF-8 Cache-Control: no-store Pragma: no-cache { "access_token":"2YotnFZFEjr1zCsicMWpAA", "token_type":"example", "expires_in":3600, "refresh_token":"tGzv3JOkF0XG5Qx2TlKWIA", "example_parameter":"example_value" }
4.4 客户端模式(Client Credentials Grant)获取授权过程
客户端模式指直接客户端已自己的名义向授权服务器请求授权令牌, 此模式中用户(即资源拥有者)不参与授权过程
所以在此模式中也并不会出现用户敏感数据,它适用场景为获取需要授权的静态资源等.
4.4.1 客户端授权模式流程图(图片引用 RFC 6749):
(B): 授权服务器返回授权令牌
4.4.2 客户端模式授权请求参数:
一. grant_type(必须):
参数值必须为"client_credentials",表示使用的客户端模式请求授权令牌
二.scope(可选):
表示请求的授权范围
一个请求的栗子如下:
POST /token HTTP/1.1 Host: server.example.com Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW Content-Type: application/x-www-form-urlencoded grant_type=client_credentials
4.4.3 客户端模式响应参数:
授权服务器响应参数和简化模式中响应的参数相同, 一个例子如下.
HTTP/1.1 200 OK Content-Type: application/json;charset=UTF-8 Cache-Control: no-store Pragma: no-cache { "access_token":"2YotnFZFEjr1zCsicMWpAA", "token_type":"example", "expires_in":3600, "example_parameter":"example_value" }
五. Oauth2.0流程中的一些疑问与安全防护
5.1.0 state参数如何防止CSRF攻击?
回到csdn和github的栗子. 假设现在有二个位用户, 一个是攻击者小明, 一个是普通用户小芳. 攻击者小明拥有Github
的账号,于是他使用授权码的模式进行CSRF攻击,流程如下:
一.小明访问csdn第三方登录,使用Github进行第三方登录。
二. csdn将小明引导至Github同意授权页面,小明点击同意。
三. 小明使用抓包工具截获Github授权服务器返回的code请求。 这是小明拿到了自己的code。
四. 小明准备一个和csdn相仿的页面,诱导普通用户小芳来访问。这个页面后台会模仿csdn向
Github申请令牌的动作。申请令牌的Code就是第三步中获得的Code.
五. 小芳访问了小明准备的页面,并点击第三方Github登录。令牌的申请流程在小芳的浏览器中顺利
的触发,csdn从github授权服务器中拿到了access_token,不过这个access_token是攻击者小明的
六. csdn将小明的github账号同小芳的csdn账号关联绑定起来 ,从此以后小明就可以用自己的Github账号
通过Oauth2小芳的csdn账号.
5.2 攻击的前提条件:
一. 小芳在csdn上已经登录过(session是有效的).
二. 授权码没有过期.
5.3防御方法
防止这种攻击的方法就是在客户端请求授权服务器的时候加上一个state参数和当前请求用户做唯一关联。这样客户端在
接收到授权服务器传回来的state值时,比对它为当前用户请求时的state值是否一致来验证当前请求是否合法。那么读者
可能在想为什么授权码模式中需要获取code这一步,不获取code直接拿token就不会被拿到code,而且减少了一次网络
请求增加了效率. 答案是不能的, 因为浏览器中的回调地址是一个不安全的信道, 不适合传递token, 而code可以在浏览器
中的redirect_uri中传递, 因为在拿code去换取token的同时还会验证code的client_id身份, 如果是其他第三方拿到code,
那么它不是当前code的client也就并不会生效。
Oauth2授权框架还拥有一些什么问题.
一. access_token不透明问题, 及客户端不知道access_token是否是授权服务器发送过来的。
二. access_token一般是md5加密的sessionid或username,所能携带的有用信息较少。
三. 授权码模式中,授权码容易被截获。
下篇文章将为大家带来Oauth2.0的扩展。 Jwt以及PKCE。
开源项目介绍: Soatuh开源项目实现了本专栏的技术要点和功能,欢迎star, 或加入开发组。 项目内容
请访问项目地址。
项目地址: https://github.com/zhoujie123/Soauth