news 2026/10/26 9:14:46

C# 单点登录 SSO 实战:票据中心、Redis 存储与全局登出

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
C# 单点登录 SSO 实战:票据中心、Redis 存储与全局登出

简介:C#单点登录示例程序SSO面向具备一定ASP.NET基础的开发者及身份认证方向的学习者,聚焦多个子站点之间共享登录状态的实际需求,提供一套可运行、可拆解的SSO实现参考。压缩包共173个文件,约2.07MB,以64个cs源码和16个aspx页面为主体,辅以dll程序集、csproj项目文件、config配置、SQL数据库文件及docx说明文档,能够较完整地呈现项目结构、依赖关系与调试环境。目前已有495人学习,适合用于课程设计、毕业设计或企业认证模块的原型验证。源码围绕中央认证服务展开,涉及ASP.NET Identity账户管理、JWT/OAuth2令牌生成与校验、子站点接入、HTTP重定向与Cookie传递,并覆盖HTTPS通信、令牌过期与重放防护、单元测试和集成测试等要点。读者可据此梳理登录跳转、令牌验证与多站点会话共享的完整链路,理解面向接口和依赖注入在SSO中的落地方式,并积累身份认证模块的排错思路。

1. 三套系统三套账号,先别急着敲代码

某公司内部有三个系统:订单、报表、工单。每个系统各有一套账号体系,员工每天上班先登录三次,改密码要改三次,离职销号经常漏掉一个。这类需求落到技术侧,就是一套 C# 单点登录(SSO)方案要回答的问题:一次登录,全部系统通行;一处登出,全部系统失效;新增系统接入只改配置、不改认证中心。这份 C# 单点登录示例程序把认证中心、票据签发与校验、业务系统接入中间件、登出广播拆成几个能独立跑起来的工程,主站和两个子系统可以在一台机器上直接启动联调。它适合两类人:正在做毕设或课设、需要交一套真能演示“单点”效果的同学;以及要给内部老系统补统一登录、又不想引入重型认证框架的从业者。

2. 需求拆解与技术选型:四条硬指标和三条 SSO 路线的取舍

在动手写认证中心之前,我一般会先把“单点登录”这个模糊的词拆成能写进验收单的指标。拆不出来,后面架构怎么画都是空的,答辩时评委问一句“你怎么证明这是单点而不是免密登录”,就容易卡住。

2.1 把“单点登录”翻译成四条可验收的硬指标

第一条是一次登录。用户在任意一个子系统完成认证后,访问其余子系统不再输入密码,整个跳转链路里的 302 重定向不超过两次。这个“不超过两次”不是拍脑袋,而是用户体验的底线:每多一次跳转,页面就多一次白屏,还会带来 Referer 泄露和回跳参数丢失的风险。

第二条是一次登出。用户在任意子系统点击退出,认证中心和所有已登录子系统在 5 秒内全部失效,再访问任意系统都要重新认证。很多示例程序只做了“登出当前站”,用户退回浏览器上一页还能继续操作,这在验收时是硬伤。

第三条是票据不可重放。回跳 URL 上携带的服务票据必须一次性消费,同一个票据被第二次提交时,认证中心要明确拒绝并记录日志。这一条决定了方案能不能叫“安全”,而不仅仅是“方便”。

第四条是接入成本可控。新增一个子系统,只需要在认证中心登记 appId、密钥和回跳白名单,业务代码里加一段中间件注册和一个配置节,不修改认证中心任何一行代码。这条指标直接决定了这套东西是“演示 Demo”还是“能交给别人用的组件”。

四条指标立住之后,后面的选型就不是凭喜好了。哪条路线能同时满足跨域、能登出、能防重放、接入便宜,就选哪条。

2.2 三条 SSO 路线的对比:共享 Cookie、会话共享、票据中心

在实际项目里能见到的实现基本就这三类,我把它们放在同一张表里对比,选型时对着看比翻文档快。

