首先,认证和授权是两个不同的概念,为了让我们的 API 更加安全和具有清晰的设计,理解认证和授权的不同就非常有必要了。

  • 认证是 authentication,指的是当前用户的身份,解决 “我是谁?”的问题,当用户登陆过后系统便能追踪到他的身份并做出符合相应业务逻辑的操作。
  • 授权是 authorization,指的是什么样的身份被允许访问某些资源,解决“我能做什么?”的问题,在获取到用户身份后继续检查用户的权限。
  • 凭证(credentials)是实现认证和授权的基础,用来标记访问者的身份或权利,在现实生活中每个人都需要一张身份证才能访问自己的银行账户、结婚和办理养老保险等,这就是认证的凭证。在互联网世界中,服务器为每一个访问者颁发会话 ID 存放到 cookie,这就是一种凭证技术。数字凭证还表现在方方面面,SSH 登录的密匙、JWT 令牌、一次性密码等。

单一的系统授权往往是伴随认证完成的,但是在开放 API 的多系统架构下,授权需要由不同的系统来完成,例如 OAuth2.0。

在流行的技术和框架中,这些概念都无法孤立的被实现,因此在现实中使用这些技术时,大家往往对 OAuth2.0 是认证还是授权这种概念争论不休。下面我们会介绍在 API 开发中常常使用的几种认证和授权技术:OAuth2.0,OpenId Connect 和 JWT。

OAuth2.0、OpenId Connect(OIDC)和 JWT

OAuth2.0

什么是 OAuth2.0

在第三方登录已经如此普遍的今天,相信大家一定都见过下面的这种界面:

525

第三方登录让我们可以在一个 app 上无需注册新用户,就能使用我们的微信、qq 等社交账号进行登录,app 可以获取到我们社交账号上的头像、邮箱等信息。

而这种现在看来已经非常普遍的操作,其背后就是 OAuth2.0 协议在支撑。

在详细讲解 OAuth2.0 之前,需要了解几个专用名词。

  • Client:第三方应用程序,即客户端应用程序。
  • HTTP service:HTTP 服务提供商,即 OAuth 服务提供商。
  • Resource Owner:资源所有者,即终端用户。
  • User Agent:用户代理,即浏览器。
  • Authorization :认证服务器,即服务提供商专门用来处理认证的服务器。
  • Resource :资源服务器,即服务提供商存放用户生成的资源的服务器。它与认证服务器,可以是同一台服务器,也可以是不同的服务器。
  • Token:包含用户身份或权限信息的令牌,通常为一串随机生成的字符,并具有时效性

OAuth2.0 是一个关于授权的开放网络标准 rfc6749,允许用户授权第三方应用访问服务授权者提供的信息,而不需要将用户名和密码提供给第三方应用或分享他们数据的所有内容。

要理解 OAuth2.0,让我们先从一个现实的场景开始:

如果要开发一个能检测代码质量的工具,面向的用户都是 GitHub 的使用者,那么如何才能让用户在不暴露 GitHub 的账号和密码的情况下,也能获得用户存储在 GitHub 的代码库的内容呢?

简单来说,OAuth2.0 就是让”Client”安全可控地获取”用户”的授权,与”Resource “进行互动。

回到我们的场景中,我们要开发的代码检测工具就是 Client,我们的用户就是 Resource Owner,GitHub 的登录系统就是 Authorization ,GitHub 的 repo 就是 Resource 。在 OAuth2.0 框架下,用户在访问代码质量检查工具时,会先通过 GitHub 的 Authorization 进行登录,GitHub 的 Authorization 会返回一个包含用户标识、且有时效性的 token,通过这个 token,代码质量检查工具可以访问 GitHub 的 Resource 来获取用户代码库的内容。

495

OAuth2.0 的核心

从我们的例子种不难发现,OAuth2.0 的关键之处,在于 Client 如何和 Authorization 进行交互,获取 token。

OAuth2.0 协议为我们提供了以下 endpoint:

  • authorization endpoint:用于申请授权码的 endpoint
  • token endpoint:用于申请 token 的 endpoint
  • introspection endpoint:用于验证解析 token 的 endpoint

