OAuth 2.0 及 SSO 单点登录
什么是 OAuth
OAuth 即开放授权,是一个开放标准,允许用户让第三方应用访问该用户在某一网站上存储的私密的资源,而无需将用户名和密码提供给第三方应用。比如访问某小程序又不想注册时则可以使用微信授权。以下主要介绍 OAuth 2.0 ,它是 OAuth 协议的下一版本,但不向下兼容 OAuth 1.0。
运行流程
+--------+ +---------------+
| |--(A)- Authorization Request ->| Resource | A. 用户要访问客户端时,客户端需要用户给予授权。
| | | Owner |
| |<-(B)-- Authorization Grant ---| | B. 用户同意给予客户端授权。
| | +---------------+
| |
| | +---------------+
| |--(C)-- Authorization Grant -->| Authorization | C. 客户端向认证服务器申请令牌。
| Client | | Server |
| |<-(D)----- Access Token -------| | D. 认证服务器对客户端进行认证后发放令牌。
| | +---------------+
| |
| | +---------------+
| |--(E)----- Access Token ------>| Resource | E. 客户端使用令牌,向资源服务器申请获取资源。
| | | Server |
| |<-(F)--- Protected Resource ---| | F. 资源服务器确认令牌无误,同意向客户端开放资源。
+--------+ +---------------+
授权模式
客户端必须得到用户的授权(authorization grant),才能获得令牌(access token)。OAuth 2.0 定义了四种授权方式:
- 授权码模式(authorization code)
- 简化模式(implicit)
- 密码模式(resource owner password credentials)
- 客户端模式(client credentials)
授权码模式
授权码模式(authorization code)是功能最完整、流程最严密的授权模式。它的特点就是通过客户端的后台服务器,与”服务提供商”的认证服务器进行互动:
+----------+
| Resource |
| Owner |
| |
+----------+
^
|
(B)
+----|-----+ Client Identifier +---------------+
| -+----(A)-- & Redirection URI ---->| | A. 用户访问客户端,后者将前者导向认证服务器。
| User- | | Authorization |
| Agent -+----(B)-- User authenticates --->| Server | B. 用户选择是否给予客户端授权。
| | | |
| -+----(C)-- Authorization Code ---<| | C. 认证服务器将用户导向客户端事先指定的重定向地址,同时附上一个授权码 code。
+-|----|---+ +---------------+
| | ^ v
(A) (C) | |
| | | |
^ v | |
+---------+ | |
| |>---(D)-- Authorization Code ---------' | D. 客户端收到授权码,向认证服务器申请令牌。
| Client | & Redirection URI |
| | |
| |<---(E)----- Access Token -------------------' E. 认证服务器核对了授权码和重定向 URI,确认无误后,向客户端发送访问令牌和更新令牌。
+---------+ (w/ Optional Refresh Token)
A 步骤中,客户端申请认证的 URI,包含以下参数:
- response_type - 必选。表示授权类型,固定值为 “code”
- client_id - 必选。表示客户端的 ID
- redirect_uri - 可选。表示重定向 URI
- scope - 可选。表示申请的权限范围
- state - 表示客户端的当前状态,可以指定任意值,认证服务器会原封不动地返回这个值
/authorize?response_type=code&client_id=s6BhdRkqt3&state=xyz&redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb
<!-- 简书登录跳微信认证的页面 -->
https://open.weixin.qq.com/connect/qrconnect?appid=wxe9199d568fe57fdd&client_id=wxe9199d568fe57fdd&redirect_uri=http%3A%2F%2Fwww.jianshu.com%2Fusers%2Fauth%2Fwechat%2Fcallback&response_type=code&scope=snsapi_login&state=%257B%257D#wechat_redirect
C 步骤中,服务器回应客户端的 URI,包含以下参数:
- code - 必选。表示授权码,具有较短的有效期且客户端只能使用一次,否则会被授权服务器拒绝。该码与客户端 ID 和重定向 URI,是一一对应关系
- state - 如果客户端的请求中包含这个参数,则原封不动返回
https://client.example.com/cb?code=SplxlOBeZQQYbYS6WxSbIA&state=xyz
D 步骤中,客户端向认证服务器申请令牌的 HTTP 请求,包含以下参数:
- grant_type - 必选。表示使用的授权模式,值固定为 “authorization_code”
- code - 必选。表示上一步获得的授权码
- redirect_uri - 必选、表示重定向 URI,且必须与 A 步骤中的该参数值保持一致
- client_id - 必选。表示客户端 ID
grant_type=authorization_code&code=SplxlOBeZQQYbYS6WxSbIA&redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb
E 步骤中,认证服务器发送的 HTTP 回复,包含以下参数:
- access_token - 必选。表示访问令牌
- token_type - 必选。表示令牌类型,该值大小写不敏感,可以是 bearer 类型或 mac 类型
- expires_in - 表示过期时间,单位为秒。如果省略该参数,必须其他方式设置过期时间
- refresh_token - 可选。表示更新令牌,用来获取下一次的访问令牌
- scope - 表示权限范围,如果与客户端申请的范围一致,此项可省略
{
"access_token": "2YotnFZFEjr1zCsicMWpAA",
"token_type": "example",
"expires_in": 3600,
"refresh_token": "tGzv3JOkF0XG5Qx2TlKWIA",
"example_parameter": "example_value"
}
简化模式
简化模式(implicit grant type)不通过第三方应用程序的服务器,直接在浏览器中向认证服务器申请令牌,跳过了”授权码”这个步骤。所有步骤在浏览器中完成,令牌对访问者是可见的,且客户端不需要认证:
+----------+
| Resource |
| Owner |
| |
+----------+
^
|
(B)
+----|-----+ Client Identifier +---------------+
| -+----(A)-- & Redirection URI --->| | A. 客户端将用户导向认证服务器。
| User- | | Authorization |
| Agent -|----(B)-- User authenticates -->| Server | B. 用户选择是否给于客户端授权。
| | | |
| |<---(C)--- Redirection URI ----<| | C. 认证服务器将用户导向客户端事先指定的重定向地址,并在 URI 的 Hash 部分包含了访问令牌。
| | with Access Token +---------------+
| | in Fragment
| | +---------------+
| |----(D)--- Redirection URI ---->| Web-Hosted | D. 浏览器向资源服务器发出请求,其中不包括上一步收到的 Hash 值。
| | without Fragment | Client |
| | | Resource |
| (F) |<---(E)------- Script ---------<| | E. 资源服务器返回一个网页,其中包含的代码可以获取 Hash 值中的令牌。
| | +---------------+ F. 浏览器执行上一步获得的脚本,提取出令牌。
+-|--------+
| |
(A) (G) Access Token G. 浏览器将令牌发给客户端。
| |
^ v
+---------+
| |
| Client |
| |
+---------+
密码模式
密码模式(Resource Owner Password Credentials Grant)中,用户向客户端提供自己的用户名和密码。客户端使用这些信息,向”服务商提供商”索要授权。在这种模式中,用户必须把自己的密码给客户端,但是客户端不得储存密码。这通常用在用户对客户端高度信任的情况下,比如客户端是操作系统的一部分,或者由一个著名公司出品。而认证服务器只有在其他授权模式无法执行的情况下,才能考虑使用这种模式:
+----------+
| Resource |
| Owner |
| |
+----------+
v
| Resource Owner
(A) Password Credentials A. 用户向客户端提供用户名和密码。
|
v
+---------+ +---------------+
| |>--(B)---- Resource Owner ------->| | B. 客户端将用户名和密码发给认证服务器,向后者请求令牌。
| | Password Credentials | Authorization |
| Client | | Server |
| |<--(C)---- Access Token ---------<| | C. 认证服务器确认无误后,向客户端提供访问令牌。
| | (w/ Optional Refresh Token) | |
+---------+ +---------------+
客户端模式
客户端模式(Client Credentials Grant)指客户端以自己的名义,而不是以用户的名义,向”服务提供商”进行认证。严格地说,客户端模式并不属于 OAuth 框架所要解决的问题。在这种模式中,用户直接向客户端注册,客户端以自己的名义要求”服务提供商”提供服务,其实不存在授权问题:
+---------+ +---------------+
| | | |
| |>--(A)- Client Authentication --->| Authorization | A. 客户端向认证服务器进行身份认证,并要求一个访问令牌。
| Client | | Server |
| |<--(B)---- Access Token ---------<| | B. 认证服务器确认无误后,向客户端提供访问令牌。
| | | |
+---------+ +---------------+
更新令牌
如果用户访问的时候,客户端的”访问令牌”已经过期,则需要使用”更新令牌”申请一个新的访问令牌:
+--------+ +---------------+
| |--(A)------- Authorization Grant --------->| |
| | | |
| |<-(B)----------- Access Token -------------| |
| | & Refresh Token | |
| | | |
| | +----------+ | |
| |--(C)---- Access Token ---->| | | |
| | | | | |
| |<-(D)- Protected Resource --| Resource | | Authorization |
| Client | | Server | | Server |
| |--(E)---- Access Token ---->| | | |
| | | | | |
| |<-(F)- Invalid Token Error -| | | |
| | +----------+ | |
| | | |
| |--(G)----------- Refresh Token ----------->| |
| | | |
| |<-(H)----------- Access Token -------------| |
+--------+ & Optional Refresh Token +---------------+
部署
推荐阅读这篇文章 👈
什么是单点登录
单点登录(Single Sign On,即 SSO)是多域名企业站点流行的登录方式,指用户只需输入一次账密,在一处完成登录,之后可以直接进入所有业务系统。想要完成单点登录的效果,必须有一个唯一身份源,其他业务系统必须配合完成改造和对接。目前主流的 SSO 技术有 CAS、OAuth2、SAML、xxl-sso 等。
同域下的单点登录实现
普通的登陆认证机制可以基于 cookie 和 session 去实现,具体可参考这篇。
一个企业一般情况下只有一个域名,通过二级域名区分不同的系统。比如我们有个域名叫做:a.com,同时有两个业务系统分别为:app1.a.com 和 app2.a.com。我们要做单点登录,需要一个登录系统,叫做:sso.a.com。
我们只要在 sso.a.com 登录,app1.a.com 和 app2.a.com 就也登录了。通过上面的登陆认证机制,我们可以知道,在 sso.a.com 中登录了,其实是在 sso.a.com 的服务端的 session 中记录了登录状态,同时在浏览器端的 sso.a.com 下写入了 Cookie。那么我们怎么才能让 app1.a.com 和 app2.a.com 登录呢?这里有两个问题:
- Cookie 是不能跨域携带的,我们 Cookie 的 domain 属性是 sso.a.com,在给 app1.a.com 和 app2.a.com 发送请求是带不上的。
- sso、app1 和 app2 是不同的应用,它们的 session 存在自己的应用内,是不共享的。
解决方案:
- 可以将 Cookie 的域设置为顶域,即.a.com,这样所有子域的系统都可以访问到顶域的 Cookie
- 共享 Session 的解决方案有很多,例如:Spring-Session
跨域下的单点登录 CAS 实现
同域下的单点登录是巧用了 Cookie 顶域的特性。跨域的话可以使用 CAS 流程,这个流程是单点登录的标准流程。CAS(Central Authentication Service) 即中心授权服务,是耶鲁大学发起的一个开源项目,旨在为 Web 应用系统提供一种可靠的单点登录方法:
- 用户访问 app.example.com 网页,但用户未登录。
- 跳转到 CAS server,即 SSO 登录系统。SSO server 找不到用户信息,弹出用户登录页。
- 用户填写用户名、密码,SSO server 进行认证后,将登录状态写入 SSO 的 session(key 为 TGT,即 Ticket Granting Ticket),浏览器中写入 SSO 域下的 Cookie(名称为 CASTGC)。
- SSO server 登录完成后会生成一个 ST(Service Ticket),然后跳转到网页,同时将 ST 作为参数传递给网页。
- 网页拿到 ST 后,向 SSO 发送请求,验证 ST 是否有效。
- 验证通过后,网页将更新 session 并设置客户端的 Cookie(默认名为 jSESSIONID)。
- 用户再次访问该网页时,带上 cookie 即可认证。如果访问的是网页 2,由于 SSO 能够检测到用户,所以也不需要重新登录了,分配新的 ST 即可
至于为啥中间要多一步 ST 认证环节?用户在给 SSO 服务器提供了用户名密码后,作为业务系统并不知道这件事。SSO 随便给业务系统一个 ST,那么业务系统是不能确定这个 ST 是用户伪造的,还是真的有效,所以要拿着这个 ST 去 SSO 服务器再问一下,这个用户给我的 ST 是否有效,是有效的我才能让这个用户访问。
总体而言。从开发集成难易程度方面考虑,依次为 CAS、OAuth2 稍复杂,SAML 最复杂。OAuth2 是目前互联网最流行的单点登录技术,比如微信平台、QQ 平台、钉钉平台等,但在企业应用方面,OAuth2 使用远没有 CAS 多,尤其是企业存在大量的存量系统,有的是前后端分离架构,基于 token 认证鉴权,有的是传统 SOA 架构,基于中间件 session 会话认证鉴权,所以在企业内部改造 OAuth2 的成本比较高。SAML 是协议最复杂的一种 SSO,安全性最好,仅仅适用于 web,开发集成难度高,一般企业内部的应用系统不推荐使用。