分布式会话技术
会话技术
HTTP协议的本质:无状态(Stateless)。
无状态意味着服务器不会记住任何关于客户端的过往请求。你第一次访问和第一万次访问,对于服务器来说,都是一个全新的、陌生的请求。
问题出现:Web应用很快就需要“记住”用户,比如用户是否登录、购物车里有什么商品。如果无法记住,用户每点击一个链接就需要重新登录一次,购物车也会被清空。
Session的诞生:为了解决这个问题,开发者们引入了“Session”这个逻辑概念。它的目标很简单:在无状态的HTTP之上,人为地创造出一个“有状态的对话(Stateful Conversation)”。
常见的技术有3种:
cookie:我们接下来会看到的技术。安全设计比较完备。
Set-Cookie
和Cookie
是对位的header。url query param:服务器主动把返回的 html 里所有的
<a href>
都加上 session id。这种方式是特别不安全的,因为分享链接意味着会话状态暴露。隐藏表单。在生成 HTML 页面时,动态地嵌入一个
<input>
标签,并将其 type 属性设置为 hidden。服务器在处理一个请求时,确定了有一些信息需要“记住”或者传递到下一个请求。这些信息不是给用户看的,而是给程序自己用的。例如:产品ID、用户的唯一标识、一个安全令牌(Token)等。完全不安全(Hidden is NOT Secure):这是它最致命的缺点。“隐藏”不等于“安全”。任何一个懂点的用户都可以通过在浏览器中“查看网页源代码”或使用开发者工具(F12)轻松看到隐藏域的值。
这三种技术都不能真的实现把信息完全从服务端剥离的目的,数据细节仍然是需要放在专门的存储-MySQL、Redis里,而这些载体主要存储 id。
关键 header
每次都应该有的基础Header
- Content-Type
- 作用:告诉浏览器响应体是什么类型的数据。这是最最基本的Header之一。
- 示例:Content-Type: text/html; charset=utf-8 (HTML页面), Content-Type: application/json (JSON数据), Content-Type: image/jpeg (图片)。
- 重要性:如果缺失,浏览器只能去猜测内容类型,可能导致页面渲染错误或安全问题,会导致很多文件不能打开,而只能下载。
- Content-Length 或 Transfer-Encoding
- 作用:告诉浏览器响应体的长度。
- Content-Length: 一次性告知总长度。
- Transfer-Encoding: chunked: 表示响应体是分块传输的,长度未知。
- 重要性:两者必有其一(对于有响应体的请求),用于浏览器正确接收数据和管理连接。否则response可能会中断。
- Date
- 作用:提供响应消息被创建的日期和时间。
- 重要性:HTTP/1.1规范要求服务器包含此字段。
强烈推荐每次都包含的Header(尤其是安全和缓存)
- Cache-Control
- 作用:精细地控制浏览器和代理服务器如何缓存此响应。
- 示例:Cache-Control: no-store (完全不缓存), Cache-Control: public, max-age=3600 (公开缓存1小时)。
- 重要性:对于提升网站性能和保证数据新鲜度至关重要。
- Strict-Transport-Security (HSTS)
- 作用:强制浏览器在未来的一段时间内,只能通过HTTPS访问此站点。
- 重要性:极大地提升了网站的安全性,防止中间人攻击。
- X-Content-Type-Options
- 作用:固定为nosniff,防止浏览器对Content-Type进行“嗅探”猜测,避免某些类型的攻击。
- 重要性:一个简单有效的安全加固措施。
- Content-Security-Policy (CSP)
- 作用:定义一个可信内容源的白名单,帮助抵御XSS(跨站脚本)攻击。
- 重要性:现代Web安全的核心防御机制之一。
cookie 技术
cookie 是为了“弥补 http 无状态”而特别设计出来的“可重放的 header”。它的定位 small block of data。起源于 Unix 程序员里的小 packet data,recieved and sends back unchanged,本来是网景用来实现虚拟购物车用的,后来逐渐成为严重的隐私问题。为了防止被盗用,通常它不是具体的数据,而是一个加密过的id,或者一个会话id。
cookie 的属性
domain:如果不设置就是默认当前的域(包括子域):d2ksr8gxr5kl2o(子域名).cloudfront(主域名).net(顶级域名):sina.com(主域名),sso.sina.com(二级域名)。
- d2ksr8gxr5kl2o.cloudfront.net 的 cookie 不能用在任何 xxx.cloudfront.net 上。
path:如果不设置则为当前路径的父,所以最好设置为/。
MaxAge/Expires:旧版本协议用 Expires,新版本用MaxAge。每次check ticket/ check token,得到了 expire time,都要专门使用 Set-Cookie 来延长 cookie 的寿命,不要忘记。
HttpOnly:当设置为true时,表示该 Cookie 只能通过 HTTP/HTTPS 协议访问。这种设置可以防止 JavaScript 通过 document.cookie 访问该 Cookie,从而降低 XSS (跨站脚本)攻击的风险。这是 Web 安全的最佳实践之一,特别适用于存储敏感信息(如认证令牌)的Cookie。
Secure: 是否使用 https
SameSite:
SameSite=Strict:最严格。只有当请求完全来自于你当前正在浏览的网站时,才会发送Cookie。比如你在example.com里点击一个链接跳转到example.com/other,会发送。但如果你在google.com上点击一个链接跳转到example.com,则不会发送。
SameSite=Lax (现代浏览器默认值):一个折中。允许在一些顶层导航(比如从其他网站点击链接跳转过来)时发送Cookie,但会阻止在跨站的子请求(如加载图片、iframe、AJAX请求)中发送。
SameSite=None:最宽松。允许在所有跨站请求中发送Cookie,但必须同时设置Secure标志(即只能用于HTTPS)。
cookie 的重放
总结:服务器对Set-Cookie的使用是指令性和事件驱动的,而不是每次都无脑重放。
Set-Cookie 是一个特殊的、浏览器能看懂的指令。浏览器识别出这个Header后,会自动地、安全地将Cookie内容保存在自己专门的存储区里。浏览器会自动地检查本地存储的Cookie,如果发现有匹配当前域名和路径的Cookie,就会在后续的请求中自动地添加 Cookie 请求头,把数据发回给服务器。
服务端通常不需要每次都 Set-Cookie(完全没有这个规范,而且通常是应该极力避免的错误实践),但是如果如果需要动态更新 cookie,则需要设计一个前后端不断接力的 cookie 机制。
服务端频繁 Set-Cookie 的缺点是:
浪费带宽:每个HTTP响应都增加了一些不必要的Header数据。
- 逻辑混乱:让开发者和调试工具难以判断Set-Cookie的真实意图。
服务器只在以下四种明确的场景下,才应该发送Set-Cookie头:
创建Cookie:当用户首次登录或触发某个功能时,服务器需要创建一个新的会话或状态标识,此时发送Set-Cookie来在浏览器中“种下”这个Cookie。
修改Cookie:当需要更新Cookie的值时。一个最经典的安全实践是会话更新(Session Regeneration):当用户登录成功后,服务器应该废弃旧的会话ID(登录前的),并生成一个全新的会话ID,通过Set-Cookie下发给浏览器,以防止会话固定(Session Fixation)攻击。
续期Cookie:如果你的会话策略是“滑动过期”(Sliding Expiration,即用户有操作就自动延长过期时间),那么在用户每次与服务器交互时,服务器都应该发送一个带有新的过期时间的Set-Cookie头,来为会话“续命”。
删除Cookie:删除一个Cookie的标准方法,就是发送一个同名、同路径、同域的Set-Cookie头,并将其过期时间设置为一个过去的时间点(或Max-Age=0)。
与其他 Web Storage(IndexedDB、LocalStorage)对比
Cookie的局限性
容量太小:每个Cookie的大小被限制在4KB左右,一个域名下的Cookie总数也有限制(通常是20-50个)。这对于存储少量身份信息足够,但无法存储更复杂的数据。
性能开销:Cookie在每次HTTP请求中都会被完整地携带在请求头里,即使当前请求完全不需要这些数据(比如请求一张图片或一个CSS文件)。如果Cookie很大,会明显增加网络流量,造成不必要的性能浪费。
API不友好:原生操作Cookie的API document.cookie 是一个简单的字符串,读写和解析都非常不便。
定位不纯粹:Cookie的设计初衷是“服务器与客户端的通信”,而不是纯粹的“客户端本地存储”。
third-party cookie
第一步:埋下追踪器
你访问了一个新闻网站 A.com。
这个网站上有一个来自广告联盟 Ad-Network.com 的广告位。当你加载A.com时,你的浏览器也向Ad-Network.com发出了请求以下载广告。
此时,Ad-Network.com 就在你的浏览器里“种下”了一个属于它自己的第三方Cookie。这个Cookie里有一个独一无二的ID,比如 ID=xyz123。
第二步:跨站追踪
过了一会儿,你又去访问一个购物网站 B.com,它恰好也使用了同一个广告联盟 Ad-Network.com。
当你加载B.com时,浏览器同样会请求Ad-Network.com的资源。这时,浏览器会自动带上之前存下的那个ID=xyz123的Cookie。
Ad-Network.com 的服务器看到这个ID,立刻就知道:“哦!访问B.com的这个人,就是刚才访问了A.com的那个人!”
第三步:建立你的“数字档案”
你继续访问汽车论坛 C.com,旅游博客 D.com… 只要这些网站上有 Ad-Network.com 的“探针”(广告、分析脚本等),你的行踪就会被一次又一次地记录下来。
久而久之,Ad-Network.com 就围绕着 ID=xyz123 这个Cookie,建立起了一个关于你的、极其详尽的“数字侧写”或“影子档案”:
你的兴趣:你对数码产品、汽车、东南亚旅游感兴趣。
你的意图:你最近可能在考虑买车或计划度假。
你的特征:结合数据分析,它甚至能推断出你的大致年龄、消费水平、政治倾向等。
Web Storage(LocalStorage, SessionStorage)的出现解决了这些问题
容量更大:LocalStorage提供了5-10MB的存储空间,远超Cookie的4KB,可以存储大量的JSON数据、配置信息等。
性能更优:LocalStorage中存储的数据不会被自动附加到HTTP请求头中。它纯粹是客户端的本地仓库,只在需要时通过JavaScript手动读取。这避免了Cookie带来的性能开销。
API更友好:提供了简单的键值对API,如 localStorage.setItem(‘key’, ‘value’), localStorage.getItem(‘key’),操作非常方便。
定位纯粹:它的定位就是纯粹的客户端存储,与服务器通信无关。
IndexedDB则更进一步
如果说LocalStorage是一个简单的“键值对”仓库,那么IndexedDB就是一个功能完善的客户端小型数据库。
解决复杂数据存储:它支持存储大量的结构化数据(JSON对象),并能创建索引来快速查询。这对于需要离线功能、本地数据缓存、PWA(渐进式Web应用)等复杂场景至关重要。
支持事务:IndexedDB的操作是事务性的,保证了数据操作的原子性、一致性、隔离性和持久性(ACID特性),非常可靠。
异步API:它的API是异步的,不会阻塞浏览器主线程,适合处理大量数据的读写操作。
总结
特性 | Cookie (小饼干) | localStorage (文件柜) | IndexedDB (图书馆) |
---|---|---|---|
核心用途 | 服务器驱动的状态管理。主要用于服务器识别用户、维持会话(如登录状态) | 客户端的简单数据存储。主要用于存储用户偏好、UI状态等纯客户端数据 | 客户端的复杂数据存储。用于构建离线应用、缓存大量结构化数据 |
与服务器通信 | 自动发送。在每次符合条件的HTTP请求中,都会被自动添加到 Cookie 请求头里 | 从不自动发送。必须由你手动编写JavaScript代码(如 fetch)来读取并发送给服务器 | 从不自动发送。和 localStorage 一样,必须手动发送 |
容量大小 | 非常小,约 4KB | 较大,约 5-10MB | 巨大,可达数百MB甚至GB级别(取决于浏览器和用户授权) |
数据类型 | 只能是字符串 | 只能是字符串(通常用 JSON.stringify 存对象) | 几乎所有类型:字符串、数字、对象、数组、文件(Blob)等 |
API易用性 | 难用。通过 document.cookie 操作,需要手动解析字符串 | 非常简单。同步的.setItem, getItem 键值对操作 | 非常复杂。异步的、基于事务和事件的API,像一个真正的数据库 |
生命周期 | 可设置过期时间。到期后浏览器自动删除 | 永久有效。除非用户手动清除或代码主动删除,否则永不过期 | 永久有效。和 localStorage 一样 |
访问范围 | 只能在主线程中访问 | 只能在主线程中访问 | 可以在Web Workers (后台线程)中访问,适合处理复杂计算不阻塞页面 |
Cookie 的本质是“服务器的备忘录”,它写在你的浏览器上,每次你访问服务器时,浏览器都会自动带上这个备忘录。它的缺点是容量小,且每次都带上会浪费流量。
localStorage 是纯粹“浏览器的记事本”,你想记什么就记什么,服务器完全不知道。只有当你需要时,你才用代码把记事本里的内容告诉服务器。
IndexedDB 是“浏览器的数据库”,当你的应用需要在本地存储大量结构化信息(比如一个邮件客户端要离线存储所有邮件),并且需要高效查询时,就用它。
jwt
起因
现代的 http 的客户端实现和服务器端实现并不是真正的无状态。
针对特定的域,客户端需要 cookie 重放:
- 但是移动端的浏览器没有 cookie 机制。
- cookie 重放要处理跨域问题。
服务器必须使用内存来关联 session id,把 session 的内容放在单一服务器,或者分布式 session 的载体里。这就产生了 session 共享、session 黏着之类的与“无状态”正相反的 http 服务端问题。
核心思想
服务器不再存储Session,而是将用户信息本身加密后生成一个令牌(Token),返给客户端。客户端在后续请求中携带这个Token,服务器只需验证Token的合法性即可,无需查询任何存储。
结构
Header.Payload.Signature
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Header (头部):描述JWT元数据,通常包含两部分:令牌类型(typ: “JWT”)和所使用的签名算法(alg: “HS256”, “RS256”等)。这部分会进行Base64Url编码。
Payload (载荷):存放实际需要传递的数据,也称为Claims(声明)。包含一些标准字段(如iss签发者, exp过期时间, sub主题),也可以自定义私有字段(如userId, role)。注意:Payload默认只是Base64Url编码,不是加密,所以不应存放敏感信息。
Signature (签名):这是JWT最核心的部分。计算方式是:
将编码后的Header和编码后的Payload用点 . 连接起来。
使用Header中指定的算法,配合一个只有服务器知道的密钥 (Secret),对连接后的字符串进行签名。
签名的作用是:保证Token在传输过程中没有被篡改。当服务器收到Token后,会用同样的算法和密钥重新计算签名。如果计算出的签名与Token中的签名一致,说明数据是可信的;如果不一致,则说明Token是伪造的或被修改过。
验证合法性只要使用单向签名算法就能实现。
在实践中,我们在 jwt 的 claim 里最好只放入一些会话id,把大的数据结构存入 db 或者 redis里,把 jwt的 claim 当作存储指针的地方来用。
例子
1 |
|
SSO
精确的SSO登录流程 (结合联邦认证)
这个流程描述了当用户首次访问系统,且 SSO 认证中心本身也需要借助第三方(如QQ)来完成认证的完整过程。
核心参与者
用户 (浏览器): 用户的设备。
系统 A (应用/Service Provider): 用户想要访问的业务系统,例如 app.a.com。
SSO 服务器 (认证中心/Identity Provider): 统一的身份认证服务,例如 sso.com。
QQ (外部认证方/External IdP): 提供身份信息的第三方,例如 qq.com。
流程步骤
访问应用: 用户在浏览器中访问系统 A (
app.a.com
) 的受保护页面。系统 A 检查发现浏览器请求中没有自己的登录凭证(app_a_cookie
),判定用户未登录。重定向至 SSO: 系统 A 将用户的浏览器重定向到SSO服务器的登录地址,并在 URL 中附带自己的身份和期望的回调地址。
sso.com/login?service=https://app.a.com/callback
SSO 检查并再次重定向: SSO 服务器检查发现浏览器请求中也没有 SSO 的全局登录凭证(
sso_cookie
),判定用户在认证中心也未登录。此时,SSO 服务器呈现一个登录页面,上面有多种登录选项(如:用户名密码、使用 QQ 登录)。用户选择 QQ 登录: 用户点击“使用 QQ 登录”按钮。SSO 服务器将浏览器重定向到 QQ 的授权页面,并附带自己的客户端 ID 和预设的回调地址(指向 SSO 自己)。
qq.com/auth?client_id=sso_client_id&redirect_uri=https://sso.com/auth/qq/callback
QQ 认证与授权: 用户在 QQ 的页面上输入账号密码,并同意授权 SSO 服务器获取其基本信息。
QQ 回调 SSO: QQ 认证成功后,将浏览器重定向回第4步中指定的 SSO 回调地址,并在 URL 中附带一个一次性的
authorization_code
。sso.com/auth/qq/callback?code=qq_auth_code_123
SSO后台换取用户信息: SSO服务器的后端收到
authorization_code
后,向QQ的API服务器发起一个安全的后台HTTP请求,用code
换取access_token
,再用access_token
获取用户的唯一标识(openid
)等信息。SSO 建立全局会话: SSO 服务器确认了用户的身份,此时:
- 在 SSO 域 (
sso.com
) 下为浏览器种下全局登录凭证(sso_cookie
)。 - 这标志着用户已经在单点登录系统中完成了认证。
- 在 SSO 域 (
SSO 携带 Ticket 重定向回系统 A:SSO 服务器现在将浏览器重定向回系统 A 在第2步中提供的回调地址,并在URL中附带一个一次性票据 (Ticket)。在现实中还会有多余的一步,首先返回一个 uid列表(如果一个qq下有多个 uid的话),让用户选择 uid 以后,再根据 uid 返回一个独一无二的 ticketA(只绑定 uid)。
app.a.com/callback?ticket=ST-xyz-789
系统 A 后台验证 Ticket: 系统 A 的后端收到
ticket
后,向SSO服务器的验证接口发起一个安全的后台HTTP请求来核销这个ticket
。SSO 验证并返回用户信息: SSO 服务器验证
ticket
有效(且是发给系统A的),然后立即销毁该ticket
防止重放攻击。验证成功后,向系统 A 返回该用户的身份信息(如统一的uid
、role
等)。系统 A 建立本地会话: 系统 A 收到了确认信息,信任了该用户。它会在自己的域 (
app.a.com
) 下为浏览器种下应用自身的登录凭证(app_a_cookie
),并最终将用户带到他最初想访问的页面。
至此,登录流程完成。如果用户再去访问已接入 SSO 的系统 B,流程会大大简化:他会被重定向到 SSO(步骤2),SSO 检测到sso_cookie
已存在(步骤8),会直接跳过登录和QQ认证,立即执行步骤9,发放一个新票据给系统B,实现无感知的“单点登录”。
1 |
|
设计思想
每个系统都是靠本域下的 cookie 来省却登录流程,如果无 cookie,要先确定是不是本系统作为最初授权源。如果不是,去找授权源。
授权源完成授权是用户手动操作,如果完成以后,向授权请求方给出一个一次性令牌( ticket/access_token),再通过 query params 之类的方案回调。
授权请求方得到一次性令牌后,再通过自身的鉴权机制拿这个一次性去授权源验证,获取令牌带有的长效信息(如用户的 OpenId)。
授权请求方把长效信息和一个本地令牌(会话id/sso_token)关联起来,然后把长效信息存在缓存里,或者每次验证本地令牌时去询问授权源。
本地令牌给 api 使用方,使用方每次来访问本系统,如果有 cookie 都直接验证账号和 token 的关联性,直接进入下一步,否则就经浏览器跳转到授权源。
授权源的拓扑关系大概是:user -> gateway (本地令牌提供方) -> sso 中心 -> 真授权方:
- 如果一个页面有 gateway 的 cookie,则请求直接放行
- 如果没有 gateway 的 cookie,有 sso 的 cookie,则 sso 不需要登录,带着 ticket 跳回 gateway
- 否则执行登录流程
任何从没有 sso 登录过的新系统登录时,如从 A 到 B,B 会到达 SSO,直接触发 SSO 的 cookie 的免登录通过。
跳转都是经过浏览器实现的,多层调用有逐层嵌套和解套回调:
尽量从 Referer 头获取回调地址。
回调地址里面要尽量去掉 sso_ticket 这种sso 回调才会添加的附加参数,避免触发一次性凭证的校验机制。
安全问题
如果 token 或者 ticket 被复制、分享或者窃听拿来用,应该怎么防御水平越权
保证这些明文不被挪用的机制有:
验证凭证要使用加密协议,而且 caller 要单独向 callee 鉴权。
ticket 和 token 最好在授权源和账号、回调 uri 绑定,不单独使用,和某些明 principal id 一起混合使用。
- 如果从多个区域(header、body和 query param)都能获取账号与 sso 校验,而数据库写入只从 body 取值,这就产生了一种可以窃取 request 的其他部分,但是修改 body 到其他值的水平越权问题。解法是:统一从一个地方-body来获取账号值。
水平权限兼容问题
如我有1个token可以关联账号1和2,但不允许访问3。那么怎样让1和2可以互相切换,但是禁止切换3,就是这个体系的边界要考虑的问题了。
死循环
已存在的 token 解析出一个登录账号,然后重定向到 sso 服务,因为 sso 域下的 cookie 已存在,又会重定向回这个登录账号在当前账号下的页面。即/9/account
已经登录了,浏览器输入了一个/10/account
,则需要靠重定向到 sso,再重定向回来/9/account
。如果给 sso 的重定向uri是/10/account
则可能产生无限循环。
解法
解析出一个不会循环的地址
尝试给出一个和 sso_token 匹配的回调地址再登录,比如当前错误地从/9/account
跳到/10/account
,如果能够解析出9来,就不会产生循环。有些 sso 系统不支持。因为 cookie 可能账户绑定关系是一对多的,没人知道具体原来使用了哪个账户(可能一个 token绑定了7、8、9)。
选择账号列表法
如果已经有了正确的 cookie 跨了错误的账号(no permission on this account问题):
- 回到 sso 中心的账号选择列表,用新的账号,跳到当前网站的主页。即用户可以使用 1、2,但是误跳到了3,重新回到账号选择界面,让它在 1 和 2 里选择。
重登陆法
本 token 本身已经不合法:
登出:一次性清除全部的 cookie-当前登录系统的 cookie + 所有能通知到的系统的 cookie。
- 一个完整清除 cookie 的 header 是:
Set-Cookie: sso_token=; Expires=Thu, 01 Jan 1970 00:00:00 GMT; Max-Age=0; Domain=.example.com; Path=/
。如果原 cookie 没有设置 domain,这里也不设置 domain,用的就是当前的 host 全地址(不包括所有二级域名),但是 path 即使是/
也是必须的。
- 一个完整清除 cookie 的 header 是:
带有 status code == 0,加 data.code != 0 + location header 回 sso 服务。
SSO登出流程 (Logout)
SSO的核心理念是“一次登录,处处通行”。相应地,登出的核心理念应该是“一次登出,处处失效”,这被称为单点登出 (Single Logout, SLO)。
如果只清除了当前应用(系统 A )的登录状态,而 SSO 认证中心的全局会话依然存在,那么用户刷新页面后,系统 A 会再次将他重定向到 SSO,SSO 发现全局会话还在,会立刻又把他自动登录回系统 A。这就导致了用户无法真正登出的尴尬局面。
因此,正确的登出流程必须由 SSO 认证中心来协调。也因此,中间服务不要缓存 sso_token,因为不一定能感知到这个 sso_token 被他处 invalidate 了。
流程步骤:
用户发起登出: 用户在系统A的界面上点击“登出”按钮。
系统 A 清理本地会话: 系统 A 的后端接收到登出请求后,首先执行本地登出操作。这包括:
- 销毁系统 A 自身的会话 (Session)。
- 清除种在
app.a.com
域下的本地登录凭证(app_a_cookie
)-这一步需要重新使用 Set-Cookie header 设置一个过期的 cookie。
重定向至 SSO 进行全局登出: 清理完本地会话后,系统 A 必须将用户的浏览器重定向到 SSO 服务器的登出地址。通常还会附带一个参数,告诉 SSO 登出后应该跳转回哪里。注意这个参数是靠重放 cookie 而不是携带参数来实现的。
sso.com/logout?post_logout_redirect_uri=https://app.a.com/login
SSO 执行全局登出: SSO 服务器接收到请求后,执行全局登出操作:
- 销毁 SSO 的中央认证会话。
- 清除种在
sso.com
域下的全局登录凭证(sso_cookie
)。
SSO 通知所有相关应用登出 (关键步骤): 这是实现单点登出 (SLO) 的核心。SSO 服务器会查找在此次全局会话期间,该用户都登录了哪些应用(例如系统A、系统B、系统C)。然后,SSO服务器会从后端向所有这些应用的预设登出接口发送登出通知 (Logout Notification)。
- 这个通知通常是一个包含了
logout_token
的HTTP POST请求,应用收到后需要验证这个token
的合法性。
- 这个通知通常是一个包含了
其他应用清理本地会话: 系统 B、系统 C 等其他应用收到来自 SSO 的登出通知后,执行与步骤2相同的本地登出操作,清理各自的会话和 Cookie。
最终跳转: 在 SSO 服务器完成所有登出操作后,它会将用户的浏览器重定向到第3步中指定的
post_logout_redirect_uri
地址,通常是应用的登录页面。用户此时会看到系统 A 的登录页,表示已成功登出。
至此,用户在整个 SSO 体系中的所有会话均已失效,实现了彻底的“单点登出”。
1 |
|
OAUTH2
基本角色定义
授权服务器 - 验证身份并颁发令牌的服务器(我上面代码示例中的 OAuth 服务器)
资源服务器 - 提供 API 资源的服务器
客户端应用 - 访问 API 的应用,可以是:
第一方应用 - 您公司自己的应用
第二方应用 - 业务合作伙伴的应用
第三方应用 - 外部开发者的应用
token 类型
access_token (默认有效期 24 小时)
refresh_token(默认有效期 30 自然日,每次刷新 access_token 的操作可自动刷新 refresh_token 有效期的起始计算时间)。
客户端凭证流程(适用于服务器到服务器通信)
1 |
|
这种流程中:
- 获取令牌方: 第一方或第二方应用直接获取令牌
- 使用令牌方: 同样的应用使用令牌调用API
- 无用户参与: 整个过程无需最终用户授权
1 |
|
授权码流程(适用于有用户参与的场景)
1 |
|
这里面有两次重定向:
第一次:重定向到授权服务器。用户是带着一个 param 去访问 authserver 的,在我们常见的场景里线索通跳到 sso 是第一次重定向,参数里带有 https://app.example.com/callback 就是授权完以后再回到线索通的一个 url。
当用户尝试访问需要授权的功能时,应用生成一个授权请求 URL
这个 URL 包含 redirect_uri 参数(应用事先在授权服务器注册的 URL)
应用通过 HTTP 302重定向或者前端跳转,将用户浏览器导向授权服务器
1 |
|
第二次:重定向回应用,携带授权码(code)。
用户在授权服务器完成身份验证并授予权限后
授权服务器使用之前请求中的redirect_uri,将用户浏览器重定向回应用
重定向 URL 中附加授权码(code)作为查询参数
1 |
|
- 授权服务器只会重定向到预先注册的 URI,防止授权码被重定向到恶意网站
- 防止授权码被重定向到恶意网站
1 |
|
刷新令牌流程 (Refresh Token Flow)
1 |
|