我们的 client 需要向 OAuth2.0 的提供商去申请一个 client id 和 client secret,用于帮助 OAuth2.0 的提供商来验证 client 的合法性。(client id 和 client secret 既可以在 url param 中验证,也可以携带于 authorization basic token 验证,这取决于你使用的 Authorization 服务商)

OAuth2.0 包含 6 种授权类型(Grant Type),用于 client 和 Authorization 进行交互:

授权码模式(Authorization Grant Type)

授权码模式是我们最常见的一种方式,传统的授权码模式通常使用在 client 为前后端一体的应用中。授权码模式与其他授权类型相比具有一些优势。

  • 当用户授权应用程序时,会带着 URL 中的授权码返回应用程序。
  • 应用程序用授权码来交换 access token。
  • 当应用程序发出 token 请求时,该请求将使用 client secret 进行身份验证,从而降低攻击者拦截授权码并自行使用它的风险。这也意味着 token 永远不会被用户看到,因此这是将 token 传递回应用程序的最安全方式,从而降低 token 泄露给其他人的风险。(对于 token endpoint 的 post 请求参数有时也会以 form-data 的形式出现,这取决于你使用的 Authorization 服务商)

495

获取授权码

授权码是授权流程的一个中间临时凭证,是对用户确认授权这一操作的一个暂时性的证书,其生命周期一般较短,协议建议最大不要超过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.1302 Found
Location: https://client.example.com/cb?code=SplxlOBeZQQYbYS6WxSbIA&state=xyz

如果请求参数错误,或者服务器端响应错误,那么需要将错误信息添加在回调地址后面,以 302 形式下发(回调地址错误,或客户端标识无效除外)。

错误响应参数说明:

名称是否必须描述信息
error必须错误代码
error_description可选具备可读性的错误描述信息
error_uri可选错误描述信息页面地址
state可选如果客户端传递了该参数,则必须原封不动返回

错误响应示例:

HTTP/1.1302 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.1400 Bad Request
Content-Type: application/json;charset=UTF-8
Cache-Control: no-store
Pragma: no-cache
 
{
    "error": "invalid_request"
}
令牌的刷新

为了防止客户端使用一个令牌无限次数使用,令牌一般会有过期时间限制,当快要到期时,需要重新获取令牌,如果再重新走授权码的授权流程,对用户体验非常不好,于是OAuth2.0 允许用户自动更新令牌。

具体方法是,B 网站颁发令牌的时候,一次性颁发两个令牌,一个用于获取数据,另一个用于获取新的令牌(refresh token 字段)。令牌到期前,用户使用 refresh token 发一个请求,去更新令牌。

https://b.com/oauth/token?grant_type=refresh_token&client_id=CLIENT_ID&client_secret=CLIENT_SECRET&refresh_token=REFRESH_TOKEN

上面 URL 中:

  • grant_type参数为refresh_token表示要求更新令牌,此处的值固定为refresh_token,必选项;
  • client_id参数和client_secret参数用于确认身份;
  • refresh_token参数就是用于更新令牌的令牌。

B 网站验证通过以后,就会颁发新的令牌。

注意: 第三方应用服务器拿到刷新令牌必须存于服务器,通过后台进行重新获取新的令牌,以保障刷新令牌的保密性。

刷新模式(Refresh Token Grant Type)

通常在请求到 access token 时,都会携带有一个 refresh token,因为 access token 是有有效时长的,所以我们需要用一个不会过期的 refresh token 来刷新 access token,在用户无需重新登录的情况下,让 client 也能保持使用正确有效的 access token。

495

隐藏模式(Implict Grant Type)

在前后端分离的架构中,client 只有一个运行在浏览器上的前端的单页应用,不包含后端。因为 JavaScript 应用的特殊性,我们无法安全的将 client secret 存储在纯前端应用里,并且因为请求全部从浏览器发起,我们无法保证传统的授权码模式的两步请求中,是否会有中间人攻击。因此需要一种不通过 client secret 和两步请求就可以获取 access token 的方式,这就是隐藏模式。在此模式中,access token 将作为 redirect url 的 fragment 返回到 client,并且出于安全性考虑,将不会返回 refresh token,因此我们不能用传统的 refresh token 模式来刷新 access token,只能通过 silent refresh 的方式刷新。

