OAuth2.0 从入门到精通,不谈论概念,直击实战演练,教你如何实现一个基于OAuth2.0的授权服务,并且了解最新的 OIDC 与 OAuth2.0 的关系。
一、前言
1. 什么是 OAuth ?
OAuth 不是一个具体的框架或服务,而是一个验证授权(Authorization)的开放标准。常见的 SpringSecurity 框架即基于此标准实现。
OAuth 通常是为了解决开放平台资源服务的认证授权而生,但随着时代发展,越来越多系统的用户登录也是用了该标准(基本是因为所用的安全框架基于这个标准)。
OAuth 主要有 OAuth 1.0a 和 OAuth 2.0 两个版本,并且二者完全不同,且不兼容,OAuth2.0 是目前广泛使用的版本,我们多数谈论 OAuth 时,为 OAuth2.0。
2. 为什么要有 OAuth ?
在之前,认证授权多为用户名和密码进行验证,而在三方登录兴起之后,例如某论坛要支持 QQ 登录,支持微信登录,我们不可能在这个论坛上输入自己的 qq 账号密码,因此 OAuth 的出现就是为了解决访问资源的安全性以及灵活性。OAuth 使得第三方应用对资源的访问更加安全。
3. 怎么实现 OAuth2.0 标准的授权服务 ?
OAuth2.0 的授权模式大概分为 授权码模式
、简化模式
、密码模式
、客户端模式
和最新扩展 OIDC模式
(open id connect)等等,每种模式的请求逻辑略有不同。
一个 OAuth2.0 服务大致会有以下端点(可理解为页面或接口)
- a. 授权端点(Authorization Endpoint)
- 用途:用于用户认证和授权请求的起点。
- 示例:类似微信扫码登录后出现的页面,或某论坛支持 github 登录,点击跳转到 github 的授权用户信息给某论坛的确认页面。
- 端点:
/authorize
- b. 令牌端点(Token Endpoint)
- 用途:用于获取访问令牌和刷新令牌。
- 示例:例如授权码模式下,通过
code
发起请求获取access_token
。 - 端点:
/token
- c. 用户信息端点(Userinfo Endpoint):
- 用途:用于获取关联于访问令牌的用户信息。
- 示例:通过
access_token
获取用户 JSON 信息。 - 端点:
/userinfo
一个 OAuth2.0 服务必定会有 client_id
和 client_secret
,服务端会通过 client_id
去识别谁请求的登录。若我们的系统只需要自己用户去登录,甚至后端写固定值也无所谓,若我们的系统需要支持第三方应用来授权登录,则需要将每个第三方应用对应的 client_id
等信息生成并保存,需要后续去做识别认证。
二、授权码模式
最安全但也是最复杂的模式,多适用于第三方登录的场景
下文模拟场景:假设我们作为 Github 服务端,而有一个第三方论坛百度贴吧要来对接我们,实现 Github 快捷登录
1. 分配 Client 信息
我们需要在 Github 服务端给百度贴吧生成专属信息包含 client_id
和 client_secret
,并且一般百度贴吧也要告诉我们授权成功后的重定向 url redirect_uri
,避免当它 client 信息泄露被滥用登录。
而我们会将 client_id
和 client_secret
以及上文说到的a,b,c
三个端点都告诉百度贴吧。
2. 跳转登录
百度贴吧需要在其登录页面上,展示一个 Github 登录按钮,点击后应该跳转到我们 Github 的授权端点页面,并且带上 client 信息,Github 才能知道是谁请求的登录,例如点击跳转的页面应该类似如下地址
1 | https://github.com/login/oauth/authorize |
其中也就是访问 Github 的授权页面 https://github.com/login/oauth/authorize
,并通过 url 传递几个参数
- client_id:客户端 id,识别是哪个平台请求的授权,我们 Github 应该对此值判断是否有效
- response_type:相应类型,此时固定
code
,表示是授权码模式 - scope:范围,该值决定了获取信息时返回的字段,或该令牌的权限,多个范围用空格隔开,例如
profile repo:read repo:write
等等,每个平台都可以不同,例如百度贴吧只需要用户邮箱,而有些平台就需要 github 的仓库读写权限等等,至于填写 profile 还是 repo 等等,一般依赖于不同平台的识别需求,并没有固定要求 - redirect_uri:授权成功后重定向 url,我们 Github 也应该对此校验,否则毕竟 client_id 是明文,可能被滥用
3. 授权页面编写
我们 Github 则需要编写第一个 /login/oauth/authorize
登录页面,该页面逻辑应该先判断用户是否登录,没有则有登录逻辑,若登录后则应该校验 client_id
和 scope
等值,校验无误后在界面显示一个授权样式,提示用户是否将信息授权给百度贴吧,当用户点击同意,我们应该跳转回 redirect_uri
并传递 code
参数,该参数后端自由生成,code 的有效期应尽可能短(如 10 分钟)保证安全,后续后端能通过 code 获取到授权的用户信息即可,例如跳转回 https://tieba.baidu.com/login?code=xxx
。
4. 获取令牌
百度贴吧页面读取到 code 值后,得知用户已经授权登录了,再请求 Github 令牌端点(此请求应该在百度贴吧自己的后端执行),并传递 code
和 client_secret
等值去获取令牌,例如 https://github.com/login/oauth/access_token?code=xxx&grant_type=authorization_code&client_id=xx&client_secret=xx&redirect_uri=xx
其中也就是通过 POST
请求 Github 的令牌接口 https://github.com/login/oauth/access_token
,并通过 url 传递几个参数(或通过 JSON body 传递,取决于令牌接口如何设计)
- client_id:客户端 id,识别是哪个平台请求的授权,我们 Github 应该对此值判断是否有效
- grant_type:授权类型,此时固定 authorization_code,表示请求需要提供
code
和client_id
等值 - client_secret:客户端密钥,Github 后端应该校验此值与
client_id
是否配套,但部分系统也会允许“百度贴吧”可不传递此值,主要看是否注重安全性 - redirect_uri:授权成功后重定向 url,我们 Github 应该对此值校验,与
/authorize
端点传递的是否一致,避免 code 被劫持
当校验通过后,我们 Github 应该返回如下 JSON 数据
1 | { |
5. 获取用户信息
百度贴吧获取到令牌后,再通过 token_type
和 access_token
去请求用户信息端点接口,获取授权用户的信息,例如
发送 GET
请求,Github 提供的用户信息接口 https://github.com/user
,并设置请求头 Authorization: Bearer ${access_token}
,即可获取到用户信息,有哪些字段,以及字段名称主要根据授权服务和 scope 而定,一般会如下值及字段
1 | { |
6.总结
至此已经完成授权码模式全流程,期间我方平台需要做的便是
- 生成 client 信息
- 提供授权页面并编写相关逻辑(此处还需要包含一个内部使用接口,去生成 code 之类)
- 提供获取令牌接口,并作相关校验
- 提供获取用户信息接口
三、简化模式
基于授权码模式的简化版,在 3.授权页面
此步骤,原本 Github 应该重定向并传递 code,而百度贴吧再通过 code 请求令牌接口获取 access_token
,而简化模式则是百度贴吧跳转 Github 授权页面时,修改 response_type=code
为 response_type=token
,表示简化模式,此时 Github 授权成功后重定向回百度贴吧时就不传递 code,而是直接传递 access_token
例如 https://tieba.baidu.com/login?access_token=xxx
,省去了 code
通过令牌接口换取 access_token
的步骤。
后续获取用户信息接口就和授权码模式一样了,带上请求头 Authorization: Bearer ${access_token}
即可。
四、密码模式
密码模式一般用于自己平台登录,毕竟如果用于第三方登录,用户需要把自己 github 的密码告诉百度贴吧明显十分危险
密码接口只需要调用令牌端点即可,例如
通过 POST
请求 https://github.com/login/oauth/access_token
,并传入参数
1 | { |
返回
1 | { |
后续获取用户信息接口就和授权码模式一样了,带上请求头 Authorization: Bearer ${access_token}
即可。
五、客户端模式
客户端模式一般用于公共接口的授权,表示我 Github 知道你是哪个平台请求的,但不需要知道你是哪个用户,例如某些字典接口,不能对外暴露,但是又必须让例如百度贴吧它先在我平台注册才能访问这个接口
客户端模式也只需要请求令牌端点即可
通过 POST
请求 https://github.com/login/oauth/access_token
,并传入参数
1 | { |
返回
1 | { |
后续调用资源服务提供的公共接口,和获取用户信息接口一样,带上请求头 Authorization: Bearer ${access_token}
即可。
六、刷新令牌
刷新令牌只需要调用令牌端点即可,例如
通过 POST
请求 https://github.com/login/oauth/access_token
,并传入参数
1 | { |
返回
1 | { |
七、OIDC 模式
OIDC 全称为 OpenID Connect,是对于 OAuth2.0 的一个扩展,毕竟在标准 OAuth2.0 模式下,要获取到用户信息需要经过多个端点请求,而某些平台的第三方登录,只需要获取这个用户在第三方的唯一标识,以及昵称头像之类即可,所以便出现了 OIDC 模式。
1. 最常见的 OIDC 用法
在授权码模式中的第一步骤,跳转往授权端点时,传递参数scope增加openid,例如 scope=email openid
,多个用空格隔开。
在令牌端点时,Github服务端检测到scope含有openid,则多返回一个 id_token
,例如
1 | { |
此时百度贴吧直接解析 id_token 中的 jwt 值即可获取到用户的一些基本信息,信息字段与用户信息端点基本应保持一致,也就是
1 | { |
通过这种模式,既完全兼容原有的 oauth2.0
标准,又能扩展出 id_token
值,百度贴吧不需要再通过 access_token
去请求用户信息端点,直接解析jwt即可知道用户的信息。
2. OIDC 简化模式
有人说这种模式仍然还是需要请求两次,那么有没有更简单一点的方法能直接获取到用户信息呢?当然有,回到第一步骤,百度贴吧跳往 Github 授权页,当 response_type=code
改为 response_type=id_token
即为 OIDC 模式(需要注意,在OAuth2.0规范中,response_type并不能传递id_token值,这是OIDC引申出的)。
1 | https://github.com/login/oauth/authorize |
当 Github 授权成功后,重定向回百度贴吧时,附带的值就是 jwt 格式的 id_token,例如 https://tieba.baidu.com/login?id_token=xxx
,这种模式不需要再请求令牌端点,也不需要请求用户信息端点,大大简化了流程。
这种模式十分适合授权登录场景,不过若百度贴吧后续还需要代表用户请求 Github 接口,例如查询用户所有仓库等等,这种简化的 OIDC 模式明显无法实现,毕竟没有拿到 access_token
,因此又延申出了混合模式。
八、混合模式
1. OIDC 模式 + 简化模式
混合模式也很简单,实际就是百度贴吧跳往 Github 授权页,当 response_type=id_token
改为 response_type=token id_token
即为 OIDC 模式+简化模式结合,表示 Github 授权成功后,重定向回百度贴吧时,应该带上 OIDC 模式和简化模式应该传递的值,例如
1 | https://tieba.baidu.com/login?id_token=xxx&access_token=xxx |
2. OIDC 模式 + 授权码模式
授权端点传递 response_type=id_token code
就表示 OIDC 模式+授权码模式结合,重定向时应附带
1 | https://tieba.baidu.com/login?id_token=xxx&code=xxx |
3. 其他任意组合模式
当然只要 response_type
可以组合的都行,但其实没有实际意义了。
九、额外安全措施防护
百度贴吧通过额外传递两个参数 state
和 nonce
进行额外安全措施防护,这两个值不是必须传递,且也没要求一定得同时使用,例如
1 | https://github.com/login/oauth/authorize |
1. state
state
应该是一个随机不重复字符串,Github 授权成功后,会原值返回给百度贴吧,百度贴吧应对此值进行逻辑判断,防止恶意第三方攻击者通过操纵或篡改授权请求和响应来进行 CSRF 攻击,提高安全性
1 | https://tieba.baidu.com/login?code=xxx&state=7t3ze8muocp |
2. nonce
nonce
应该是一个随机不重复字符串,在 OIDC 模式下,Github 在进行授权时,应该判断 nonce 值是否已经被授权过,可以防止 ID 令牌(id_token)的重放攻击。在授权成功后,Github 应该将 nonce
值存放于 id_token
的 jwt 中,而非在重定向链接中显式传递,例如
1 | https://tieba.baidu.com/login?id_token=xxx |
而百度贴吧解析 id_token
中的信息,以及 nonce
值后,应该判断 nonce
值与自己发送的是否一致,防止 id_token
被串改。
1 | { |
而在授权码模式中,nonce
值应该在令牌端点时返回,例如
1 | { |
十、总结
总结一下,OAuth2.0 的核心基本围绕以下这三个端点
1. 授权端点(是个页面)
传参
1 | https://example.com/login/oauth/authorize |
成功后重定向传参
1 | https://example.com/callback |
其中各种模式的核心便是 response_type
传递的值,且多个值时用空格隔开,其决定了在重定向回回调地址时会传递什么值,例如
- 授权码模式:code->code
- 简化模式:token->access_token
- OIDC简化模式:id_token->id_token
2. 令牌端点(是个接口)
通过 POST
请求接口 /oauth/token
传参
1 | { |
返回
1 | { |
其中核心便是 grant_type
传入的值,根据不同值,服务端需要取不同参数做不同的逻辑处理校验,而返回时除了必填的几个参数,其他则基本会根据scope返回不同值。
3.用户信息端点(是个接口)
通过 GET
请求接口 /userinfo
,附带请求头 Authorization: Bearer ${access_token}
,且无需传参
回参
1 | { |
十一、最后
最后再说几句,OAuth2.0 并非指哪个安全框架,而是一个授权标准,OIDC 也不是一个新的安全框架,而是基于 OAuth2.0 的扩展而已。在实际项目中我们应该去理解 OAuth2.0 的各种模式认证流程,根据我们的业务场景去选择使用哪种授权模式,这样才能提升对于系统安全方面的认知。而在普通单体项目或简单的外包项目中,其实简单的 token + 过滤器 就已经能对系统的安全提供保护了,不一定要强行使用各种大而全但相对麻烦的安全框架。