对比项共享 Cookie会话(Session)共享票据中心(本方案)
基本原理主域写 Cookie,子域自动携带各站点 Session 存同一个 Redis认证中心签发票据,业务后端校验
跨主域能力无,域名不同直接失效弱,依赖统一的 Session 结构强,跨域跨语言都能接
单点登出删 Cookie 即可,但各站本地缓存难清删 Session 即可,各站无感知逻辑广播通知,各站主动清本地会话
防重放无票据概念,谈不上重放无票据概念一次性票据 + 时间窗 + 签名
异构客户端差,桌面端拿不到浏览器 Cookie差好,桌面端走回环端口取票据
实现复杂度低中中偏高,但边界清晰

共享 Cookie 是最省事的做法,把 Cookie 的 Domain 设成.corp.internal这种主域,所有子域共用一份认证 Cookie。它的问题在于“域”这个边界太硬:一旦有系统部署在完全不同的域名下,或者要给桌面客户端用,方案立刻失效。而且它本质上没有票据,登出只能靠删 Cookie,各站内存里的用户对象照样活着。

会话共享的问题在耦合。把 Session 塞进 Redis 让各站共用,听起来巧妙,实际上要求所有站点用同一套 Session 序列化结构、同一个框架版本。一旦有个老系统是 .NET Framework 写的,序列化格式对不上,整条链路就断了。

票据中心走的是另一条路:认证中心只负责“证明你是谁”,业务系统各自维护自己的本地会话,两边靠一张一次性票据对接。耦合点被压缩到一个 HTTP 接口和一份签名算法上,跨语言、跨框架都不是问题。这份示例程序选的就是这条路线,认证中心用 ASP.NET Core Web API 实现,票据存储用 Redis,业务侧提供一个可复用的中间件类库。

2.3 解决方案分层:三个工程各管一摊

示例程序按职责切成四个项目,刻意让“认证”和“接入”在物理上分开,避免有人图省事把认证逻辑直接写进业务系统。

SsoSolution/ ├── Sso.Contracts/ 共享契约:票据模型、签名工具、异常码 ├── Sso.AuthCenter/ 认证中心:登录页、票据签发、校验、登出广播 ├── Sso.Client/ 接入类库:中间件、本地会话存储、回跳处理 └── Sso.DemoApps/ 演示子系统:OrderSystem、ReportSystem 两个站点

Sso.Contracts是最薄的一层,只放 DTO 和 HMAC 签名实现,认证中心和客户端都要引用它,所以它不能依赖任何 Web 相关包,否则客户端引用进来会拖一堆东西。Sso.AuthCenter是唯一持有用户密码和应用密钥的地方,它对内提供四个接口:授权跳转、票据签发、票据校验、全局登出。Sso.Client是给业务系统引用的,核心是一个中间件,业务方在Program.cs里app.UseSsoAuthentication()一行接进来。Sso.DemoApps只是两个最小可运行的站点,用来验证“同一浏览器登录一次,两边都放行”。

这个分层的价值在于边界清晰:认证中心的任何改动都不会波及业务系统的编译,业务系统升级也不用重新发布认证中心。带过完整项目的人都知道,这种“物理隔离”比写在文档里的规范管用得多。

3. 认证中心的设计与实现:双票据模型、Redis 存储与登出广播

认证中心是整个方案中唯一需要特别小心处理的部分,它一旦出问题,所有子系统一起失效。设计上我把票据分成两层,不是故意复杂化,而是被 URL 的天然属性逼出来的。

3.1 为什么是两层票据:TGT 与 ST 的分工

回跳 URL 上携带的东西,会进入浏览器历史记录、进入服务器访问日志、还可能通过 Referer 头泄露给第三方资源。所以 URL 上绝对不能放“能长期代表用户身份”的凭证。反过来,如果什么都不放,业务系统每次请求都要回认证中心问一次,认证中心就会变成全站单点瓶颈。

两层票据就是在这个矛盾里找的平衡点。第一层是 TGT(Ticket Granting Ticket,全局票据),它代表“这个浏览器已经登录过”,存放在 HttpOnly + Secure 的 Cookie 里,有效期 30 分钟且带滑动续期,前端脚本读不到。第二层是 ST(Service Ticket,服务票据),它只在一次回跳中有效,有效期 60 秒、用完即删、且绑定目标 appId。ST 泄露了也没关系,攻击者拿到的时候它多半已经过期,或者已经被正常流程消费掉了。

Redis 里的键设计如下,几个键的职责和生命周期各不相同:

