前言 OAuth2。0全称是OpenAuthorization2。0,是用于授权(authorization)的行业标准协议。OAuth2。0专注于客户端开发人员的简单性,同时为Web应用程序、桌面应用程序、移动设备应用等提供了特定的授权流程。它在2012年取代了OAuth1。0,并且OAuth2。0协议不向后兼容OAuth1。0。 需要注意的是,OAuth2。0是一个授权(authorization)协议,而不是身份验证(authentication)协议。Roles角色 首先还需要了解一些概念,因为整个OAuth授权流程都是围绕这些抽象的概念展开的,角色是OAuth2。0授权框架核心规范的一部分,OAuth定义了以下4种角色ResourceOwner 资源所有者,这里通常是拥有资源权限的用户或者系统。Client 客户端应用,它可以通过访问令牌(Token)访问受保护资源,可以是Web浏览器上的网站也可以是桌面应用或者手机App。AuthorizationServer 授权服务器,在经过用户的授权后,向客户端应用发放访问令牌(AccessToken)。ResourceServer 资源服务器,存放受保护资源的服务器,接受来自客户端(Client)请求的有效访问令牌(AccessToken),然后返回对应的资源。ClientTypes客户端类型 OAuth2。0核心规范定义了两种客户端类型,confidential机密的,和public公开的,区分这两种类型的方法是,判断这个客户端是否有能力维护自己的机密性凭据(password,clientsecret)。confidential 对于一个普通的web站点来说,虽然用户可以访问到前端页面,但是数据都来自服务器的后端api服务,前端只是获取授权码code,通过code换取accesstoken这一步是在后端的api完成的,由于是内部的服务器,客户端有能力维护密码或者密钥信息,这种是机密的的客户端。public 对于一个没有后端的纯前端应用来说(比如SPA),数据的展示和操作都是在前端完成的,包括获取令牌和操作令牌,把一个客户端密码或者密钥放在纯前端应用是不安全的,这种是公开的客户端。ClientAuthentication客户端身份认证 前面已经说过了,OAuth2。0是授权协议,那为什么还要对OAuth2。0客户端进行身份验证呢?身份验证和授权有什么区别?简单说身份验证确认用户是否是本人,而授权则是授予用户访问资源的权限,授权的前提条件一定是要先通过身份认证,而且接下来的内容中,也有用到了身份认证,为了方便理解,所以对认证做了简单的介绍。 授权服务器对客户端进行身份验证可以保证把令牌颁发给了合法的客户端,但是认证其实已经超出了OAuth2。0的协议范围,在〔RFC6749〕中也只是简单介绍了以下2种认证方式: 第一种是使用HTTPBasic〔RFC2617〕中定义的身份验证方案进行身份认证,这种方式叫clientsecretbasic,首先需要对username,password或者clientid,clientsecret用冒号进行拼接。 {username}:{password}或者{clientid}:{clientsecret}就像这样admin:123456 然后对字符串进行Base64编码,然后设置为请求Header中的Authorization,注意前面要拼接一个Basic和空格,如下POSTtokenHTTP1。1Host:www。authorizationserver。comAuthorization:BasicYWRtaW46MTIzNDU2ContentType:applicationxwwwformurlencoded 第二种方式就更简单粗暴了,直接在请求体中添加clientid和clientsecret参数,如下POSTtokenHTTP1。1Host:www。authorizationserver。comContentType:applicationxwwwformurlencodedgranttyperefreshtokenrefreshtokentGzv3JOkF0XG5Qx2TlKWIAclientids6BhdRkqt3clientsecret7Fjfp0ZBr1KtDRbnfVdmIwProtocolFlow协议流程(A)AuthorizationRequestResourceOwner(B)AuthorizationGrant(C)AuthorizationGrantAuthorizationClientServer(D)AccessToken(E)AccessTokenResourceServer(F)ProtectedResource 上图是抽象的授权协议流程,也展示了4种角色(Role)之间的交互,具体的过程如下 (A)客户端向资源所有者(用户)发起授权请求,资源所有者选择授予权限或者取消,这个过程中,授权服务器充当中介的角色,userauthorizationserverclient。 (B)客户端收到授权许可(code),这是一个代表资源所有者授权的凭证。 (C)客户端通过授权许可(code)向授权服务器发起请求,并期望获取一个访问令牌(accesstoken)。 (D)授权服务器对客户端进行身份验证并验证授权许可,如果有效,则颁发访问令牌(accesstoken)并返回。 (E)客户端通过访问令牌向资源服务器请求受保护的资源。 (F)资源服务器验证访问令牌,如果有效,则返回相应的资源。AccessToken访问令牌 accesstoken是一个用来访问受保护资源的凭证,它是由授权服务器(AuthorizationServer)颁发给客户端(Client)的,通常是字符串形式,accesstoken拥有特定的访问范围(scope),并且有时间限制,访问令牌可以有不同的格式、结构,这点并没有限制。RefreshToken刷新令牌 refreshtoken是一个用来获取accesstoken的凭证,同样它是由授权服务器(AuthorizationServer)颁发给客户端(Client)的,刷新令牌的时效性比访问令牌要长,当访问令牌过期的时候,可以直接用刷新令牌去授权服务器获取新的访问令牌,而无需重新登录。和访问令牌不同的是,授权服务器颁发访问令牌是必须的,而颁发刷新令牌则是可选的,并且访问令牌还会和资源服务器交互,而刷新令牌只和授权服务器交互。 刷新令牌的设计非常巧妙,它是用户体验和安全两方面取舍的一个平衡。(A)AuthorizationGrant(B)AccessTokenRefreshToken(C)AccessToken(D)ProtectedResourceResourceAuthorizationClientServerServer(E)AccessToken(F)InvalidTokenError(G)RefreshToken(H)AccessTokenOptionalRefreshToken (A)客户端向授权服务器发起请求,并提供授权许可。 (B)授权服务器对客户端进行身份验证并验证授权许可,如果有效,则颁发访问令牌和刷新令牌。 (C)客户端请求受保护资源并提供访问令牌。 (D)资源服务器验证这个访问令牌,如果有效,返回相应的内容。 (E)重复步骤(C)和(D),直到访问令牌过期。如果客户端知道了访问令牌已经过期,它跳到步骤(G),如果不知道,继续向资源服务器发起请求。 (F)由于访问令牌无效,资源服务器返回无效的令牌错误。 (G)客户端发起获取刷新令牌的请求,同时要带上当前的刷新令牌。 (H)授权服务器对客户端进行认证并验证刷新令牌,如果有效,则发出新的访问令牌和一个可选的新的刷新令牌。AuthorizationGrant授权许可 授权许可是一个资源所有者授权的凭证,客户端通过它去获取访问令牌(accesstoken),OAuth2。0定义了以下四种许可模式。AuthorizationCode授权码Implicit隐式ResourceOwnerPasswordCredentials密码ClientCredentials客户端凭证AuthorizationCodeGrant授权码模式 授权码模式是最常用的一种授权许可模式,也是最经典的一种,这种模式可以获取到访问令牌和刷新令牌。还有一个特点是,授权码模式是基于Web重定向的流程。 (A)客户端提供一个授权链接,引导用户点击跳转到授权服务的authorize端点,如下https:www。authorizationserver。comoauth2authorize?responsetypecodeclientids6BhdRkqt3scopeuserstate8b815ab1d177f5c8eredirecturihttps:www。client。comcallback 参数说明如下:responsetype:必选项,表示响应类型,此处的值固定为codeclientid:必选项,客户端的身份标识redirecturi可选项,经过用户允许授权后,授权服务器跳转到客户端的回调地址scope可选项,希望用户同意授权的权限范围state可选项,推荐使用,客户端可以维护一个在请求和回调之间的状态,授权服务器重定向到回调地址时,会带上这个参数,state可以防止跨站点请求伪造CSRF攻击。 (B)授权服务器提供授权页面,用户选择同意授权或者拒绝来自客户端的请求,如下所示 (C)假如用户同意了授权,授权服务器会通过url重定向到客户端的回调地址,并且会带上一个授权码code和state参数(如果之前客户端的请求中传递了state参数的话)https:www。client。comcallback?coded8c2afe6ecca004eb4bd7024state8b815ab1d177f5c8e (D)现在已经拿到了授权码code并获得了用户的授权,接下来需要用code来换取访问令牌accesstoken,可以向授权服务的token端点发送POST请求。POSTtokenHTTP1。1Authorization:BasicczZCaGRSa3F0MzpnWDFmQmF0M2JWContentType:applicationxwwwformurlencodedhttps:www。authorizationserver。comoauth2token?granttypeauthorizationcodecoded8c2afe6ecca004eb4bd7024redirecturihttps:www。client。comcallback 参数说明如下:granttype:必选项,表示授权类型,此处的值固定为authorizationcodecode:必选项,授权码,这是上一步从授权服务器传给回调地址(redirecturi)的参数redirecturi:必选项,客户端的回调地址,注意要和(A)步骤中的redirecturi一致。clientid:必选项,客户端的身份标识 注意,上面使用了HttpBasic身份认证(Authorization:Basic。。。),在本文的客户端身份认证部分有介绍,主要是为了验证Client的合法性。 通过code换取accesstoken步骤中,还有一种比较常见的身份验证做法是,直接在请求体中传入clientid,clientsecret,如下:POSTtokenHTTP1。1ContentType:applicationxwwwformurlencodedhttps:www。authorizationserver。comoauth2token?granttypeauthorizationcodecoded8c2afe6ecca004eb4bd7024clientids6BhdRkqt3clientsecretecca004eb4bd7024c2afe6eccredirecturihttps:www。client。comcallback (E)授权服务器对client,code验证通过后,会返回accesstoken和一个可选的refreshtoken,如下:HTTP1。1200OKContentType:charsetUTF8CacheControl:nostorePragma:nocache{accesstoken:2YotnFZFEjr1zCsicMWpAA,tokentype:bearer,expiresin:3600,refreshtoken:tGzv3JOkF0XG5Qx2TlKWIA} 参数介绍:accesstoken:必选项,访问令牌tokentype:令牌类型,通常是Bearer〔RFC6750〕,访问受保护资源需要在请求头设置(Authorization:Bearer。。。)expiresin:访问令牌的有效期,以秒为单位refreshtoken:可选的刷新令牌 (F)客户端使用accesstoken向资源服务器发起请求 (G)资源服务器验证accesstoken,验证通过后,返回受保护的资源 这里有一个问题是,文章上面说accesstoken只是一个字符串,那么资源服务器如何来验证该令牌?在OAuth2。0核心协议中,关于这点并没有提及。 访问令牌主要分为两种,一种是没有意义的随机字符串,比如2YotnFZFEjr1zCsicMWpAA,这种情况客户端本身是不能鉴别令牌是否有效,只能去授权服务器发起请求来验证该令牌,这种安全性高,但性能差,可以参考RFC7662。 第二种就是很常见的JWT令牌,可以参考RFC7519,令牌本身就包含了一些用户信息,资源服务器可以通过加密算法和签名验证令牌是否有效,而且不需要和授权服务器进行交互,但是缺点是,如果令牌在到期前被撤销,资源服务器是没办法知道的。ImplicitGrant隐式授权模式 上面是隐式授权的流程图,它和授权码模式很像,区别在于,授权码模式是先拿到code,然后再换取accesstoken,而隐式授权只用一次请求就拿到了accesstoken,通过url参数的形式返回,令牌也直接暴露在了浏览器地址栏,实际上这种模式是OAuth2。0对公开(public)的客户端的授权流程进行了优化,上面说到了客户端分为两种,机密的的和公开的,因为公开的客户端没有能力维护自己的机密凭证,所以适合这种模式,并且授权码模式需要客户端认证(通过code换取accesstoken的时候,需要使用HttpBasic认证,或者传入clientsecret),而隐式授权在整个流程中并没有客户端认证,所以是不安全也不推荐使用的。 请求参数: responsetype这里固定是tokenGEThttps:www。authorizationserver。comoauth2authorize?responsetypetokenclientids6BhdRkqt3scopeuserstate8b815ab1d177f5c8eredirecturihttps:www。client。comcallback 响应参数:https:www。client。comcallbackaccesstoken2YotnFZFEjr1zCsicMWpAAstate8b815ab1d177f5c8etokentypeBearerexpiresin3600 这里注意accesstoken实际上并不是一个url参数,它前面是号,表示一个fragment,有别于?,?后面的查询字符串会被网络请求发送到服务器,而fragment则不会发送到服务器,但是js是可以解析到fragment的值,也就是accesstoken,这个设计很巧妙!ResourceOwnerPasswordCredentialsGrant密码凭证模式ResourceOwnervResourceOwner(A)PasswordCredentialsv(B)ResourceOwnerPasswordCredentialsAuthorizationClientServer(C)AccessToken(wOptionalRefreshToken) 密码模式就更简单粗暴了,用户直接把账号密码告诉客户端,客户端向授权服务器发起POST请求,并携带用户名和密码,授权服务器验证通过后,返回访问令牌和可选的刷新令牌,这种模式的特点是,用户和客户端是高度信任的。 请求参数:POSTtokenHTTP1。1Host:www。authorizationserver。comAuthorization:BasicczZCaGRSa3F0MzpnWDFmQmF0M2JWContentType:applicationxwwwformurlencodedgranttypepasswordusernamejohndoepasswordA3ddj3w 响应参数:HTTP1。1200OKContentType:charsetUTF8CacheControl:nostorePragma:nocache{accesstoken:2YotnFZFEjr1zCsicMWpAA,tokentype:Bearer,expiresin:3600,refreshtoken:tGzv3JOkF0XG5Qx2TlKWIA}ClientCredentialsGrant客户端凭证模式(A)ClientAuthenticationAuthorizationClientServer(B)AccessToken 客户端凭证模式的特点是,客户端就是资源所有者,客户端访问资源也不需要用户的授权,因为这个过程中没有用户,资源本身就属于客户端,通过在请求体中传入clientid,clientsecret参数或者HttpBasic进行客户端认证,这种模式很适合后端服务或者api之间调用的场景。 请求参数: 此处的granttype固定是clientcredentialsPOSTtokenHTTP1。1Host:www。authorizationserver。comAuthorization:BasicczZCaGRSa3F0MzpnWDFmQmF0M2JWContentType:applicationxwwwformurlencodedgranttypeclientcredentials 响应参数:HTTP1。1200OKContentType:charsetUTF8CacheControl:nostorePragma:nocache{accesstoken:2YotnFZFEjr1zCsicMWpAA,tokentype:Bearer,expiresin:3600,exampleparameter:examplevalue}总结 本文介绍了OAuth2。0核心协议,主要参考RFC6749(TheOAuth2。0AuthorizationFramework)核心协议 ,相信读完本文,你会发现有些流程其实是不安全的,没错,其中的隐式授权和密码授权模式已经不再建议使用,因为隐式授权从一开始就没有真正安全过,这里介绍一下背景,当时OAuth2。0出现的时间点在2010年左右,移动端应用是全新的,单页面应用程序(SPA)也才刚开始出现,当时的Web生态和现在还是差别很大,由于技术问题,并不能使用常规的OAuth模式进行授权。对于现在来说,推荐使用专门为移动设备应用而设计的PKCE(RFC7636)模式,它是OAuth2。0核心的一个扩展协议,也是最近几年移动设备应用授权的最佳实践。 目前OAuth2。1也是一项正在进行中的工作,它围绕OAuth2。0对其授权功能进行加强和优化,下篇文章我会继续介绍OAuth2。1的新功能。