525

1.  `* silent refresh` 
2.  `* slient refresh是隐藏模式的一种特殊的刷新方式,其原理是运用html中iframe的特性,在access token过期之前,使用一个隐藏的iframe来重新用隐藏模式申请一次token,若authorization 的session不过期,便无需用户重新输入其登录信息就能获取一个新的access token。` 

PKCE 模式(Proof Key for Exchange by OAuth Public Clients)

在隐藏模式介绍中我们可以发现,该模式是有明显的缺点的,即其 silent refresh 的刷新方式,authorization 所保存的用户登录 session 不可能永远不失效,一旦失效,我们还是需要用户重新登录才能确保 client 使用正确有效的 token。为了解决这个缺点,PKCE 模式应运而生。 PKCE 模式通过改造传统的授权码模式,在请求 authorization endpoint 的同时,加入 code_challenge 和 code_challenge_method 参数,得到授权码后,在请求 token endpoint 时加入 code_verifier 参数,authorization 会验证 code_challenge 和 code_verifier 是否匹配,以此来防止两步请求中可能产生的中间人攻击。

525

密码模式(Password Grant Type)

如果你高度信任某个应用,OAuth2.0 也允许用户直接使用用户名和密码,该应用通过用户提供的密码,申请 token,这种方式称为密码模式。密码模式无需浏览器作为代理,可以直接通过 post 请求获得 token。

525

凭证模式(Client Credentials Grant Type)

当你的应用只需要代表应用本身,而不是某个用户,来获取 resource 的资源时,就可以使用凭证模式。这种模式不需要任何用户信息,返回的 token 也不携带任何用户信息。凭证模式也无需浏览器作为代理,可以直接通过 post 请求获得 token。

525

而 client 在获取到 access token 之后,需要将 access token 携带于 authorization bearer token header,再向 resource 发出相应的资源请求。client 也可以使用 introspection endpoint 来验证 token 是否有效。

优势 & 解决的问题:开放系统间授权问题

OAuth2 最初是基于开放系统间授权问题提出的,假设现在有一个第三方应用:“云冲印服务”,可以将用户存储在 Google 的照片冲印出来。用户为了使用该服务,必须让“云冲印服务”读取自己储存在 Google 上的照片。问题是只有得到用户的授权,Google 才会同意“云冲印服务”读取这些照片。那么“云冲印服务”如何获取用户的授权呢?

办法 1:密码用户名复制

办法1密码用户名复制|425

传统的办法是,资源拥有者将自己的用户名和密码告诉第三方服务,然后第三方服务再去读取用户受保护的资源,这种做法适用于公司内部应用开发时使用,在开放系统间这么做就不太合适了,因为第三方服务可能为了后续的服务,会保存用户的密码,这样很不安全。

办法 2:万能钥匙

办法2万能钥匙|392

另一种方法是客户应用和受保护的资源之间商定一个通用 developer key,用户在受保护资源方得到一个 developer key 交给第三方应用,第三方应用再通过这个 developer key 去访问用户受保护的资源。这种方式适用客户应用和受保护资源之间存在信任关系的情况,如两方是合作商,或是同个公司不同部门之间的应用。但是对于不受信的第三方应用来说这种方法也不合适。

办法 3:特殊令牌

办法3特殊令牌|329

第三种方法是使用一个特殊令牌,它仅仅能访问受保护的资源,这种做法相对前两种方法要靠谱的多,并且和 OAuth2 的做法已经比较接近了,但是如何管理令牌,颁发令牌,吊销令牌就需要一些讲究了,这些我们留到后面介绍 OAuth2 再来了解。

OpenId Connect(OIDC)

尽管在今天很多 OAuth2.0 的使用中都包含身份信息,但 OAuth2.0 实际上是一个授权(authorization)的协议,并不包含认证(authentication)的内容。

OAuth2.0 框架明确不提供有关已授权应用程序的用户的任何信息。OAuth2.0 是一个委派框架,允许第三方应用程序代表用户行事,而无需应用程序知道用户的身份。