Key 模式类型值内容TTL作用
sso:tgt:{tgtId}String用户信息 JSON30 分钟滑动全局会话,登出的判断依据
sso:st:{stId}StringtgtId|appId60 秒一次性服务票据
sso:user:{userId}:ticketsSettgtId 集合与 TGT 同按用户批量清会话,支撑全局登出
sso:app:{appId}Hash密钥、回跳白名单永久应用注册表,接入时只改这里
sso:local:{sid}String用户信息 JSON30 分钟业务系统本地会话(由客户端写)

最后一行虽然归客户端用,但键的命名规则由认证中心统一约定,避免各子系统各起各的名字,运维排查时找不到。

3.2 票据签发与一次性消费的代码实现

票据模型先定下来,字段不多但每个都有用:

// Sso.Contracts/TicketModels.cs public sealed class SsoTicket { public string TicketId { get; set; } = ""; // GUID("N"),Redis 键的一部分 public string UserId { get; set; } = ""; public string UserName { get; set; } = ""; public string AppId { get; set; } = ""; // 票据归属应用,防跨应用使用 public long IssuedAt { get; set; } // Unix 秒,用于排查超时问题 public long ExpiresAt{ get; set; } public string Signature{ get; set; } = ""; // HMAC-SHA256(appId|ticketId|issuedAt) }

签发的核心逻辑在TicketService里,TGT 写入 Redis 并同步维护“用户 → 票据”的索引集合,这一步是为登出准备的:

