RFC协议:
- RFC6749 “OAuth 2.0”。官网:
https://tools.ietf.org/html/rfc6749
- RFC6750 “OAuth 2.0 授权框架:不记名令牌使用”
- RFC7519 “JSON Web 令牌(JWT)”
- RFC7636 “OAuth 公共客户端代码交换的证明密钥”
OAuth(Open Authorization)是一个关于授权(authorization)的开放网络标准,允许用户授权第三方应用访问他们存储在另外的服务提供者上的信息,而不需要将用户名和密码提供给第三方移动应用或分享他们数据的所有内容。OAuth在全世界得到广泛应用,目前的版本是2.0版。
协议特点:
- 简单:不管是OAuth服务提供者还是应用开发者,都很易于理解与使用;
- 安全:没有涉及到用户密钥等信息,更安全更灵活;
- 开放:任何服务提供商都可以实现OAuth,任何软件开发商都可以使用OAuth;
应用场景:
- 原生app授权:app登录请求后台接口,为了安全认证,所有请求都带token信息,如果登录验证、请求后台数据。
- 前后端分离单页面应用:前后端分离框架,前端请求后台数据,需要进行oauth2安全认证,比如使用vue、react后者h5开发的app
- 第三方应用授权登录,比如QQ,微博,微信的授权登录。
OAuth定义了四种角色
- resource owner:资源所有者,又称”用户”(user)。一个可以去授予访问受保护资源(protected resource)的实体,当着个owner是一个自然人时,它被称作是一个终端用户(end-user)。
- resource server:资源服务器,即服务提供商存放用户生成的受保护资源(例如用户照片或个人数据)的服务器。受保护资源的服务器,能够接收并响应使用访问令牌(access tokens)访问受保护资源(protected resource)的请求。
- client:客户端。一个可以代表resource owner并能构造访问受保护资源(protected resource)请求的应用。代表资源所有者(例如用户)访问受保护资源的应用程序。客户端可以托管在服务器、台式机、移动设备或其他设备上。”client”这个术语并没有确定的特征,即这个应用是否运行在服务器上,还是桌面或者其他设备上都可能。
- authorization server:授权服务器,即服务提供商专门用来处理授权的服务器。在对 resource owner 认证成功进而取得了授权之后,给 client 签发访问令牌(access tokens)的服务器。它与资源务器,可以是同一台物理服务器,也可以是不同的物理服务器。
术语
- Access token:用于访问受保护资源的令牌。
- Authorization code:当用户授权客户端代表他们访问受保护的资源时生成的中间令牌。客户端收到此令牌并将其交换为访问令牌。
- Grant:授权是一种获取访问令牌的方法。
- Scope:一个许可。
- JWT:(JSON Web Token)是一种在两方之间安全地表示声明的方法,如RFC 7519 中所定义。
协议流程
抽象OAuth 2.0流程描述了四个角色之间的交互。包括以下步骤:
- (A)客户端请求资源所有者授权。该授权请求可以直接直接呈现给资源拥有者,也可间接地通过授权服务器进行(例如跳转到授权服务器)。
- (B)客户端收到授权许可,即表示资源所有者授权的凭证,使用本规范中定义的四种授权类型之一或使用扩展授权类型表示。授权许可类型取决于客户端使用何种方法请求授权服务器,以及授权服务器支持哪些授权类型。
- (C)客户端通过向授权服务器进行认证并呈现用户赋予的权限来请求access token。
- (D)授权服务器验证客户端并验证用户赋予的权限,如果有效,则颁发access token。
- (E)客户端从资源服务器请求受保护资源,并通过呈现access token进行身份验证。
- (F)资源服务器验证access token,如果有效,则为该请求提供服务。
授权许可
权限授予( Authorization Grant)是资源拥有者同意授权请求(访问受保护资源)的凭据,客户端可以用它来获取access token。本规范定义了四种授权(grant)类型:授权码模式(authorization code),简化模式(implicit),密码模式(resource owner password credentials)和客户端模式(client credentials),以及用于定义其他类型的可扩展性机制。
授权码模式
授权码(Authorization Code)是通过授权服务器来获得的,授权服务器是客户端和资源拥有者之间的媒介。客户端不是直接从资源所有者请求授权,而是引导资源所有者至授权服务器(由在RFC2616中定义的user-agent),然后授权服务器将资源拥有者 redirect 到客户端 client,并带着授权码(authorization code)。
在将资源拥有者 redirect 到 client(附带authorization code)之前,授权服务器验证资源拥有者身份并获取授权。因为资源拥有者仅与授权服务器进行身份验证,所以资源拥有者的凭据(用户名、密码等)永远不会泄露给客户端(尤其是第三方客户端),也不需要与客户端分享。
授权码有一些重要的安全优势,比如验证客户端 client 的能力,比如直接将access token传送给client,而不是暴露给资源拥有者的user-agent,也不会将 access token 泄露给第三方。
它的步骤如下:
- 用户访问客户端,后者将前者导向认证服务器。
- 用户选择是否给予客户端授权。
- 假设用户给予授权,认证服务器将用户导向客户端事先指定的重定向URI,同时附上一个授权码。
- 客户端收到授权码,附上早先的重定向URI,向认证服务器申请令牌。这一步是在客户端的后台的服务器上完成的,对用户不可见。
- 认证服务器核对了授权码和重定向URI,确认无误后,向客户端发送访问令牌(access token)和更新令牌(refresh token)等。
步骤详情
第一:客户端申请认证的URI
包含以下参数:
- response_type:表示授权类型,必选项,此处的值固定为”code”。
- client_id:表示客户端的ID,必选项。(如微信授权登录,此ID是APPID)。
- redirect_uri:表示重定向URI,可选项。
- scope:表示申请的权限范围,可选项。
- state:表示客户端的当前状态,可以指定任意值,认证服务器会原封不动地返回这个值。
//示例: GET /authorize?response_type=code&client_id=s6BhdRkqt3&state=xyz&redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb HTTP/1.1 HTTP/1.1 Host: server.example.com //网站应用微信登录:请求CODE https://open.weixin.qq.com/connect/qrconnect?appid=APPID&redirect_uri=REDIRECT_URI&response_type=code&scope=SCOPE&state=STATE#wechat_redirect
第二:认证服务器回应客户端的URI
包含以下参数:
- code:表示授权码,必选项。该码的有效期应该很短,通常设为10分钟,客户端只能使用该码一次,否则会被授权服务器拒绝。该码与客户端ID和重定向URI,是一一对应关系。
- state:如果客户端的请求中包含这个参数,认证服务器的回应也必须一模一样包含这个参数。
示例: HTTP/1.1 302 Found Location: https://client.02405.com/cb?code=SplxlOBeZQQYbYS6WxSbIA&state=xyz
第三:客户端向认证服务器申请令牌的HTTP请求
包含以下参数:
- grant_type:表示使用的授权模式,必选项,此处的值固定为”authorization_code”。
- code:表示上一步获得的授权码,必选项。
- redirect_uri:表示重定向URI,必选项,且必须与A步骤中的该参数值保持一致。
- client_id:表示客户端ID,必选项。
//示例: POST /token HTTP/1.1 Host: server.02405.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%2Fcb //网站应用微信登录:通过code获取access_token https://api.weixin.qq.com/sns/oauth2/access_token?appid=APPID&secret=SECRET&code=CODE&grant_type=authorization_code
第四:认证服务器发送的HTTP回复
包含以下参数:
- access_token:表示访问令牌,必选项。
- token_type:表示令牌类型,该值大小写不敏感,必选项,可以是bearer类型或mac类型。
- expires_in:表示过期时间,单位为秒。如果省略该参数,必须其他方式设置过期时间。
- refresh_token:表示更新令牌,用来获取下一次的访问令牌,可选项。
- scope:表示权限范围,如果与客户端申请的范围一致,此项可省略。
//示例: 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" }
从上面代码可以看到,相关参数使用JSON格式发送(Content-Type: application/json)。此外,HTTP头信息中明确指定不得缓存。
网站应用微信登录:返回样例
{ "access_token":"ACCESS_TOKEN", "expires_in":7200, "refresh_token":"REFRESH_TOKEN", "openid":"OPENID", "scope":"SCOPE", "unionid": "o6_bmasdasdsad6_2sgVt7hMZOPfL" }
更新令牌
如果用户访问的时候,客户端的访问令牌 access_token 已经过期,则需要使用更新令牌refresh_token申请一个新的访问令牌。客户端发出更新令牌的HTTP请求,包含以下参数:
- granttype:表示使用的授权模式,此处的值固定为”refreshtoken”,必选项。
- refresh_token:表示早前收到的更新令牌,必选项。
- scope:表示申请的授权范围,不可以超出上一次申请的范围,如果省略该参数,则表示与上一次一致。
//示例: POST /token HTTP/1.1 Host: server.02405.com Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW Content-Type: application/x-www-form-urlencoded grant_type=refresh_token&refresh_token=tGzv3JOkF0XG5Qx2TlKWIA //微信刷新Token: https://api.weixin.qq.com/sns/oauth2/refresh_token?appid=APPID&grant_type=refresh_token&refresh_to
隐式授权模式
隐式授权模式,又名简化模式,grant类型为implicit。隐式授权模式用于获取访问令牌(不支持签发刷新令牌),并且对操作特定重定向URI的公共客户端进行了优化,这类的客户端一般使用类似JavaScript的脚本语言在浏览器上实现。由于该模式依赖重定向,因此客户端必须有能力与资源所有者的user-agent(如web浏览器)进行交互,并且有接收授权服务器请求(通过重定向)的能力。隐式授权流程中,不再给客户端颁发授权码 authorization code,取而代之的是客户端直接被颁发访问令牌 access token(作为资源所有者的授权),不支持签发刷新令牌。这种许可类型是隐式的,因为没有获取 authorization code中间环节,而是通过授权请求直接获取访问令牌。
隐式授权流程中颁发 access token 时,授权服务器没有对客户端 client 进行身份验证,它依赖资源所有者的认证和注册时的重定向URI。由于访问令牌会在重定向URI中,因此它可能会暴露给资源所有者 user-agent 或者对资源所有者的user-agent 有访问权限的其他应用程序。
隐式授权提高了一些客户端(例如一个作为浏览器内应用实现的客户端)的响应速度和效率,因为它减少了获取访问令牌 access token 所需的往返数。但是,应该权衡其便捷性与安全隐患之间的利害关系,尤其是授权码模式可用时。
隐式授权模式通常适用于同一个公司自有系统之间的认证,尤其是客户端应用不能安全存储令牌信息的时候。客户端需要client id申请授权。
适用场景
- 用户参与:使用隐式授权需要与用户交互,用户对授权服务器进行登录与授权。
- 单页应用:SPA前端,没有后端或者后端属于授权方。
- 客户端密码:访问授权时,不需要带第三方应用secret,前提是资源服务校验token使用的client信息与客户端(第三方应用)不同,且配置了secret。
- 前端:必须要有前端,否则无法使用授权功能。
- 客户端后端:Options,仅当应用前后端不分离MVC场景。
- 资源所属方:授权方。
步骤详细
第一:授权请求
客户端使用”application/x-www-form-urlencoded”格式,去构造授权端点的请求参数:
- response_type:必须。值必须为”token”。
- client_id:必须。客户端标识。
- redirect_uri:可选。
- scope:可选。请求访问的范围。
- state:建议。客户端用来维护请求和回调之间状态的不透明值。授权服务器将user-agent重定向回客户端时会携带该值。该值一般用于防止跨站请求伪造攻击。
客户端通过HTTP重定向的响应或其它可行的方式,将资源所有者的user-agent引导到构造的URI。
比如,客户端引导user-agent通过TLS发起如下请求:
GET /authorize?response_type=token&client_id=s6BhdRkqt3&state=xyz&redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb HTTP/1.1 Host: server.02405.com
授权服务器需要校验请求中所有必须的参数是否传入且有效,并且要校验即将用于传递访问令牌的重定向URI,是否与客户端注册时提供的重定向URI完全匹配。
如果请求校验通过,授权服务器将引导资源所有者进行授权,并且让其做出授权决策(通过询问资源所有者,或建立其它有效的授权方式)。
当用户完成其授权决策后,授权服务器将通过HTTP重定向或其它可行的方式,将user-agent引导到重定向URI。
第二:访问令牌响应
如果资源所有者授权了访问请求,授权服务器需要签发访问令牌,并以”application/x-www-form-urlencoded
“的格式添加到重定向URI的查询组件部分:
- access_token:必须。授权服务器签发的访问令牌。
- token_type:必须。令牌类型,大小写敏感。
- expires_in:建议。访问令牌的寿命,以秒为单位。比如,值为”3600″,代表访问令牌将在响应后的一小时后过期。如果忽略该值,则授权服务器比如通过其它方式实践过期时间,或者在文档中明确默认的过期时间。
- scope:当与请求中列举的范围相同时,则该返回值可选;否则必须返回。
- state:当授权请求中包含该字段时,必须原样返回。
授权服务器不得签发刷新令牌。
比如,授权服务器通过如下的HTTP响应对user-agent进行重定向:
HTTP/1.1 302 Found Location: https://02405.com/cb#access_token=2YotnFZFEjr1zCsicMWpAA&state=xyz&token_type=example&expires_in=3600
开发者必须注意,某些 user-agent 并不支持在 HTTP 的 Location 头中包含 fragment 组件,这一类的客户端不能使用 3xx 重定向响应,而是需要提供其它方式对 user-agent 进行重定向,比如返回一个包含’continue’按钮的HTML页面,在点击后跳转到重定向URI。
客户端必须忽略未定义的响应参数。访问令牌的长度并未在该规范中定义,因此客户端应该避免假设该字段的长度。授权服务器应明确签发的所有值的长度。
第三:错误响应
如果授权请求由于重定向URI或客户端标识参数存在问题(丢失、无效或不匹配)导致请求失败,授权服务器应该告知资源所有者出现异常,而不是自动将 user-agent 重定向到无效的重定向URI。
如果资源所有者拒绝授权,或出现除丢失、无效重定向URI以外的异常,授权服务器应该以”application/x-www-form-urlencoded”格式,通过以下参数告知客户端异常原因:
- error:必须。参数值包含的字符必须在%x20-21 /%x23-5B /%x5D-7E范围内。值为如下简单的ASCII错误码:
- invalid_request:缺少必须的参数,包含无效的参数值,使用同名参数,以及其它问题。
- unauthorized_client:客户端没有使用该方法获取访问令牌的权限。
- access_denied:资源所有者拒绝授权。
- unsupported_response_type:授权服务器不支持通过该方法获取访问令牌。
- invalid_scope:请求的授权范围无效或未定义。
- server_error:授权服务器发生意外情况,无法完成请求(之所以需要该错误代码,是因为无法通过HTTP重定向将500内部服务器错误状态返回给客户端)。
- temporarily_unavailable:由于服务器临时过载或维护,授权服务器当前无法处理该请求(之所以需要该错误代码,是因为无法通过HTTP重定向将503服务器不可用状态返回给客户端)。
- error_description:可选。可理解的ASCII文本,用于提供额外的信息,帮助客户端开发者理解发生了何种异常。error_description参数值包含的字符必须在%x20-21 /%x23-5B /%x5D-7E范围内。
- error_uri:可选。包含错误信息的web界面的URI,用于向客户端开发人员提供额外的错误信息。error_uri参数值必须符合URI-reference语法,并且包含的字符必须在%x21 /%x23-5B /%x5D-7E范围内。
- state:如果客户端在授权请求中携带该参数,则必须原样返回。
比如,授权服务器通过如下HTTP响应对user-agent进行重定向:
HTTP/1.1 302 Found Location: https://client.02405.com/cb#error=access_denied&state=xyz
密码授权模式
密码授权模式,(resource owner password credentials)。资源拥有者密码凭据(如用户名和密码),可以直接作为获取访问令牌 access token的授权许可方式。这种凭据只能应该当资源所有者和客户端 client 之间具有高度信任时(例如,客户端是设备的操作系统的一部分,或者是一个高度特权应用程序),以及当其他授权许可类型(例如授权码)不可用时被使用。
尽管这种授权类型需要client直接接触资源拥有者的凭据,资源拥有者的凭据仅被用于单次的获取access token的请求。通过使用用户凭据来交换具有较长寿命的访问令牌 access token或者刷新令牌refresh token,这种授权模式可消除client在将来需要授权时对资源拥有者凭据的需求(就是说,这次通过用户凭据获取了access token,以后就可以直接通过access token而不是用户凭据来访问受限资源了)。
OAuth2 密码授权机制可以让你自己的客户端(如移动应用程序)使用邮箱地址或者用户名和密码获取访问令牌。如此一来你就可以安全地向自己的客户端发出访问令牌,而不需要遍历整个 OAuth2 授权代码重定向流程。
密码模式的授权步骤如下:
- (A)用户向客户端提供用户名和密码。
- (B)客户端将用户名和密码发给认证服务器,向后者请求令牌。
- (C)认证服务器确认无误后,向客户端提供访问令牌。
步骤详细
第一:发出请求
客户端发出的HTTP请求,包含以下参数:
- grant_type:表示授权类型,此处的值固定为”password”,必选项。
- username:表示用户名,必选项。
- password:表示用户的密码,必选项。
- scope:表示权限范围,可选项。
POST /token HTTP/1.1 Host: server.02405.com Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW Content-Type: application/x-www-form-urlencoded grant_type=password&username=codingl0&password=kdkas-Ld
第二:访问令牌响应
认证服务器向客户端发送访问令牌:
- token_type:令牌类型,该值大小写不敏感,必选项。一般是 Bearer。
- expires_in:过期时间,单位为秒。
- access_token:访问令牌,必选项。
- refresh_token:更新令牌,用来获取下一次的访问令牌,可选项。
HTTP/1.1 200 OK Content-Type: application/json;charset=UTF-8 Cache-Control: no-store Pragma: no-cache { "token_type": "Bearer", "access_token":"2YotnFZFEjr1zCsicMWpAA", "expires_in":3600, "refresh_token":"tGzv3JOkF0XG5Qx2TlKWIA" }
第三:刷新 token
刷新令牌是用于获取访问令牌的凭据。刷新令牌授权发布到客户机的服务器,用于获得一个新的访问令牌时,当前的访问令牌变得无效或过期,或获得额外的访问令牌相同或窄范围(访问令牌可能生存期较短,和更少的比资源所有者授权的权限)。根据授权服务器的判断,发出刷新令牌是可选的。
简单理解就是,若 access_token 的 expires_in 时间到了,则 access_token 失效,需要使用 refresh_token 重新获取 access_token 。
客户端调用刷新token接口
https://www.02405.com/v1/oauth/token?grant_type=refresh_token&client_id=CLIENT_ID&client_secret=CLIENT_SECRET&refresh_token=REFRESH_TOKEN
- client_id:客户端的ID,必选项。
- client_secret:客户端的密钥,必选项。
- grant_type:表示使用的授权模式,此处的值固定为“refreshtoken”,必选项。
- refresh_token:表示早前收到的更新令牌,必选项。
响应客户端数据
{ "access_token":"2YotnFZFEjr1zCsicMWpAA", "token_type":"example", "expires_in":3600, "refresh_token":"tGzv3JOkF0XG5Qx2TlKWIA", "scope":"basic" }
- access_token:访问令牌,必选项。
- token_type:令牌类型,该值大小写不敏感,必选项。
- expires_in:过期时间,单位为秒。
- refresh_token:更新令牌,用来获取下一次的访问令牌,可选项。
- scope:权限范围,如果与客户端申请的范围一致,此项可省略。
客户端凭证模式
客户端凭证模式(Client Credentials Grant)。当客户端是资源所有者时,或者当授权范围限于受客户端控制的受保护资源时,客户端 client 可以仅使用自身凭证(或其他支持的身份验证手段)向”授权服务器”请求访问令牌 access token。
客户端凭证模式,必须仅在机密客户端使用。客户端通过client id和client secret申请授权,获得的访问令牌 access token 的值,一般是固定不变的。常用于针对第三方应用的,而不是针对用户的,即有可能多个用户共享同一个令牌。
它的步骤如下:
- (A)客户端向授权服务器进行身份认证,并从令牌端点 token endpoint 请求一个访问令牌。
- (B)授权务器确验证客户端,如果有效,向客户端发出访问令牌。
使用场景
当对于我们针对一个非常信任的第三方去登陆时,可以采用这种模式。例如:某服务方授权给第三方服务调用本地OpenAPI。或者微信授权给公众号调用微信API。
使用方法
首先服务方要提供给第三方一个client_id 和 client_secret ,相当于公用的用户名密码。
接着第三方拿着 client_id 和 client_secret 去服务方处换取令牌。
最后第三方拿着令牌去调用接口或获取数据。
步骤详细
第一:授权请求及响应
由于客户端认证被用作 authorization grant ,所以不需要额外的授权请求。
第二:访问令牌请求
客户端通过建立一个特定的请求并把该请求发送给令牌端点(token endpoint)来获取访问令牌。该特定请求的内容要求:在HTTP request entity-body中以UTF-8
字符编码并按”application/x-www-form-urlencoded
“规定的格式添加以下参数:
- grant_type:表示授权类型,值为”client_credentials”,必选项。
- scope:表示权限范围,可选项。
例如,客户端建立如下传输层安全性(transport-layer security)(带有只用于显示目的的额外的换行符)的HTTP请求:
POST /token HTTP/1.1 Host: server.example.com Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW Content-Type: application/x-www-form-urlencoded grant_type=client_credentials
授权(认证)服务器必须以某种方式认证客户端身份。
第三:访问令牌响应
如果访问令牌请求是有效的并已被授权的,则授权服务器发出访问令牌。不应包括刷新令牌。如果客户端身份验证请求失败或无效,授权服务器将返回错误响应。
An example successful response: 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" }
访问令牌
访问令牌 access token 是用来访问受限资源的凭据。access token是一个代表授予客户端 client 的权限的字符串。该字符串通常对 client 不透明。access token 表示特定范围和持续时间的访问权限,由资源所有者授予,由资源服务器和授权服务器执行。
访问令牌 access token 可以表示用于检索授权信息的标识符,或者以可验证的方式自包含授权信息(即,由一些数据和签名组成的字符串)。client可能需要额外的身份验证凭据(超出本规范的范围)来使用令牌。
访问令牌 access token 提供了一个抽象层,使资源服务器可以理解的令牌,来替换其他不同的身份验证方式(如用户名+密码方式)。这种抽象使得颁发访问令牌比用于获取令牌的授权许可更具限制性,并且消除了资源服务器理解各种不同身份验证方法的需要。
基于资源服务器的安全要求,访问令牌 access token 可以有不同的格式、结构和使用方法(例如,加密属性)。访问令牌属性和用于访问受保护资源的方法,在RFC6750等配套规范中定义。
刷新令牌
刷新令牌 refresh token 是由授权服务器颁发给客户端,用于在当前访问令牌 access token 失效或过期时,获取一个新的访问令牌access token,或者获得相等或更窄范围的额外的访问令牌 access token(访问令牌可能具有比资源所有者所授权的更短的生命周期和更少的权限)。颁发刷新令牌是可选的,由授权服务器决定。如果授权服务器颁发刷新令牌,在颁发访问令牌时它被包含在内。
刷新令牌 refresh token 是一个代表由资源所有者给客户端许可的授权的字符串。该字符串通常对于客户端是不透明的。该令牌表示一个用于检索授权信息的标识符。不同于访问令牌,刷新令牌设计只与授权服务器使用,并不会发送到资源服务器。
所示流程包含以下步骤:
- (A)客户端通过与授权服务器进行身份验证并出示授权许可请求访问令牌。
- (B)授权服务器对客户端进行身份验证并验证授权许可,若有效则颁发访问令牌和刷新令牌。
- (C)客户端通过出示访问令牌向资源服务器发起受保护资源的请求。
- (D)资源服务器验证访问令牌,若有效则满足该要求。
- (E)步骤(C)和(D)重复进行,直到访问令牌到期。如果客户端知道访问令牌已过期,跳到步骤(G),否则它将继续发起另一个对受保护资源的请求。
- (F)由于访问令牌是无效的,资源服务器返回无效令牌错误。
- (G)客户端通过与授权服务器进行身份验证并出示刷新令牌,请求一个新的访问令牌。客户端身份验证要求基于客户端的类型和授权服务器的策略。
- (H)授权服务器对客户端进行身份验证并验证刷新令牌,若有效则颁发一个新的访问令牌(和——可选地——一个新的刷新令牌)。
客户注册
在启动协议交互之前,client需要先去授权服务器 authorization server 那里进行注册。client注册的方式使用授权服务器超出了本规范的范围,但通常涉及终端用户与HTML注册表单的交互。
所谓的客户注册并不需要授权服务器(authorization server)与客户端(client)之间进行直接交互。当授权服务器支持时,注册可以依靠其他方式建立信任并获得所需的客户端属性(比如重定向URI,客户端类型)。例如,注册可以通过使用自签发(self-issued)或第三方签发(third-party-issued)的断言(assertion)来完成,或由授权服务器通过使用一个受信任的通道来扮演一个客户端发现的角色。
在进行客户端注册时,客户端开发人员应提供如下的信息:
- 客户端类型(client type)。
- 客户端的重定向 URI。
- 包括授权服务器所需的任何其他信息(例如,应用程序名称,网站,说明,徽标图片,接受法律条款)。
客户端类型
根据客户端与授权服务器安全地进行身份验证的能力(即维护客户端凭据机密性的能力),OAuth定义了两种客户端类型(Client Type):
- 机密客户端:能够维持其凭据机密性(如客户端执行在具有对客户端凭据有限访问权限的安全的服务器上),或者能够使用其他方式保证客户端身份验证的安全性。
- 公开客户端:不能够维持其凭据的机密性(如客户端执行在由资源所有者使用的设备上,例如已安装的本地应用程序或基于Web浏览器的应用),且不能通过其他方式保证客户端身份验证的安全性。
客户端类型的选择基于授权服务器的安全身份认证定义以及其对客户端凭据可接受的暴露程度。授权服务器不应该对客户端类型做假设。客户端可以以分布式的组件集合实现,每一个组件具有不同的客户端类型和安全上下文(例如,一个同时具有机密的基于服务器的组件和公开的基于浏览器的组件的分布式客户端)。如果授权服务器不提供对这类客户端的支持,或不提供其注册方面的指导,客户端应该注册每个组件为一个单独的客户端。
本文档会围绕如下的客户端的配置户或者说是从应用的角度划分的客户端类型:
- Web应用程序(web application):是一个运行在Web服务器上的机密客户端(confidential client)。资源所有者通过其使用的设备上的用户代理(user-agent)里渲染的HTML用户界面访问客户端。客户端凭据以及向客户端颁发的任何访问令牌都存储在Web服务器上,且不会暴露给资源所有者或者被资源所有者可访问。
- 基于用户代理的应用(user-agent-based application):是一个公开客户端,客户端的代码从Web服务器下载,并在资源所有者使用的设备上的用户代理user-agent(如Web浏览器)中执行。协议数据和凭据对于资源所有者是可轻易访问的(且经常是可见的)。由于这些应用驻留在 user-agent 用户代理内,在请求授权时它们可以无缝地使用用户代理的功能。
- 本机应用程序(native application):是一个安装、运行在资源所有者使用的设备上的公开客户端。协议数据和凭据对于资源所有者是可访问的。假定包含在应用程序中的任何客户端身份认证凭据可以被提取。另一方面,动态颁发的如访问令牌或者刷新令牌等凭据可以达到可接受的保护水平。至少,这些凭据被保护不被应用可能与之交互的恶意服务器接触。在一些平台上,这些凭证可能被保护免于被驻留在相同设备上的其他应用接触。
User Agent中文名为用户代理,是Http协议中的一部分,属于头域的组成部分,User Agent也简称UA。它是一个特殊字符串头,是一种向访问网站提供你所使用的浏览器类型及版本、操作系统及版本、浏览器内核、等信息的标识。通过这个标识,用户所访问的网站可以显示不同的排版从而为用户提供更好的体验或者进行信息统计;例如用手机访问谷歌和电脑访问是不一样的,这些是谷歌根据访问者的 UA 来判断的。UA可以进行伪装。浏览器的UA字串的标准格式:浏览器标识(操作系统标识;加密等级标识;浏览器语言)渲染引擎标识版本信息。但各个浏览器有所不同。
客户端标识
授权服务器给前来注册的客户端颁发了一个客户端标识符(client id),一个代表客户端提供的注册信息的唯一字符串。客户端标识不是一个密文,它暴露给资源所有者并且不能单独用于客户端身份验证。客户端标识对于授权服务器是唯一的。
客户端的字符串大小本规范未定义。客户端应该避免对标识大小做假设。授权服务器应记录其发放的任何标识的大小。
客户端身份验证
如果客户端为机密类型的(confidential client),客户端和授权服务器建立适合于授权服务器的安全性要求的客户端身份验证方法。授权服务器可以接受符合其安全要求的任何形式的客户端身份验证。
机密类型的客户端通常颁发(或建立)一组客户端凭据用于与授权服务器进行身份验证(例如,密码、公/私钥对)。授权服务器可以与公共客户端建立客户端身份验证方法。然而,授权服务器不能依靠公共客户端身份验证达到识别客户端的目的。
客户端在每次请求中不能使用一个以上的身份验证方法。
(一)客户端密码
拥有客户端密码的客户端可以使用RFC2617中定义的HTTP基本身份验证方案与授权服务器进行身份验证。客户端标识使用按照“application/x-www-form-urlencoded”编码算法编码,编码后的值用作用户名;客户端密码使用相同的算法编码并用作密码。授权服务器必须支持HTTP基本身份验证方案,用于验证被颁发客户端密码的客户端的身份。例如(额外的换行仅用于显示目的):Authorization: Basic czZCaGRSa3F0Mzo3RmpmcDBaQnIxS3REUmJuZlZkbUl3
。
此外,授权服务器可以使用下列参数支持在请求正文中包含客户端凭据:
client_id
。必需的。客户端注册过程中颁发给客户端的客户端标识。client_secret
。必需的。客户端秘密。客户端可以忽略该参数若客户端秘密是一个空字符串。
使用这两个参数在请求正文中包含客户端凭据是不被建议的,应该限于不能直接采用HTTP基本身份验证方案(或其他基于密码的HTTP身份验证方案)的客户端。参数只能在请求正文中传送,不能包含在请求URI中。
例如,使用请求正文参数请求刷新访问令牌(额外的换行仅用于显示目的):
POST /token HTTP/1.1 Host: server.example.com Content-Type: application/x-www-form-urlencoded grant_type=refresh_token&refresh_token=tGzv3JOkF0XG5Qx2TlKWIA &client_id=s6BhdRkqt3&client_secret=7Fjfp0ZBr1KtDRbnfVdmIw
当使用密码身份验证发送请求时,授权服务器必须要求使用如1.6所述的TLS。由于该客户端身份验证方法包含密码,授权服务器必须保护所有使用到密码的端点免受暴力攻击。
(二)其他身份验证方法
授权服务器可以支持任何与其安全要求匹配的合适的HTTP身份验证方案。当使用其他身份验证方法时,授权服务器必须定义客户端标识(注册记录)和认证方案之间的映射。
传统用户认证
用户登录后,怎么跟踪用户的状态呢?我们都是知道 http 协议作为技术背景的web应用程序请求——应答模式是无状态的,于是引入 cookies、session 等机制,辨别客户端的身份,跟踪用户状态,去实现由状态的web应用。随着Web,应用程序,已经移动端的兴起,基于服务器端 session 验证的方式逐渐暴露出了问题:
- seesion方式:每次认证用户发起请求时,服务器需要去创建一个记录来存储信息。当越来越多的用户发请求时,内存的开销也会不断增加。
- 可扩展性:在服务端的内存中使用 seesion 存储登录信息,伴随而来的是可扩展性问题。
- CSRF(跨站请求伪造):用户在访问银行网站时,他们很容易受到跨站请求伪造的攻击,并且能够被利用其访问其他的网站。
- CORS(跨域资源共享):当我们需要让数据跨多台移动设备上使用时,跨域资源的共享会是一个让人头疼的问题。在使用Ajax抓取另一个域的资源,就可以会出现禁止请求的情况。
Token认证
token 访问资源的凭据,是服务端生成的一串字符串。使用基于 Token 的身份验证方法。大概的流程是这样的:
- 客户端使用用户名跟密码请求登录。
- 服务端收到请求,去验证用户名与密码。验证成功后,服务端会签发一个 token,再把这个 token 发送给客户端。
- 客户端收到 token 以后可以把它存储起来,比如放在
Cookie
里或者Local Storage
里。 - 客户端每次向服务端请求资源的时候,需要带着服务端签发的 token。
- 服务端收到请求,然后去验证客户端请求里面带着的 token,如果验证成功,就向客户端返回请求的数据。
当我们在程序中认证了信息并取得 token 之后,我们便能通过这个 token 做许多的事情。我们甚至能基于创建一个基于权限的 token 传给第三方应用程序,这些第三方程序能够获取到我们的数据(当然只有在我们允许的特定的 token )
token验证优点
token验证相对于 session/cookie 验证的优点
- 无状态:session有状态的,需要在服务端储存状态;token无状态,不需要在服务端储存,节约开销。
- 多平台跨域:cookie验证是不允许垮域访问;token验证能实现跨域资源共享,可扩展性更强。
- 安全性:请求中发送 token 而不再发送 cookie,能够防止 CSRF 攻击(跨站请求伪造)。token放入header请求中,是无法被直接伪造的。即使在客户端使用 cookie存储 token ,cookie也仅仅是一个存储机制而不是用于认证。不将信息存储在Session中,让我们少了对session操作。token 是有时效的,一段时间之后用户需要重新验证。我们也不一定需要等到 token 自动失效, token 有撤回的操作,通过 token revocataion可以使一个特定的 token 或是一组有相同认证的 token 无效。
- 去耦:不需要绑定到一个特定的身份验证方案。Token可以在任何地方生成,只要在你的API被调用的时候,你可以进行Token生成调用即可。
- 性能:在网络传输的过程中,性能更好。
- 基于标准化:你的API可以采用标准化的 JSON Web Token(JWT)。这个标准已经存在多个后端库(.NET, Ruby, Java,Python, PHP)和多家公司的支持(如: Firebase,Google, Microsoft)
token验证缺陷
- 占带宽:正常情况下要比 session_id 更大,需要消耗更多流量,挤占更多带宽,假如你的网站每月有 10 万次的浏览器,就意味着要多开销几十兆的流量。听起来并不多,但日积月累也是不小一笔开销。实际上,许多人会在 JWT 中存储的信息会更多。
- 性能问题:JWT 的卖点之一就是加密签名,由于这个特性,接收方得以验证 JWT 是否有效且被信任。但是大多数 Web 身份认证应用中,JWT 都会被存储到 Cookie 中,这就是说你有了两个层面的签名。听着似乎很牛逼,但是没有任何优势,为此,你需要花费两倍的 CPU 开销来验证签名。对于有着严格性能要求的 Web 应用,这并不理想,尤其对于单线程环境。
- 无法在服务端注销,那么久很难解决劫持问题。
Token验证——JWT方法
JWT(JSON Web Tokens)就是一个加密的字符串,作为验证信息在计算机之间传递,只有可以访问持有对应正确的加密密钥的计算机才能对其进行解密,从而验证携带这个令牌(Token)的请求是否合法。
JWT 标准的 Token 由三个部分组成:header.payload.signature:
- header(头部)
- payload(数据)
- signature(签名)
中间用.(点)分隔开,并且都会使用 Base64 编码,所以真正的 Token 看起来像这样:
eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJuaW5naGFvLm5ldCIsImV4cCI6IjE0Mzg5NTU0NDUiLCJuYW1lIjoid2FuZ2hhbyIsImFkbWluIjp0cnVlfQ.SwyHTEx_RQppr97g4J5lKXtabJecpejuef8AqKYMAJc
Header
每个 JWT token 里面都有一个 header,也就是头部数据。里面包含了使用的算法,这个 JWT 是不是带签名的或者加密的。主要就是说明一下怎么处理这个 JWT token 。
唯一在头部里面要包含的是 alg 这个属性,如果是加密的 JWT,这个属性的值就是使用的签名或者解密用的算法。如果是未加密的 JWT,这个属性的值要设置成 none。
{ "alg": "HS256" }
意思是这个 JWT 用的算法是 HS256,在base64url编码之后变成:eyJhbGciOiJIUzI1NiJ9
Payload
Payload 里面是 Token 的具体内容,这些内容里面有一些是标准字段,你也可以添加其它需要的内容。
- iss:Issuer,发行者
- sub:Subject,主题
- aud:Audience,观众
- exp:Expiration time,过期时间
- nbf:Not before
- iat:Issued at,发行时间
- jti:JWT ID
两个自定义的字段,一个是 name ,还有一个是 admin 。
{ "iss": "ninghao.net", "exp": "1438955445", "name": "wanghao", "admin": true }
使用 base64url 编码以后就变成了这个样子:eyJpc3MiOiJuaW5naGFvLm5ldCIsImV4cCI6IjE0Mzg5NTU0NDUiLCJuYW1lIjoid2FuZ2hhbyIsImFkbWluIjp0cnVlfQ
Signature
JWT 的最后一部分是 Signature ,这部分内容有三个部分:
- 第一部分:Base64 编码的header。
- 第二部分:Base64 编码的payload。
- 第三部分:再用加密算法加密一下,加密的时候要放进去一个 Secret ,这个相当于是一个密码,这个密码秘密地存储在服务端,不得泄露。
组成结构是这样子:
const encodedString = base64UrlEncode(header) + "." + base64UrlEncode(payload); // 这里的 HMACSHA256() 就是我们在第一部分定义的加密算法。 HMACSHA256(encodedString, 'secret');
Signature部分处理完成以后看起来像这样:SwyHTEx_RQppr97g4J5lKXtabJecpejuef8AqKYMAJc
最后这个在服务端生成并且要发送给客户端的 Token 看起来像这样:(由以上三部分组成)
eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJuaW5naGFvLm5ldCIsImV4cCI6IjE0Mzg5NTU0NDUiLCJuYW1lIjoid2FuZ2hhbyIsImFkbWluIjp0cnVlfQ.SwyHTEx_RQppr97g4J5lKXtabJecpejuef8AqKYMAJc
客户端收到这个 Token 以后把它存储下来,下回向服务端发送请求的时候就带着这个 Token 。服务端收到这个 Token ,然后进行验证,通过以后就会返回给客户端想要的资源。