前一段时间,参与了老项目的迁移工作,配合后端接口迁移时,由于两个项目采取了不一样的登陆方案,所以遇到了跨域登录态无法共享的问题。经过各方协调,最终老项目将迁移页面部署在新项目的指定网关下,并且使用新项目的SSO登录方案。由迁移中遇到的登陆态共享问题,引发了我对SSO的思考与学习。如果发现文章中有什么错误之处,请及时指正~🙈
JWT是一个开放的 JSON 格式 token 存储标准。它定义了一种安全、紧凑的方式来保存数据,通过签名的方式来校验 token 的合法性,主要支持的签名算法如 HMAC、RSA、ECDSA。
通常使用它来保存登录信息,相比传统的 session 方案,它的优点在于服务端无需维护登录态,不再需要依赖第三方存储(如 redis、memcached),所以说 JWT 是无状态的。
但它也存在缺点。由于它只在客户端维护,因此服务端无法方便的清除登录态,相比传统的 session 方案,只需要将 session 清除即可。你可能会说,可以直接将这个 token 删除就算退出登录了。但实际上这只是一种假注销,若该用户再次拿到相同的 token 还是会被认为是登录的。
实际上JWT是由header(头部)
、payload(负载)
、signature(签名)
这三个部分组成的,中间用.
来分隔开,写成一行就是这个样子的:Header.Payload.Signature
。
{
"alg": "HS256", // 表示签名的算法(algorithm),默认是 HMAC SHA256(写成 HS256)
"typ": "JWT" // 表示这个令牌(token)的类型(type),JWT 令牌默认统一写为 JWT
}
iss (issuer):签发人
exp (expiration time):过期时间
sub (subject):主题
aud (audience):受众
nbf (Not Before):生效时间
iat (Issued At):签发时间
jti (JWT ID):编号
⚠️ JWT 默认是不加密的,任何人都可以读到,所以不要把秘密信息放在这个部分。这个 JSON 对象也要使用 Base64URL 算法转成字符串。
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
+
、/
和=
,在 URL 里面有特殊含义,所以要被替换掉:=
被省略、+
替换成-
,/
替换成_
。这就是 Base64URL 算法。(1) Cookie
Cookie是服务器发送到用户浏览器并保存在本地的一小块数据,它会在浏览器下次向同一服务器再发起请求时被携带并发送到服务器上。通常,它用于告知服务端两个请求是否来自同一浏览器,如保持用户的登录状态。Cookie 使基于无状态的 HTTP 协议记录稳定的状态信息成为了可能。
有两种类型的 Cookie,一种是 Session Cookie(会话期 Cookie),一种是 Persistent Cookie(持久性 Cookie),如果 Cookie 不包含到期日期,则将其视为会话 Cookie。会话 Cookie 存储在内存中,永远不会写入磁盘,当浏览器关闭时,此后 Cookie 将永久丢失。如果 Cookie 包含有效期
,则将其视为持久性 Cookie,到期后,Cookie 将从磁盘中删除。
主要用途:
(2)Session
Session 代表着服务器和客户端一次会话的过程。Session 对象存储特定用户会话所需的属性及配置信息。这样,当用户在应用程序的 Web 页之间跳转时,存储在 Session 对象中的变量将不会丢失,而是在整个用户会话中一直存在下去。
常见误区:Session 不是关闭浏览器就消失了。对 Session 来说,除非程序通知服务器删除一个 Session,否则服务器会在Session失效前一直保留。大多数情况下浏览器是不会在关闭网页之前通知服务器的,所以服务器根本不知道浏览器已经关闭。之所以会有这种错觉,是大部分 session 机制都使用会话 cookie 来保存 session id,而关闭浏览器后这个 session id 就消失了,再次连接服务器时也就无法找到原来的 session。如果服务器设置的 cookie 被保存在硬盘上,或者使用某种手段改写浏览器发出的 HTTP 请求头,把原来的 session id 发送给服务器,则再次打开浏览器仍然能够打开原来的 session。
(1)关系
服务器第一次接收到请求时,开辟了一块 Session 空间(创建了Session对象),同时生成一个 session id ,并通过响应头的Set-Cookie:JSESSIONID=XXXXXXX
命令,向客户端发送要求设置 Cookie 的响应;客户端收到响应后,在本机客户端设置了一个JSESSIONID=XXXXXXX
的 Cookie 信息,该 Cookie 的过期时间为浏览器会话结束。接下来客户端每次向同一个网站发送请求时,请求头都会带上该 Cookie信息(包含 sessionId ), 然后服务器通过读取请求头中的 Cookie 信息,获取名称为 JSESSIONID 的值,得到此次请求的 session id。
(2)区别
SSO(Single sign-on)即单点登录,一种对于许多相互关联,但是又是各自独立的软件系统,提供访问控制的方法。
单点登录 (SSO) 发生在用户登录到一个应用程序,然后自动登录到其他应用程序时,无论用户使用何种平台、技术或域。例如,如果你登录 Gmail 等 Google 服务,会自动通过 YouTube、AdSense、Google Analytics 和其他 Google 应用程序的身份验证。同样,如果退出 Gmail 或其他 Google 应用程序,将自动退出所有应用程序;这称为单点注销(SLO)。
CAS(Central Authentication Service), 集中式认证服务, 是 Yale 大学发起的一个企业级的、开源的项目,旨在为 Web 应用系统提供一种可靠的单点登录解决方法(属于 Web SSO)。下图来自维基百科。
SSO 体系中的角色:
TGT 是 CAS 为用户签发的登录票据,拥有了 TGT,用户就可以证明自己在 CAS 成功登录过。TGT 封装了 Cookie 值以及此 Cookie 值对应的用户信息。当 HTTP 请求到来时,CAS 以此 Cookie 值(TGC)为 key 查询缓存中有无 TGT ,如果有的话,则相信用户已登录过。
CAS Server 生成TGT放入自己的 Session 中,而 TGC 就是这个 Session 的唯一标识(SessionId),以 Cookie 形式放到浏览器端,是 CAS Server 用来明确用户身份的凭证。
ST 是 CAS 为用户签发的访问某一 service 的票据。用户访问 service 时,service 发现用户没有 ST,则要求用户去 CAS 获取 ST。用户向 CAS 发出获取 ST 的请求,CAS 发现用户有 TGT,则签发一个 ST,返回给用户。用户拿着 ST 去访问 service,service 拿 ST 去 CAS 验证,验证通过后,允许用户访问资源。ST 只能使用一次就会失效,这与 OAuth 中的 access_token 不同。
👇PGTIOU, PGT, PT 是 CAS 2.0 的 代理模式 中的内容 ,感兴趣的同学可以自行了解。
Proxy Service的代理凭据。用户通过CAS生成一个PGT对象,缓存在PGTIOU。
是CAS的serviceValidate接口验证ST成功后,CAS会生成验证ST成功的xml消息,返回给Proxy Service,xml消息中含有PGTIOU,proxy service收到Xml消息后,会从中解析出PGTIOU的值,然后以其为key,在map中找出PGT的值,赋值给代表用户信息的Assertion对象的pgtId,同时在map中将其删除。
是用户访问Target Service(back-end service)的票据。如果用户访问的是一个Web应用,则Web应用会要求浏览器提供ST,浏览器就会用cookie去CAS获取ST,而是通过访问proxy service的接口,凭借proxy service的PGT去获取一个PT,然后才能访问到此应用。
下图是CAS官网的登录时序图,可以更好地帮助我们理解,建议细看一下~
CAS除了提供SSO功能,还提供了SLO(单点登出)功能,由于 CAS Service 和 Client Service 各维护了一个登陆态,所以两者之间的登录态是割裂的,那我们应该怎么实现SLO呢?
由于官方没有给出一个详细的流程图,所以我就根据自己的理解画了一个,供大家参考一下~
主要流程:
/logout
接口;在OAuth中“O”是Open的简称,表示“开放”的意思。Auth表示“授权”的意思,所以连起来OAuth表示“开放授权”的意思,它是一个关于授权(authorization)的开放网络标准。OAuth允许用户授权第三方应用访问他存储在另外服务商里的各种信息数据,而这种授权不需要提供用户名和密码提供给第三方应用。比较直接的例子就是第三方App使用微信或QQ来登录,这些授权登录采用的就是OAuth。
本部分只展示一下相关的时序图,就不做文档的搬运工了🐶,想了解更多的同学,可以看最后的推荐阅读部分~
这种方式是最常用的流程,安全性也最高,它适用于那些有后端的 Web 应用。授权码通过前端传送,令牌则是储存在后端,而且所有与资源服务器的通信都在后端完成。这样的前后端分离,可以避免令牌泄漏。
第四步,直接返回access_token时,有被劫持的风险,所以OAuth采用如下的方式传递 token
https://a.com/callback#token=ACCESS_TOKEN
上面这个URL,token
参数就是令牌,客户端在前端拿到令牌。注意,令牌的位置是 URL 锚点(fragment),而不是查询字符串(querystring),这是因为 OAuth 允许跳转网址是 HTTP 协议,因此存在"中间人攻击"的风险,而浏览器跳转时,锚点不会发到服务器,就减少了泄漏令牌的风险。
这种方式把令牌直接传给前端,是很不安全的。因此,只能用于一些安全要求不高的场景,并且令牌的有效期必须非常短,通常就是会话期间(session)有效,浏览器关掉,令牌就失效了。
采用这种方式不需要跳转,而是把令牌放在 JSON 数据里面,作为 HTTP 响应,客户端拿到令牌。这种方式需要用户给出自己的用户名/密码,显然风险很大,因此只适用于其他授权方式都无法采用的情况,而且必须是用户高度信任的应用。
适用于没有前端的命令行应用,即在命令行下请求令牌。这种方式给出的令牌,是针对第三方应用的,而不是针对用户的,即有可能多个用户共享同一个令牌。
每个发到 API 的请求,都必须带有令牌。具体做法是在请求的头信息,加上一个
Authorization
字段,令牌就放在这个字段里面。
服务提供商平台颁发令牌的时候,一次性颁发两个令牌,一个用于获取数据的access_token,另一个用于获取新的令牌refresh_token。access_token的过期时间较短,refresh_token的过期时间较长,当access_token过期了,就会使用refresh_token来请求新的access_token。
通过refresh_token请求access_token的url示例:
https://server.example.com/oauth/token?
grant_type``=refresh_token&
client_id``=CLIENT_ID&
client_secret``=CLIENT_SECRET&
refresh_token``=REFRESH_TOKEN
参数解释:
OAuth 是用来处理授权(authorization)而生,实现 SSO 并不是它的初衷,它只关注如何让第三方通过让用户无需登录的方式获得私有资源
CAS
ticket 生成需要足够随机,如果被攻击者猜出规律,则可以计算出下一个 ticket 值
OAuth
模块 | 描述 |
---|---|
FE-平台A | 客户端A |
BE-平台A | Server端A,为客户端A提供接口服务 |
FE-平台B | 客户端B |
BE-平台B | Server端B,为客户端B提供接口服务 |
FE-统一登录平台 | SSO登陆前端页面 |
BE-统一登录平台 | SSO登陆Server服务 |
模块 | 接口 |
---|---|
BE-平台ABE-平台B | 面向前端 • authentication:查询用户登陆状态 • userLogout:用户退出当前系统登陆 面向SSO • ssoLogout:用于SSO清除当前系统Token • sendToken:用于接收SSO-BE发送过来的Token |
BE-统一登录平台 | 面向前端 • login • sendToken:验证存在cookie,请求发送token 面向Server • searchLoginState::查询当前用户的登陆状态 • logout:用于退出登陆状态 |
接口 | type | 请求参数 | 返回参数 | 所属平台 |
---|---|---|---|---|
/authentication | get | 成功:状态码 200 失败:状态码:1001 | 平台A、B | |
/userLogout | post | 平台A、B | ||
/ssoLogout | post | 平台A、B |
接口 | type | 请求参数 | 返回参数 | 所属平台 |
---|---|---|---|---|
/login | post | account password | 登陆成功: • 状态码200 • token 登录失败: • 状态码1002 | SSO平台 |
/sendToken | post | token | SSO平台 | |
/searchLoginState | get | token?: string | 登陆态合法: • 状态码200 • token 登陆态不合法: • 状态码1001 | SSO平台 |
/logout | post | token | SSO平台 |
模块 | 页面 |
---|---|
FE-平台AFE-平台B | 主页面 • 显示当前系统用户登录成功了 • 退出登录按钮 |
FE-统一登录平台 | 登录页面 |
本demo采用eden monorepo来组织共六个子项目,分别为三个React前端子项目和三个Node后端子项目,后端采用的是公司内部封装的gulu框架。
由于Node后端需要记录当前已登陆系统的用户token,所以本Demo采用JSON文件来暂时存储已登录用户的Token,使用fs来对json文件进行读写操作,模拟存储和删除token的过程。
本次demo仅在本地运行展示,所有使用的均为本地localhost[1]端口,端口对应如下
由于只是demo演示项目,所以采用的是config文件中的代理来暂时解决。
devServer: {
proxy: {
'/api': {
target: 'http://localhost:400x',
changeOrigin: true,
pathRewrite: { '^/api': '' },
},
},
},
Q :JWT如何注销登录?
A :由于JWT是无状态的,服务端不存储它,目前为止还没有了解到有什么能够不涉及到服务端存储的注销方式。
Q :业务方接入SSO系统,但是没有实现维护登录态的服务,会出现什么问题?
A :SSO 系统,本质上仍然是 “授权” 服务, 即提供了集中式的授权管理, 但是“鉴权” 应当业务方自己实现。例如:server通过token获取到了用户的授权信息(user_id之类的),如果业务方不把这个“授权信息(登录态)” 维护起来(session/JWT),那么每次访问都需要再走到SSO Server,走一次完整流程。那么就会导致以下两个问题:1、对SSO server而言流量、请求会被放大;2、对用户而言流程变长、响应时间变慢。
有兴趣的可以看一下这篇文章:浅谈常见的七种加密算法及实现[2]
加密算法分对称加密和非对称加密,其中对称加密算法的加密与解密密钥相同,非对称加密算法的加密密钥与解密密钥不同,此外,还有一类不需要密钥的散列算法。常见的对称加密算法主要有 DES
、3DES
、AES
等,常见的 非对称算法 主要有RSA
、DSA
等,散列算法 主要有SHA-1
、MD5
等。
哈希算法的特点:
推荐阅读
傻傻分不清之 Cookie、Session、Token、JWT[3]
前端需要了解的 SSO 与 CAS 知识[4]
OAuth.0原理浅析[5]
理解OAuth 2.0[6]
OAuth 2.0 的四种方式[7]
localhost: http://localhost
[2]浅谈常见的七种加密算法及实现: https://juejin.cn/post/6844903638117122056
[3]傻傻分不清之 Cookie、Session、Token、JWT: https://juejin.cn/post/6844904034181070861#heading-17
[4]前端需要了解的 SSO 与 CAS 知识: https://juejin.cn/post/6844903509272297480
[5]OAuth.0原理浅析: https://juejin.cn/post/7010636081305485319
[6]理解OAuth 2.0: https://www.ruanyifeng.com/blog/2014/05/oauth_2_0.html
[7]OAuth 2.0 的四种方式: https://www.ruanyifeng.com/blog/2019/04/oauth-grant-types.html
以上便是本次分享的全部内容,希望对你有所帮助^_^
喜欢的话别忘了 分享、点赞、收藏 三连哦~。
欢迎关注公众号 ELab团队 收货大厂一手好文章~
我们来自字节跳动,是旗下大力教育前端部门,负责字节跳动教育全线产品前端开发工作。
我们围绕产品品质提升、开发效率、创意与前沿技术等方向沉淀与传播专业知识及案例,为业界贡献经验价值。包括但不限于性能监控、组件库、多端技术、Serverless、可视化搭建、音视频、人工智能、产品设计与营销等内容。
字节跳动校/社招内推码: 5JEEG2Q
投递链接: https://job.toutiao.com/s/8R7N4c6