概述
OAuth2.0 是一种授权机制,其内部引入了一个授权层,可以分离两种不同的角色:客户端和资源所有者。资源所有者同意客户端(第三方应用程序)的请求以后,资源服务器就可以向客户端颁发短期的进入令牌(token),用来代替密码,客户端通过这个令牌,去请求有限的资源数据来使用。所以OAuth2.0 的核心就是向第三方应用颁发令牌。
#@ OAuth2.0发展历程
OAuth(Open Authorization)--开放授权,这是一种关于开放授权的协议标准。 所谓的OAuth2.0,表示这是OAuth协议第2版,但并不兼容之前的OAuth1.0协议,即现在已经完全废止了OAuth1.0协议。
那么为什么会废止OAuth1.0协议呢?我们先来简单说一下OAuth协议的发展历程。
2007年12月的时候,OAuth协议已经诞生了,并与2010年4月份,被IETF(国际互联网工程任务组)认可并作为认证授权的标准发布。但是这个OAuth1.0协议有些致命缺陷,就是它的签名逻辑特别复杂,开发时对程序员来说极度恶心。而且OAuth1.0的授权流程是单一,存在较大的漏洞,容易被黑客攻击。所以在2012年10月的时候,IETF又更新发布了OAuth2.0协议,在这个版本中,放弃了之前复杂的数字签名和加密方案,使用HTTPS来作为安全保障手段,这降低了程序员的开发难度。但是OAuth2.0协议与OAuth1.0协议互不兼容,所以现在我们进行开发时,都是采用OAuth2.0协议,而不再采用OAuth1.0协议。
OAuth2.0功能
通过上一小节,我们了解到OAuth是一种关于开放授权的协议,该协议其实是一个 服务提供商 授权 第三方应用 获取 资源所有者 部分访问权限 的授权机制。通俗的说,就是OAuth协议允许用户在不提供密码给第三方应用的情况下,使得第三方应用有权获取用户数据等基本信息。在整个授权过程中,第三方应用都不必获取用户的密码,就可以取得用户部分资源的使用权限,所以OAuth是一种安全开发的协议。
OAuth2.0使用场景
我们可以总结一下OAuth2.0的使用场景,大致如下:
- 第三方应用登录: 比如利用QQ,微博,微信授权登录到其他网站或App。
- 分布式或微服务项目中授权: 在Java分布式或微服务开发时,后端业务拆分成若干服务,服务之间或前端进行请求调用时,为了安全认证,可以利用OAuth2.0进行认证授权。
OAuth2.0核心概念(重点)
- 客户端/第三方应用: 获取用户信息, 如果我们使用QQ账号来登录CSDN网站,这时候CSDN相对于QQ来说就是第三方应用。
- 服务提供商: 对外提供请求的服务器,比如上面说的QQ。
- Resource Owner: 资源所有(拥有)者,首先我们要明确“资源”的含义,“资源”是指我们请求的各种URL接口及各种数据,这里的资源所有者,也就是登录用户。
- Authorization Server: 认证服务器,即服务提供商(比如QQ)专门用来处理认证的服务器。
- Resource Server: 资源服务器,即服务提供商存放用户资源的服务器。它与认证服务器,可以是同一台服务器,也可以是不同的服务器。
- Access token: 访问令牌,供客户端用来请求Resource Server(API)的资源,Access token通常是short-lived短暂的。
访问令牌(access token)
访问令牌是在用户授权许可下,授权服务器下发给客户端的一个授权凭证,该令牌所要表达的意思是“用户授予该APP在多少时间范围内允许访问哪些与自己相关的服务”,所以访问令牌主要在 时间范围 和 权限范围 两个维度进行控制,此外访问令牌对于客户端来说是非透明的,外在表现就是一个字符串,客户端无法知晓字符串背后所隐藏的用户信息,因此不用担心用户的登录凭证会因此而泄露。
刷新令牌(refresh token)
刷新令牌的作用在于更新访问令牌,访问令牌的有效期一般较短,这样可以保证在发生访问令牌泄露时,不至于造成太坏的影响,但是访问令牌有效期设置太短存在的副作用就是用户需要频繁授权,虽然可以通过一定的机制进行静默授权,但是频繁的调用授权接口,之于授权服务器也是一种压力,这种情况下就可以在下发访问令牌的同时下发一个刷新令牌,刷新令牌的有效期明显长于访问令牌,这样在访问令牌失效时,可以利用刷新令牌去授权服务器换取新的访问令牌,不过协议对于刷新令牌没有强制规定,是否需要该令牌是客户端可以自行选择。
回调地址(redirect uri)
OAuth2.0 是一类基于回调的授权协议,在授权码模式中,整个授权需要分为两步进行,第一步下发授权码,第二步根据第一步拿到的授权码请求授权服务器下发访问令牌。
OAuth 在第一步下发授权码时,是将授权码以参数的形式添加到回调地址后面,并以 302 跳转的形式进行下发,这样简化了客户端的操作,不需要再主动去触发一次请求,即可进入下一步流程,但若在客户端请求过程中修改了对应的回调地址,并指向其他的服务器,使客户端的授权码被盗用,或使用户被引导至恶意站点而被攻击,此外,还会使授权服务器变成“请求发送器”,以授权服务器为代理请求目标地址,消耗授权服务器性能的同时,对目标地址服务器产生 DDOS 攻击。
为了避免上述安全隐患,OAuth 协议强制要求客户端在注册时填写自己的回调地址,这个回调地址的目的是为了让回调请求能够到达客户端自己的服务器,从而可以走获取访问令牌的流程。客户端可以同时配置多个回调地址,并在请求授权时携带一个地址,服务器会验证客户端传递上来的回调地址是否与之前注册的回调地址相同,或者前者是后者集合的一个元素,只有在满足这一条件下才允许下发授权码,同时协议还要求两步请求客户端携带的回调地址必须一致,通过这些措施来保证回调过程能够正常达到客户端自己的服务器,并继续后面拿授权码换取访问令牌的流程。
权限范围(scope)
访问令牌自带过期时间,可以在时间维度上对授权进行控制,而在范围维度上,OAuth 引入了一个 scope 的概念。在请求授权时指明该参数,表明获取该应用所允许的哪些权限,这些权限在用户确认授权时,必须毫无保留的展示给用户,以让用户知道该APP需要获取用户的哪些数据或服务,这些权限在用户确认授权后,第三方客户端即可获取相应的资源
回调地址(redirect uri)
OAuth2.0 是一类基于回调的授权协议,在授权码模式中,整个授权需要分为两步进行,第一步下发授权码,第二步根据第一步拿到的授权码请求授权服务器下发访问令牌。
OAuth 在第一步下发授权码时,是将授权码以参数的形式添加到回调地址后面,并以 302 跳转的形式进行下发,这样简化了客户端的操作,不需要再主动去触发一次请求,即可进入下一步流程,但若在客户端请求过程中修改了对应的回调地址,并指向其他的服务器,使客户端的授权码被盗用,或使用户被引导至恶意站点而被攻击,此外,还会使授权服务器变成“请求发送器”,以授权服务器为代理请求目标地址,消耗授权服务器性能的同时,对目标地址服务器产生 DDOS 攻击。
为了避免上述安全隐患,OAuth 协议强制要求客户端在注册时填写自己的回调地址,这个回调地址的目的是为了让回调请求能够到达客户端自己的服务器,从而可以走获取访问令牌的流程。客户端可以同时配置多个回调地址,并在请求授权时携带一个地址,服务器会验证客户端传递上来的回调地址是否与之前注册的回调地址相同,或者前者是后者集合的一个元素,只有在满足这一条件下才允许下发授权码,同时协议还要求两步请求客户端携带的回调地址必须一致,通过这些措施来保证回调过程能够正常达到客户端自己的服务器,并继续后面拿授权码换取访问令牌的流程。
授权流程
作为第三方登录服务提供方,我们的核心矛盾点就是既要让用户在对接我们服务的APP上登录,同时还不能让该APP拿到用户的登录凭证(用户名和密码)。解决这一矛盾的利器就是 token(中文译为令牌),而 OAuth 协议的最终目的就是给第三方应用下发 token,它记录了用户的登录或授权状态,通过将 token 传递给第三方应用,既能让第三方应用登录并拿到用户许可数据,也可以将用户的凭证牢牢拽在自己的手里(token是加密存储的,所以不担心因token下发而泄露用户凭证数据)。
说到用户登录状态的记录,我们可能最先想到的是 session 机制,想想你在做的第一个用户登录应用的时候,是不是拿服务器的 session 去记录用户是否登录。这一做法简单,但是也存在问题,session 说到底也还是缓存,当用户量较大的时候,需要相当大容量的缓存才能够容纳所有用户的登录状态,并且我们的 WEB 服务器往往有多台,通过负载均衡机制来提升服务的可用性,这样的场景下,我们不能简单的通过本地 session 来记录用户的登录状态,必须有专门的 session 服务器,还需要考虑宕机造成的 session 丢失等问题,总之用户量大了,许多最初不是问题的问题逐渐暴露出来,有的甚至可能是极其棘手的。实际上对于用户登录状态的保存,我们可以走 token 机制,让客户端自己去保存用户的登录状态,将服务器从繁重的压力中解脱出来,利用 SSO(单点登录:Single Sign On)来实现公司内各业务之间“一次登录,到处可用”。
回到 OAuth 协议,上面的论述可能侧重了第三方登录,实际上登录只是一个授权的过程,对于一个应用,其最终目的还是希望能够拿到用户存储在资源服务器上的用户数据,所以登录授权还只是第一步,后续 APP 还需要携带 token 去资源服务器请求用户数据,这个时候是一个鉴权的过程,OAuth 协议的主要目的在于授权,至于鉴权,实现上主要还是对 APP 传递过来的 token 进行解析和验证,这一块相对要简单一些,所以下面主要讲解 OAuth 授权的流程。
OAuth授权流程
OAuth协议已定义了 4 种授权模式,其中最具代表性的就是授权码模式,授权流程图如下:
假设整个流程开始之前,用户已经登录,那么整个授权流程如下:
- 客户端请求授权服务器
- 授权授权服务的授权端点重定向用户至授权交互页面,并询问用户是否授权
- 如果用户许可,则授权端点验证客户端的身份,并发放授权码给客户端
- 客户端拿到授权码之后,携带授权码请求授权服务器的令牌端点下发访问令牌
- 令牌端点验证客户端的身份和授权码,通过则下发访问令牌和刷新令牌(可选)
- 客户端拿到访问令牌后,携带访问令牌请求资源服务器上的受保护资源
- 资源服务器验证客户端身份和访问令牌,通过则响应受保护资源访问请求
整个流程中,客户端都无法接触到用户的登录凭证信息,客户端通过访问令牌请求受保护资源,用户可以通过对授权操作的控制来间接控制客户端对于受保护资源的访问权限范围和时效。
授权模式
OAuth2.0 相对于 1.0 版本在授权模式上做了更多的细化,已定义的授权模式分为四种:
- 授权码模式(Authorization Code Grant)
- 隐式授权模式(Implicit Grant)
- 资源所有者密码凭证模式(Resource Owner Password Credentials Grant)
- 客户端凭证模式(Client Credentials Grant)
授权码授权模式流程严谨,安全性更高。
授权码模式在整个授权流程上与 1.0 版本最贴近,但是整个流程还是要简化了许多,也是 OAuth2.0 中最标准,应用最广泛的授权模式。这类授权模式非常适合于具备服务端的应用,当然现在大多数 APP 都有自己的服务端,所以大部分 APP 的 OAuth 授权都可以采取授权码模式,下图为授权码各个角色之间的交互时序(这里让用户直接参与其中,省略了用户代理)。
整个授权流程说明如下(具体参数释义见下文):
- 客户端携带 client_id, scope, redirect_uri, state 等信息引导用户请求授权服务器的授权端点下发 code
- 授权服务器验证客户端身份,验证通过则询问用户是否同意授权(此时会跳转到用户能够直观看到的授权页面,等待用户点击确认授权)
- 假设用户同意授权,此时授权服务器会将 code 和 state(如果客户端传递了该参数)拼接在 redirect_uri 后面,以302形式下发 code
- 客户端携带 code, redirect_uri, 以及 client_secret 请求授权服务器的令牌端点下发 access_token (这一步实际上中间经过了客户端的服务器,除了 code,其它参数都是在应用服务器端添加,下文会细讲)
- 授权服务器验证客户端身份,同时验证 code,以及 redirect_uri 是否与请求 code 时相同,验证通过后下发 access_token,并选择性下发 refresh_token
获取授权码
授权码是授权流程的一个中间临时凭证,是对用户确认授权这一操作的一个暂时性的证书,其生命周期一般较短,协议建议最大不要超过10分钟,在这一有效时间周期内,客户端可以凭借该暂时性证书去授权服务器换取访问令牌。
请求参数说明:
名称 | 是否必须 | 描述信息 |
---|---|---|
response_type | 必须 | 对于授权码模式 response_type=code |
client_id | 必须 | 客户端ID,用于标识一个客户端,等同于appId,在注册应用时生成 |
redirect_uri | 可选 | 授权回调地址,具体参见 2.2.3 小节 |
scope | 可选 | 权限范围,用于对客户端的权限进行控制,如果客户端没有传递该参数,那么服务器则以该应用的所有权限代替 |
state | 推荐 | 用于维持请求和回调过程中的状态,防止CSRF攻击,服务器不对该参数做任何处理,如果客户端携带了该参数,则服务器在响应时原封不动的返回 |
请求参数示例:
GET /authorize?response_type=code&client_id=s6BhdRkqt3&state=xyz&redirect_uri=https://client.example.com/cb HTTP/1.1
Host: server.example.com
客户端携带上述参数请求授权服务器的令牌端点,授权服务器会验证客户端的身份以及相关参数,并在确认用户登录的前提下弹出确认授权页询问用户是否授权,如果用户同意授权,则会将授权码(code)和state信息(如果客户端传递了该参数)添加到回调地址后面,以 302 的形式下发。
成功响应参数说明:
名称 | 是否必须 | 描述信息 |
---|---|---|
code | 必须 | 授权码,授权码代表用户确认授权的暂时性凭证,只能使用一次,推荐最大生命周期不超过10分钟 |
state | 可选 | 如果客户端传递了该参数,则必须原封不动返回 |
成功响应示例:
HTTP/1.1 302 Found
Location: https://client.example.com/cb?code=SplxlOBeZQQYbYS6WxSbIA&state=xyz
如果请求参数错误,或者服务器端响应错误,那么需要将错误信息添加在回调地址后面,以 302 形式下发(回调地址错误,或客户端标识无效除外)。
错误响应参数说明:
名称 | 是否必须 | 描述信息 |
---|---|---|
error | 必须 | 错误代码 |
error_description | 可选 | 具备可读性的错误描述信息 |
error_uri | 可选 | 错误描述信息页面地址 |
state | 可选 | 如果客户端传递了该参数,则必须原封不动返回 |
错误响应示例:
HTTP/1.1 302 Found
Location: https://client.example.com/cb?error=access_denied&state=xyz
下发访问令牌
授权服务器的授权端点在以 302 形式下发 code 之后,用户 User-Agent,比如浏览器,将携带对应的 code 回调请求用户指定的 redirect_url,这个地址应该能够保证请求打到应用服务器的对应接口,该接口可以由此拿到 code,并附加相应参数请求授权服务器的令牌端点,授权端点验证 code 和相关参数,验证通过则下发 access_token。
请求参数说明:
名称 | 是否必须 | 描述信息 |
---|---|---|
grant_type | 必须 | 对于授权码模式 grant_type=authorization_code |
code | 必须 | 上一步骤获取的授权码 |
redirect_uri | 必须 | 授权回调地址,具体参见 2.2.3 小节,如果上一步有设置,则必须相同 |
client_id | 必须 | 客户端ID,用于标识一个客户端,等同于appId,在注册应用时生成 |
- 如果在注册应用时有下发客户端凭证信息(
client_secret
),那么客户端必须携带该参数以让授权服务器验证客户端的有效性。 - 针对客户端凭证需要多说的一点就是,不能将其传递到客户端,客户端无法保证凭证的安全,凭证应该始终留在应用的服务器端,当下发code回调请求到应用服务器时,在服务器端携带上凭证再次请求下发令牌。
请求参数示例:
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://client.example.com/cb
授权服务器需要验证客户端的有效性,以及是否与之前请求授权码的客户端是同一个(请求授权时的信息可以记录在 code,或以 code 为 key 建立缓存),授权服务器还要保证code 处于生命周期内(推荐10分钟内有效),且只能被使用一次。授权服务器验证通过之后,生成 access_token,并选择性下发 refresh_token,OAuth2.0 协议明确了 token 的下发策略,对于生成策略没有做太多说明。
成功响应参数说明:
名称 | 是否必须 | 描述信息 |
---|---|---|
access_token | 必须 | 访问令牌 |
token_type | 必须 | 访问令牌类型,比如 bearer,mac 等等 |
expires_in | 推荐 | 访问令牌的生命周期,以秒为单位,表示令牌下发后多久时间过期,如果没有指定该项,则使用默认值 |
refresh_token | 推荐 | 刷新令牌,选择性下发,参见 2.2.2 |
scope | 可选 | 权限范围,如果最终下发的访问令牌对应的权限范围与实际应用指定的不一致,则必须在下发访问令牌时用该参数指定说明 |
最后访问令牌以 JSON 格式响应,并要求指定响应首部 Cache-Control: no-store 和 Pragma: no-cache。
成功响应示例:
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"
}
错误响应参数说明:
名称 | 是否必须 | 描述信息 |
---|---|---|
error | 必须 | 错误代码 |
error_description | 可选 | 具备可读性的错误描述信息 |
error_uri | 可选 | 错误描述信息页面地址 |
错误响应示例:
HTTP/1.1 400 Bad Request
Content-Type: application/json;charset=UTF-8
Cache-Control: no-store
Pragma: no-cache
{
"error":"invalid_request"
}
授权码授权流程分为两步走,将用户授权与下发 token 分开,这给授权带来了更多的灵活性,正常授权过程中必须经过用户登录这一步骤,在用户已登录的前提下,可以直接询问用户是否同意授权,但是在一些场景下,比如内部走 SSO 登录的应用集成了基于 OAuth 登录的第三方应用,这个时候在 OAuth 授权登录第三方应用时用户体验较好的流程是不需要用户再一次输入用户名和密码登录的,这就需要将外围APP 的登录态传递给该应用,但是这样是存在安全问题的,用户的登录态必须把握在走SSO 登录流程的应用中,这样的场景下授权码授权模式的两步走流程就可以满足在不交出用户登录态的情况下,无需再次登录即可授权。
内部应用可以拿着第三方应用的client_id 等信息代替第三方应用去请求获取 code,因为自己持有用户的登录态,所以过程中无需用户再次输入用户名和密码,拿到 code 之后将其交给第三方应用,第三方应用利用 code 和自己的 client_secret 信息去请求授权服务器下发 token,整个流程内部应用不需要交出自己持有的用户登录态,第三方应用也无需交出自己的 client_secret 信息,最终却能够实现在保护用户登录凭证的前提下无需再次登录即可完成整个授权流程。