文章首发于:github.com/USTB-musion…
写在前面
昨天在组内分享了第三方登录与单点登录,其中着重分享了第三方登录当中的oAuth2协议,在这里记录整理一下。oAuth协议是一个授权的开放网络标准,主要是用来解决第三方登录的,即所谓第三方登录,实际上就是oAuth的授权。
什么是第三方登录
很多网站登录时,允许使用第三方网站的身份来进行登录,这称为“第三方登录”。比如知乎和慕课网等,可以使用微信,QQ,或微博来进行登录。一个网站想接入第三方登录,需要用到oAuth这个协议。
什么是oAuth
oAuth是一个关于授权的开放网络标准,用来授权第三方应用,获取用户的数据。其最终的目的是为了给第三方应用颁发一个有时效性的令牌access_token,第三方应用根据这个access_token就可以去获取用户的相关资源,如头像,昵称,email这些信息。现在大家用的基本是2.0的版本。
oAuth2.0的这个协议是从RFC 6749这篇文章当中提出来的,如果想了解更全面的信息,可以点击👆这篇文章进行了解。下面来介绍一下oAuth2的角色和流程。
协议流程
在详细介绍oAuth2协议流程之前,先来简单了解几个角色,方便后续的理解。
- Resource Owner,资源所有者,因为是请求用户的头像和昵称的一些信息,所以资源的所有者一般指用户自己。
- Client,客户端,如web网站,app等
- Resource Server,资源服务器,托管受保护资源的服务器
- Authorization Server,授权服务器,一般和资源服务器是同一家公司的应用,主要是用来处理授权,给客户端颁发令牌
- User-agent,用户代理,一般为web浏览器,在手机上就是app
了解了上面这些角色之后,来看下oAuth2.0的运行流程是怎么样的。
+--------+ +---------------+
| |--(A)- Authorization Request ->| Resource |
| | | Owner |
| |<-(B)-- Authorization Grant ---| |
| | +---------------+
| |
| | +---------------+
| |--(C)-- Authorization Grant -->| Authorization |
| Client | | Server |
| |<-(D)----- Access Token -------| |
| | +---------------+
| |
| | +---------------+
| |--(E)----- Access Token ------>| Resource |
| | | Server |
| |<-(F)--- Protected Resource ---| |
+--------+ +---------------+
(A). 用户打开客户端(Client),客户端向授权服务器(Resource Owner)发送一个授权请求
(B). 用户同意给客户端(Client)授权
(C). 客户端使用刚才的授权去向认证服务器(Authorization Server)认证
(D). 认证服务器认证通过后,会给客户端发放令牌(Access Token)
(E). 客户端拿着令牌(Access Token),去向资源服务器(Resource Server)申请获取资源
(F). 资源服务器确认令牌之后,给客户端返回受保护的资源(Protected Resource)
授权方式
在oAuth2当中,定义了四种授权方式,针对不同的业务场景:
- 授权码模式(authorization code): 流程最完整和严密的一种授权方式,服务器和客户端配合使用,主要是针对web服务器的情况采用
- 简化模式(implicit):主要用于移动应用程序或纯前端的web应用程序,主要是针对没有web服务器的情况采用
- 密码模式(resource owner password credentials):不推荐,用户需要向客户端提供自己的账号和密码,如果客户端是自家应用的话,也是可以的
- 客户端模式(client credentials):客户端以自己的名义,而不是用户的名义,向“服务提供商”进行认证,如微信公众号以此access_token来拉取所有已关注用户的信息,docker到dockerhub拉取镜像等
授权码模式
+----------+
| Resource |
| Owner |
| |
+----------+
^
|
(B)
+----|-----+ Client Identifier +---------------+
| -+----(A)-- & Redirection URI ---->| |
| User- | | Authorization |
| Agent -+----(B)-- User authenticates --->| Server |
| | | |
| -+----(C)-- Authorization Code ---<| |
+-|----|---+ +---------------+
| | ^ v
(A) (C) | |
| | | |
^ v | |
+---------+ | |
| |>---(D)-- Authorization Code ---------' |
| Client | & Redirection URI |
| | |
| |<---(E)----- Access Token -------------------'
+---------+ (w/ Optional Refresh Token)
Note: The lines illustrating steps (A), (B), and (C) are broken into two parts as they pass through the user-agent.
授权码模式如上图所示,这种流程是功能最完整,流程也是最严密的授权方式,适用于那些有后端的web应用。它的特点是通过客户端的后台服务器和服务商的认证服务器进行通讯。它的流程如下,如果我想使用github来接入第三方登录:
(A). 用户(Resource Owner)在用户代理(User-Agent,如web浏览器,app)上选择了第三方应用(如github)来进行登录,会重定向到github的授权端点:
https://github.com/login/oauth/authorize?
response_type=code&
client_id=your_code&
redirect_uri=重定向的url&
scope=read&
state=uuid
字段 | 描述 |
---|---|
response_type | 必须,在授权码模式中固定为code |
client_id | 必须,唯一标识了客户端,在github注册时获得的客户端ID |
redirect_url | 客户端在github注册的重定向url,用户同意或拒绝的时候都会跳转到这个重定向url |
scope | 可选,请求资源范围,如有多项,使用多个空格隔开 |
state | 推荐,客户端生成的随机数,资源服务器会原样返回,防止CSRF的攻击 |
(B). 页面跳转后,github会要求用户登录,然后询问是否给予客户端授权,用户点击同意。
(C). 然后github就会将授权码(Authorization Code)返回给redirect_uri(重定向uri)。
redirect_uri?code=xxxxxxx
字段 | 描述 |
---|---|
code | 必须,授权码 |
state | 防止CSRF攻击的参数 |
(D). 客户端(Client)在通过在URL中取出授权码之后,就可以在后端向github请求令牌
https://github.com/login/oauth/access_token?
client_id=your_code&
client_secret=your_secret&
grant_type=authorization_code&
code=取出的code&
redirect_uri=重定向的url
字段 | 描述 |
---|---|
client_id | 必须,客户端在github注册的唯一标识 |
client_secret | 必须,客户端在github注册时返回的密钥 |
grant_type | 必须,authorization_code/refresh_code |
code | 必须,上一步中取出的授权码 |
redirect_uri | 必须,完成授权之后的回调地址,与在github注册时的一致 |
(E). github给redirect_uri指定的地址返回AccessToken,通过JSON格式返回
{
"access_token":"xxxxxxx",
"token_type":"bearer",
"expires_in":3600,
"refresh_token":"xxxxxxx"
}
客户端就可以在后端取到access_token,在这段json中,还返回了一个refresh_token,这个refresh_token表示用于访问下一次的更新令牌,refresh_token的时效性比access_token长,当access_token过期时,可以使用refresh_token换取新的access_token。
简化模式
简化模式主要针对没有后端的纯前端应用,在这种情况下,因为没有后端,所以就不能采用授权码模式的这种流程了,必须要把access_token存在前端。
+----------+
| Resource |
| Owner |
| |
+----------+
^
|
(B)
+----|-----+ Client Identifier +---------------+
| -+----(A)-- & Redirection URI --->| |
| User- | | Authorization |
| Agent -|----(B)-- User authenticates -->| Server |
| | | |
| |<---(C)--- Redirection URI ----<| |
| | with Access Token +---------------+
| | in Fragment
| | +---------------+
| |----(D)--- Redirection URI ---->| Web-Hosted |
| | without Fragment | Client |
| | | Resource |
| (F) |<---(E)------- Script ---------<| |
| | +---------------+
+-|--------+
| |
(A) (G) Access Token
| |
^ v
+---------+
| |
| Client |
| |
+---------+
Note: The lines illustrating steps (A) and (B) are broken into two parts as they pass through the user-agent.
主要是B这个步骤,页面跳转到github网站,用户同意给予客户端授权。github就会把令牌作为URL参数,跳转回到redirect_uri的这个回调地址。
回调地址#token=xxxxxx
注意,令牌的位置是 URL 锚点(fragment),而不是查询字符串(querystring),这是因为 OAuth 2.0 允许跳转网址是 HTTP 协议,因此存在"中间人攻击"的风险,而浏览器跳转时,锚点不会发到服务器,就减少了泄漏令牌的风险。
密码模式
如果你高度信任某个应用,RFC 6749 也允许用户把用户名和密码,直接告诉该应用。该应用就使用你的密码,申请令牌,这种方式称为"密码式"(password)。
+----------+
| Resource |
| Owner |
| |
+----------+
v
| Resource Owner
(A) Password Credentials
|
v
+---------+ +---------------+
| |>--(B)---- Resource Owner ------->| |
| | Password Credentials | Authorization |
| Client | | Server |
| |<--(C)---- Access Token ---------<| |
| | (w/ Optional Refresh Token) | |
+---------+ +---------------+
Figure 5: Resource Owner Password Credentials Flow
密码模式就是用户向客户端提供自己的账号和密码,客户端使用这些信息去向我们的服务提供商去索要一个授权。
客户端模式
客户端以自己的名义,而不是用户的名义,向“服务提供商”进行认证,如微信公众号以此access_token来拉取所有已关注用户的信息,docker到dockerhub拉取镜像等。
+---------+ +---------------+
| | | |
| |>--(A)- Client Authentication --->| Authorization |
| Client | | Server |
| |<--(B)---- Access Token ---------<| |
| | | |
+---------+ +---------------+
Figure 6: Client Credentials Flow
客户端模式,顾名思义就是指客户端以自己的名义而不是用户的名义去向服务的提供商去做一个认证,严格来说,这种模式并不是 oAuth 框架要解决的问题,在这种客户端模式下呢,它是直接通过客户端的密钥和id去获取一个access_token的,不需要用户去参与。
单点登录
那关于 oAuth2 的理解呢,大概介绍这么多的内容。oAuth 协议主要是用来解决第三方登录的,但聊到不同场景下的登录方案时,除了第三方登录之外,还有一个概念,就是单点登录。
单点登录就是在多个系统中,用户只需登录一次,各个系统就可以感知该用户已经登录。比如说你登录了天猫,淘宝也会自动登录。简单地理解,单点登录就是这样,它通过将两个或多个产品中的用户登录逻辑抽离出来,通过只输入一次用户名和密码,就可以达到同时登录多个产品的效果。
单点登录实现方案
第一种是同一父域下的单点登录,比如说hr.oa.com,km.oa.com,fuli.oa.com,那这种情况就可以通过将domain属性设置为二级域名oa.com来共享cookie,然后服务端通过共享session就可以实现单点登录。除了共享session之外当然也可以用JWT这种方式进行实现。
那第二种就是针对不同域下的单点登录,比如说淘宝和天猫,它的二级域名是不相同的。这种情况,就要解决cookie不共享的问题。现在主流的方案就是使用cas来实现。
总结
来简单总结一下,针对不同业务场景下登录的主流解决方案,第一种是针对同一公司,同一父域下的单点登录解决方案,这种情况因为cookie是同父域下的,设置cookie的domain属性可以实现cookie共享。然后服务端session共享就可以实现单点登录。但还有一种这种方式解决方案是JWT。JWT就是json web token。实际上就是一个字符串,由头部,载荷和签名三部分组成。
第二种是针对同一公司,但是不同域下的单点登录解决方案,比如说淘宝和天猫的单点登录,那这种方式的主流解决方案是CAS。
那第三种就是不同公司,不同域下的就使用第三方登录功能实现。如第三方网站需要接入微信登录,QQ登录,微博登录等,那第三方登录功能的实现呢,就用到刚才介绍的 oAuth2.0 的协议。
参考文章
你可以关注我的公众号「慕晨同学」,鹅厂码农,平常记录一些鸡毛蒜皮的点滴,技术,生活,感悟,一起成长。