而 OpenId 的诞生就是为了解决认证问题的:OpenId 基于 OAuth2.0,在兼容 OAuth2.0 协议的基础上,它构建了一个身份层,用于验证并为 client 展示身份信息。

540

OpenID Connect 的核心基于一个名为“ID token”的概念。authorization 将返回新的 token 类型,它对用户的身份验证信息进行编码。与仅旨在由资源服务器理解的 access token 相反,ID token 旨在被第三方应用程序理解。当 client 发出 OpenID Connect 请求时,它可以请求 ID token 以及 access token。

OpenID Connect 的 ID token 采用 JSON Web Token(JWT)的形式,JWT 是一个 JSON 有效负载,使用发行者的私钥进行签名,并且可以由应用程序进行解析和验证。

525

得益于 JWT 的自解析性,client 可以不申请 introspection endpoint 就可以解析出 ID token 所包含的身份信息。JWT 内部是一些定义的属性名称,它们为应用程序提供信息。它们用简写名称表示,以保持 JWT 的整体大小。这包括用户的唯一标识符(sub 即“subject”的缩写),发出 token 的服务器的标识符(iss 即“issuer”的缩写),请求此 token 的 client 的标识符(aud 即“audience”的缩写),以及少数属性,例如 token 的生命周期,以及用户在多长时间之前获得主要身份验证提示。

525

JWT(Json Web Token)

JWT(Json Web Token)是一个非常轻巧的规范。这个规范允许我们使用 JWT 在两个组织之间传递安全可靠的信息。

首先,我们需要理解的是,JWT 实际上是一个统称,它实际上是包含两部分 JWS(Json Web Signature)和 JWE(Json Web Encryption)。

525

JWS(Json Web Signature)

Json Web Signature 是一个有着简单的统一表达形式的字符串:

JWS 包含三部分:

  • JOSE 头(header)用于描述关于该 JWT 的最基本的信息,例如其类型以及签名所用的算法等。JSON 内容要经 Base64 编码生成字符串成为 Header。
  • JWS 负载(payload)所必须的五个字段都是由 JWT 的标准所定义的。
    • iss(issuer): 该 JWT 的签发者(通常是 authorization 的地址)
    • sub(subject): 该 JWT 所面向的用户(通常是用户名或用户 ID)
    • aud(audience): 接收该 JWT 的一方(通常是 client id)
    • exp(expires): 什么时候过期(Unix 时间戳)
    • iat(issued at): 在什么时候签发的(Unix 时间戳)

其他字段可以按需要补充。JSON 内容要经 Base64 编码生成字符串成为 PayLoad。

  • JWS 签名(signature)使用密钥 secret 进行加密,生成签名。
    • 加密时的秘钥服务私密保存

JWS 的主要目的是保证了数据在传输过程中不被修改,验证数据的完整性。但由于仅采用 Base64 对消息内容编码,因此不保证数据的不可泄露性。所以不适合用于传输敏感数据。并且加密数据只用于签名部分,所以 JWS 具有自解析性。

  • 如何验证完整性呢? 因为密钥都在服务器端,所以服务器端通过密钥来验证即可。

525

JWE(Json Web Encryption)

Json Web Encryption 是一个 JWS 的扩展,它是一串用加密算法加密过的 token,在没有密钥的情况下,它能像 JWS 一样的解析。

JWE 包含五部分:

  • JOSE 头(header):描述用于创建 JWE 加密密钥和 JWE 密文的加密操作,类似于 JWS 中的 header。
  • JWE 加密密钥:用来加密文本内容所采用的算法。
  • JWE 初始化向量:加密明文时使用的初始化向量值,有些加密方式需要额外的或者随机的数据。这个参数是可选的。
  • JWE 密文:明文加密后产生的密文值。
  • JWE 认证标签:数字认证标签。
  • JWE 规范引入了两个新元素(enc 和 zip),它们包含在 JWE 令牌的 JOSE 头中,enc 元素定义了秘文的加密算法,它应该是一个 AEAD(Authenticated Encryption with Associated Data)模式的对称算法, zip 元素定义了压缩算法,alg 元素定义了用来加密 cek(Content Encryption Key)的加密算法。

525