public async Task<string> IssueTgtAsync(UserInfo user) { var tgtId = Guid.NewGuid().ToString("N"); // 30 分钟有效期,业务侧每次命中本地会话时可调用 Refresh 续期 await _redis.StringSetAsync(TgtPrefix + tgtId, JsonSerializer.Serialize(user), TimeSpan.FromMinutes(30)); // 索引集合:全局登出时按 userId 一次取出所有 TGT await _redis.SetAddAsync($"sso:user:{user.UserId}:tickets", tgtId); return tgtId; } public async Task<string> IssueStAsync(string tgtId, string appId) { var raw = await _redis.StringGetAsync(TgtPrefix + tgtId); if (raw.IsNullOrEmpty) throw new SsoException("TGT_EXPIRED"); var stId = Guid.NewGuid().ToString("N"); // ST 只活 60 秒,值里绑定 tgtId 与 appId,防止票据被挪用到别的系统 await _redis.StringSetAsync(StPrefix + stId, $"{tgtId}|{appId}", TimeSpan.FromSeconds(60)); return stId; }

校验接口是整个方案里防守最严的地方,顺序不能乱:先验时间窗,再原子消费票据,最后验签。把验签放在消费之后,是为了让重放攻击连一次签名计算的机会都没有:

public async Task<UserInfo> ConsumeStAsync(string stId, string appId, long timestamp, string sign) { // 1. 时间窗:允许 ±300 秒,超出直接拒绝,挡住隔天重放的旧请求 if (Math.Abs(DateTimeOffset.UtcNow.ToUnixTimeSeconds() - timestamp) > 300) throw new SsoException("CLOCK_SKEW"); // 2. 一次性消费:StringGetDelete 原子取值并删除,并发请求里只有一个能拿到值 var value = await _redis.StringGetDeleteAsync(StPrefix + stId); if (value.IsNullOrEmpty) throw new SsoException("ST_INVALID_OR_USED"); var parts = value.ToString().Split('|'); if (parts.Length != 2 || parts[1] != appId) throw new SsoException("APP_MISMATCH"); // 3. 验签:参与签名的字段必须与调用方完全一致,多一个空格都算失败 if (!SignatureHelper.Verify($"{stId}|{appId}|{timestamp}", sign, GetSecret(appId))) throw new SsoException("SIGN_INVALID"); // 4. 回读 TGT:这中间用户可能已经在别处登出,TGT 已被删除 var tgt = await _redis.StringGetAsync(TgtPrefix + parts[0]); if (tgt.IsNullOrEmpty) throw new SsoException("SESSION_LOGOUT"); return JsonSerializer.Deserialize<UserInfo>(tgt)!; }

第 2 步用的StringGetDelete对 Redis 版本有要求,6.2 及以上才支持原子 GETDEL。如果部署环境版本偏低,就退化成 Lua 脚本或者先StringGet再KeyDelete的两步写法,但两步写法在并发下有极小的窗口能重复消费,演示可以接受,真要上线就老老实实升级 Redis。这个坑我在联调里踩过一次,两个浏览器标签同时回跳,两边都登录成功了,日志里同一个 ST 出现了两次。

3.3 全局登出与失效广播

登出动作要完成三件事:删 TGT、删该用户所有 TGT、通知所有子系统清本地会话。前两件在 Redis 里一次搞定,第三件靠 Pub/Sub:认证中心往sso:logout频道发一条userId|tgtId|timestamp的消息,各业务系统的后台订阅者收到后,按 userId 清掉自己本地的会话记录。

// Sso.AuthCenter:登出接口内部 public async Task LogoutAsync(string userId) { var tickets = await _redis.SetMembersAsync($"sso:user:{userId}:tickets"); foreach (var t in tickets) await _redis.KeyDeleteAsync(TgtPrefix + t); await _redis.KeyDeleteAsync($"sso:user:{userId}:tickets"); // 广播出去,订阅方各自清理本地会话,5 秒内传播完成 await _subscriber.PublishAsync("sso:logout", $"{userId}|{DateTimeOffset.UtcNow.ToUnixTimeSeconds()}"); }

注意:Pub/Sub 是“发出去就不管”的模型,订阅方在消息发出时如果正好重启,这条消息就丢了。更稳的做法是额外维护一个短期失效列表sso:revoked:{userId},客户端在下次校验本地会话时顺带查一次,作为广播的补救。示例程序里留了这个扩展点,默认没开。

4. 业务系统接入:一个中间件接住三种客户端形态

认证中心再漂亮,业务侧接入要改几十行代码,也没人愿意用。所以Sso.Client的设计目标是:业务系统只提供一个配置节,加一行中间件注册,剩下的都不管。

4.1 中间件的拦截顺序:本地会话优先,票据兜底

中间件在每个请求进来时按固定顺序判断:先看本地会话有没有,有就放行;没有再看请求里带没带 ST,带了就校验并建本地会话;两样都没有,302 到认证中心换票据。顺序反了会出现“每次请求都往认证中心跑”的性能灾难。

public sealed class SsoAuthMiddleware { private readonly RequestDelegate _next; public SsoAuthMiddleware(RequestDelegate next) => _next = next; public async Task InvokeAsync(HttpContext ctx, ISsoClient client) { // 回跳与登出接口本身不拦截,否则自己就把自己拦成死循环 var path = ctx.Request.Path.Value ?? ""; if (path.StartsWith("/sso/callback") || path.StartsWith("/sso/logout")) { await _next(ctx); return; } // 1. 本地会话命中,直接放行,这一步承担 99% 的请求量 var session = await client.GetLocalSessionAsync(ctx); if (session != null) { ctx.User = session.ToPrincipal(); await _next(ctx); return; } // 2. 拿到 ST 说明刚从认证中心跳回来,校验后建本地会话 var st = ctx.Request.Query["st"].ToString(); if (!string.IsNullOrEmpty(st)) { var user = await client.ValidateStAsync(st, ctx); if (user != null) { await client.CreateLocalSessionAsync(ctx, user); // 写 Cookie + Redis ctx.Response.Redirect(client.SafeReturnUrl(ctx) ?? "/"); return; } } // 3. 都没有,去认证中心,returnUrl 带上当前完整路径 ctx.Response.Redirect(client.BuildAuthorizeUrl(ctx.Request.Path + ctx.Request.QueryString)); } }

配置节的结构如下,Mode字段决定走共享 Cookie 还是票据回跳,两种模式共用同一份中间件代码:

{ "Sso": { "AuthCenter": "https://sso.auth.internal", "AppId": "order-system", "AppSecret": "开发环境占位值,生产从环境变量注入", "Mode": "TicketRedirect", "ReturnUrl": "http://order.local/sso/callback", "SessionMinutes": 30, "AllowedReturnHosts": [ "order.local", "report.local" ] } }

AppSecret绝对不能出现在前端能拿到的任何地方,它只参与服务端到服务端的验签。示例程序在开发环境把它写在appsettings.Development.json里方便跑通,配置文件顶部用注释标了“生产必须走环境变量或密钥管理服务”,这条注释别删,删了就一定有人在生产抄这份配置。

4.2 两种接入模式:同域共享与跨域回跳

同一主域下的子系统(order.local和report.local)其实不需要 ST,直接把认证 Cookie 的 Domain 设成.local就能共享,Mode配成SharedCookie,中间件只做本地会话转换,一跳都不用。这种模式性能最好,但只在同一主域、同一浏览器环境下成立。

跨主域、或者要接桌面端的时候,就必须用Mode: TicketRedirect,走完整的“跳过去 → 拿 ST → 跳回来 → 校验”链路。两种模式共用同一份中间件,切换只改一个配置项,这是刻意的设计:让同一个团队在开发环境用简单模式快速跑通,上线时按部署拓扑切换。

4.3 桌面端的接入:回环端口方案

WinForm 或 WPF 客户端拿不到浏览器 Cookie,最省事的做法是“借浏览器完成登录,再从回环端口把票据捞回来”:桌面程序启动时在本机随机端口起一个临时 HTTP 监听,然后打开系统浏览器访问认证中心的授权地址,把redirect_uri指向http://127.0.0.1:{port}/callback。用户在浏览器里完成登录后,认证中心回跳,桌面端从监听里拿到 ST,再调用校验接口换成本地 Token。

// 桌面端:启动临时监听,等待回跳携带的 st using var listener = new HttpListener(); listener.Prefixes.Add($"http://127.0.0.1:{port}/callback/"); listener.Start(); Process.Start(new ProcessStartInfo(authorizeUrl) { UseShellExecute = true }); var context = await listener.GetContextAsync(); // 阻塞等待回跳 var st = context.Request.QueryString["st"]; // 立刻返回一个“登录成功,请关闭本页”的静态页,避免用户以为卡住 var body = Encoding.UTF8.GetBytes("<h3>登录完成,可以关闭此页面</h3>"); context.Response.OutputStream.Write(body); listener.Stop(); var token = await ssoClient.ExchangeAsync(st); // 换本地 Token

这套方案的关键点是:监听只在登录窗口期存在,拿到票据立刻关闭;端口随机分配,避免冲突;回跳地址在认证中心的白名单里按127.0.0.1前缀放行,而不是全域名放开。如果不做这三件事,桌面端的回环端口就是一个可以被本机任意进程抢占的开放入口。

5. 避坑与排查:SSO 联调里翻车最多的五个点

SSO 的坑几乎都不在代码写错,而在“协议层和浏览器行为的组合”。下面五条是按项目里出现频率排序的,写在验收前看一遍,能省掉大半天。

5.1 现象:登录后瞬间被踢回登录页,来回死循环

现象很直观,浏览器地址栏在认证中心和业务系统之间反复跳,最后报“重定向次数过多”。

原因通常有两个。一是returnUrl参数被 URL 编码了两次,认证中心解出来是%2Fhome而不是/home,白名单校验失败,被迫退回登录页。二是回跳地址带了尾部的查询串,白名单只匹配了域名前缀,匹配不上就丢弃。

解决办法是把编码集中到一处处理,中间件里统一用Uri.EscapeDataString编码一次,认证中心统一用Uri.UnescapeDataString解码一次,禁止在上下游各编一次。白名单匹配改成解析Uri后比对 Host 和白名单列表,而不是粗暴的字符串StartsWith:

private static readonly string[] AllowedHosts = { "order.local", "report.local", "ticket.local" }; public static bool IsSafeReturnUrl(string url) { if (string.IsNullOrWhiteSpace(url)) return false; if (!Uri.TryCreate(url, UriKind.RelativeOrAbsolute, out var uri)) return false; if (!uri.IsAbsoluteUri) return url.StartsWith("/") && !url.StartsWith("//"); // 挡掉 //evil.com return AllowedHosts.Contains(uri.Host, StringComparer.OrdinalIgnoreCase); }

//evil.com这种写法绕过了StartsWith("/")的判断,浏览器会把它当成协议相对 URL 跳到外部站点,是开放重定向的经典入口,必须单独挡。

5.2 现象:主站退了,子系统还活着

用户在订单系统点退出,回到报表系统刷新一下,居然还在,用户当场就认为“这套单点登录是假的”。

原因是登出只删了认证中心的 TGT 和当前站的本地会话,其他子系统的本地会话还在 Redis 里躺着,它们的中间件只看本地会话,压根没去问认证中心。

解决办法靠 3.3 里的广播:认证中心发sso:logout,各子系统订阅后按 userId 批量删本地会话。同时把本地会话的 TTL 压到比 TGT 短(比如本地 25 分钟、TGT 30 分钟),这样即使广播丢了,最坏情况下 25 分钟后也会自然失效。验收时专门测这一条:登录三个站,在其中任意一个退出,然后在另外两个各刷新一次,必须全部跳到登录页。

5.3 现象:跨域子系统拿不到 Cookie,登录状态丢失

同域模式下一切正常,换成跨主域部署后,用户在订单系统登录成功,跳到报表系统又是未登录。

原因在 Cookie 的作用域和SameSite属性上。Domain 写成了具体的order.local而不是主域,或者SameSite=Strict导致顶层导航回跳时浏览器根本不发送 Cookie。跨站跳转属于“顶层导航”,Lax才放行 GET 请求携带 Cookie,Strict会拦掉。

解决办法是把认证 Cookie 的 Domain 设为共享主域、SameSite=Lax、Secure=true、HttpOnly=true,并且确认回跳链路是 GET 而不是 POST。如果业务必须 POST 回跳,就干脆放弃共享 Cookie 模式,改用票据回跳,别在SameSite上硬凑。

5.4 现象:同一个 ST 被用了两次,两边都登录成功

日志里出现两条相同的 ST 消费记录,两边的用户信息还都是一致的,功能上没报错,安全上已经失守。

原因是校验接口先StringGet再KeyDelete,两个并发请求同时读到了值,都通过了校验。这种问题在单线程压测下永远复现不了,只在多标签页、多设备同时回跳时冒出来。

解决办法是把消费做成原子操作,用StringGetDelete(Redis 6.2+)或 Lua 脚本,一次完成取值和删除。上线前专门写一个并发用例:同一个 ST 用两个线程同时请求校验接口,断言只有一个返回 200,另一个必须返回ST_INVALID_OR_USED。

5.5 现象:验签偶尔失败,重启后自己又好了

同一份配置,早上跑得好好的,下午开始间歇性报SIGN_INVALID,重启一下就恢复,隔天再犯。

原因是两台服务器的系统时间不同步,时间戳校验的 ±300 秒窗口被击穿。这种问题排查起来很玄学,因为报错是随机的,日志里也看不出时间差。解决办法是给时间戳校验失败单独打一条日志,把本机时间和请求时间都打出来,一眼就能看出偏移多少。

var skew = Math.Abs(now - timestamp); if (skew > 300) { _logger.LogWarning("票据时间窗超限 stId={StId} 偏移={Skew}s 本机={Now} 请求={Ts}", stId, skew, now, timestamp); throw new SsoException("CLOCK_SKEW"); }

同时把服务器时间同步纳入部署检查清单,别等报错才想起来。血泪经验是:这类问题不解决,联调期间团队会互相怀疑对方代码有问题,浪费的时间远超修它的成本。

6. 验收与答辩:用一张表和三个问题证明它真的“单点”

验收别靠“我点了下感觉是对的”,照着表逐条过,过不了的当场记下来。下面这张表是这套示例程序自带的验收清单,六项全绿才算交付。

验收项操作步骤期望结果
单点登录在订单系统登录,直接访问报表系统首页不出现登录页,直接进入
单点登出三个站各登录一次,在报表系统退出,刷新另两站5 秒内全部跳回登录页
票据一次性抓一次回跳 URL,重放其中的st参数第二次返回ST_INVALID_OR_USED
票据过期篡改请求时间戳为 10 分钟前返回CLOCK_SKEW
回跳白名单把returnUrl改成外部域名被拒绝,停留在登录页
未登录拦截清空浏览器 Cookie 直接访问受保护页面302 到认证中心,登录后回到原页面

答辩时被问最多的三个问题,提前准备好答案比临场编强。第一个是“为什么不用共享 Cookie 一把梭”,答法是从跨域和桌面端两个场景切入,说明共享 Cookie 在这两种情况下直接失效,而不是说它不好。第二个是“票据被中间人截获怎么办”,答法是 ST 一次性加 60 秒有效期,截获时大概率已被消费或过期,同时说明生产环境全链路 HTTPS 是前提。第三个是“新增一个系统要改什么”,现场打开sso:app:{appId}这个 Hash 的写入代码,指给对方看只有三行:appId、密钥、回跳白名单,其他一律不动。

答辩演示的顺序也有讲究:先演示最顺利的单点登录,建立信心;再演示单点登出,这是最能体现设计深度的一步;最后现场跑一次票据重放,用命令行工具把同一个st提交两次,第二次报错弹出来的瞬间,比讲十分钟原理都管用。演示前把三个站点的浏览器标签按顺序排好,别在台上现找地址。

从那以后我每次交付认证类的东西,都会在演示脚本里强制走一遍“登出后刷新另外两个站”这一步,因为这是唯一一个能同时暴露广播、本地会话、TTL 三处问题的动作,跳过它,问题一定留给用户去发现。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/26 9:14:03

微感知健康协作者:面向知识工作者的可解释数字健康工具

1. 项目概述&#xff1a;这不是一个“监控软件”&#xff0c;而是一套面向职场人的微感知健康协作者TrustmeWatcher 这个名字乍一听容易让人联想到某种后台监听工具——毕竟“Watcher”这个词自带 surveillance 的冷感&#xff0c;加上“Trustme”又像一句略带反讽的安抚。但实…

作者头像 李华
网站建设 2026/10/26 9:13:28

CodeWave低代码平台:智能体开发实战与优化

1. CodeWave智能体开发平台概述 SiliconCloud推出的CodeWave是当前智能体开发领域颇具创新性的低代码平台。作为一名经历过从传统编程到智能体开发转型的技术从业者&#xff0c;我亲身体验到这套工具如何将自然语言理解、决策逻辑构建和API集成等复杂过程简化为可视化操作。不同…

作者头像 李华
网站建设 2026/10/26 9:12:56

车辆辅助驾驶系统中的路面分析与交通识别三阶设计

简介&#xff1a;本资源是一套面向高校计算机相关专业学生的高分车辆辅助驾驶系统实践项目&#xff0c;聚焦计算机视觉、路面分析与交通路况识别三大核心技术&#xff0c;适用于人工智能、自动化、电子信息等方向的课程设计、毕业设计及项目入门学习。压缩包共110个文件&#x…

作者头像 李华
网站建设 2026/10/26 9:12:34

Unity地形优化:Terrain To Mesh插件实战指南

1. 项目概述&#xff1a;Terrain To Mesh插件核心价值 作为一名在Unity地形开发领域摸爬滚打多年的老手&#xff0c;我深刻理解传统地形系统&#xff08;Unity Terrain&#xff09;在性能优化和特效整合上的痛点。Terrain To Mesh插件正是为解决这些问题而生的利器——它能将Un…

作者头像 李华
网站建设 2026/10/26 9:12:25

t3code:用T3 Stack构建类型安全的全栈Web应用模板

“t3code”这个名字&#xff0c;第一次看到的人多半以为是个编码竞赛题库或者某个刷题网站的代号。实际上&#xff0c;它是我维护了大半年的一套全栈Web应用启动模板&#xff1a;基于T3 Stack&#xff0c;把Next.js、TypeScript、TailwindCSS、tRPC、Prisma和NextAuth这六样东西…

作者头像 李华
网站建设 2026/10/26 9:11:35

pstack-claude:专为Claude/Codex服务设计的Python堆栈快照工具

1. 项目概述&#xff1a;pstack-claude 是什么&#xff0c;它解决的是哪类开发者的真实痛点&#xff1f;pstack-claude 这个名字乍看像一个工具组合词&#xff0c;但拆开来看&#xff0c;它其实指向一个非常具体、高频且令人抓狂的开发场景&#xff1a;在本地调试或分析运行中的…

作者头像 李华