搞定企业级SSO单点登录:OAuth2、OIDC与SAML真实排障血泪史
OAuth 2.0:你以为是登录,其实就是借张房卡
OAuth 2.0的本质就是“授权”。这就好比你去住酒店,你去前台拿了一张房卡,这张房卡能刷开你的房间门,也能刷开酒店健身房的门。对于房间门那个电子锁来说,它根本就不关心你是张三还是李四,它也不查你的身份证,它只认房卡。这张房卡,就是OAuth2里面说的 access_token。
在这个协议里,怎么把这个 token 安全地发给客户端是个大麻烦。我们在后台配置的时候,肯定得配 client_id 和 client_secret,这就相当于你在认证服务器那边注册的账号和密码。密码这东西肯定是打死不能泄露的。这里面那个叫 redirect_uri 的回调地址简直就是事故重灾区。
你知道我在这个回调地址上浪费了多少个晚上吗?正常流程是浏览器跳到认证中心,用户输完账号密码同意授权,认证中心把浏览器再踢回到你指定的这个 redirect_uri,顺便在URL参数上带个 code。结果呢,应用后台配置的是 https://api.mycompany.com/callback,前端发请求跳过去的时候,带的参数是 http://api...,就差了一个s。有时候更扯,域名的结尾多了一个斜杠 /。认证中心那边极其死板,稍微有点不一样直接给你报个 invalid_redirect_uri。遇到这种问题根本不用瞎猜,把两边配的字符串拿出来一个个字母对,绝对有对不上的地方。
拿到 code 之后,应用后端得拿着这个 code 和 client_secret 去换 access_token。其实吧,我就见过一个特别奔放的前端开发,为了图省事,他直接在浏览器的JS代码里发起请求去拿 code 换 token。这等于是把 client_secret 这个只有后端该拿的密钥直接明文写在前端打包文件里了。要是被人扒出来,直接就能伪造请求去拿数据了。换 token 这步操作必须得在后端服务器的内网里偷偷完成。
那个 state 参数也挺坑人的。它本来是用来防 CSRF 跨站伪造请求攻击的,浏览器跳出去之前在本地存一下这个随机串,等跳回来的时候对比一下看看是不是同一次请求。结果现在的系统动不动就搞多节点集群部署,A节点生成的 state 存在它自己的本地内存里。用户从认证中心跳回来的时候,负载均衡一个不小心把流量打到了B节点。B节点一查,我这内存里没这个 state 记录啊,咔嚓一下直接报错拦截。这事儿查起来的时候特别让人抓狂,因为它是偶发的,用户多刷新几次可能又瞎猫碰到死耗子登进去了。没办法,只能让他们老老实实上 Redis 把这个会话状态做个集中共享。
OIDC:带照片的房卡,和那些让人抓狂的玄学故障
其实光拿张没名字的房卡是不够用的。业务系统得知道登录的这人到底是谁啊,总不能随便放个人进来就让他查数据库,权限控制都没法做。为了能标准化地获取用户信息,OIDC(OpenID Connect)就出来了。
OIDC 说白了就是在 OAuth 2.0 头上盖了个帽子。你只要在系统的 SSO 配置里看到有 .well-known/openid-configuration 这个东西,或者请求授权的链接参数里带着 scope=openid,那不用怀疑,它百分之百就是 OIDC。它最大的不同就是,后端拿 code 去换 token 的时候,除了拿到那个开门用的 access_token,还会额外附赠一个叫 id_token 的东西。
这个 id_token 通常是个 JWT(JSON Web Token)格式的长字符串,中间用点号隔开分成三段。这就好比现在的房卡不仅能开门,上面还印了你的照片、身份证号和过期时间。你要是好奇这串乱码是啥,找个 jwt.io 网站往里一粘,立马就能看到明文的 JSON,里面写着用户的邮箱、名字啥的。
看着挺美对吧?但这个 OIDC 排错的时候能把人折腾死。你拿到 id_token 之后不能直接就信了里面的内容,还得验签。那个 .well-known 的服务发现接口里,会提供一个 jwks_uri 端点。应用得去这个地址拉取一把公钥,用公钥来算一遍签名,看看这个 id_token 到底是不是认证中心发的,别是哪个黑客自己伪造的。
说到这我想起一个极其玄学的故障,业界俗称时钟漂移(Clock Skew)。某天半夜有个新上线的内部管理系统死活登不进去,点登录就陷入了无限重定向的死循环。抓包看网络通畅,token 也正常发过来了,但是应用拿过来一解析就报错,死活说 token 已经过期。当时我们查了半天代码逻辑,没有任何毛病。一点点扒到底层环境才发现,跑这个应用的虚机,它的系统时间比标准时间慢了两分多钟!
认证中心按它的当前时间签发了一个有效期只有五分钟的 token。应用这边的时间还停留在两分钟前,它拿到 token 解析出里面的 nbf(未生效时间)或者 exp(过期时间)字段,一对比自己当前的时间,觉得不对劲啊,这 token 的生效时间还没到呢。机器的脑回路比较直,直接给当成废票扔了,然后又重新发起登录请求,继续死循环。后来把服务器的 NTP 时间同步服务给强制拉平了一下,服务瞬间就恢复了。这坑你要是没踩过,盯着代码看一天都找不出毛病在哪。
网关配置也是个大坑。现在应用基本都跑在 K8s 里了,前面挡着个 Ingress,HTTPS 证书都在 Ingress 那一层被卸载了,进到集群里的流量全是 HTTP 打到后面的 Pod。应用代码一识别自己的请求上下文,哟,我现在是 HTTP 协议,它自己拼接 redirect_uri 的时候就老老实实用 http://。但是外面的认证中心要求必须是 HTTPS 才安全,两边协议对不上直接拒接。解决这事儿得在网关层面动手脚,把 X-Forwarded-Proto: https 这个头硬塞进去传过去,让后面的应用知道自己其实是在被 HTTPS 代理着。
SAML 2.0:满屏的XML,政企对接绕不开的巨无霸
那些老牌的政企客户可不认这套轻量级 JSON 的东西,他们以前做系统的时候就喜欢用 XML,觉得那玩意严谨。如果你跟他们对接,或者对接一些有点年头的企业级 SaaS 软件,你绝对逃不过 SAML 2.0。
SAML 2.0 简直就是个庞然大物。这玩意儿连个像样的动态发现接口都没有,两边交换配置全靠一个叫 Metadata 的极其庞大的 XML 文件。你得把你的文件发邮件给我,我把我的发给你,有时候还得互相把 XML 里的多余换行符删掉才能导进去。这堆密密麻麻的标签里,核心看俩参数就行。一个是 EntityID,也就是大家给自己起的全局唯一名字,互相得认对人,别张冠李戴了;另一个是 ACS URL,全称叫 Assertion Consumer Service。其实就相当于 OAuth 里的回调地址,就是认证中心把认证结果返回给你的应用的那个接口地址。
这套流程走起来挺神奇的。用户要登录,应用自己在那吭哧吭哧组装一个一大坨的 XML 请求(AuthnRequest),这玩意儿还得压缩再 base64 编码一下塞到 URL 里,特别长。浏览器带着这坨乱码跳到认证中心。用户登完录,认证中心再生成一个更大的 XML 文件,这就是著名的 Assertion(断言),里面塞满了用户信息,然后用自己的私钥给整个 XML 签好名。
接着认证中心会返回一个极其鸡贼的隐藏 HTML 表单,里面有段 JS 代码让浏览器自动触发 Submit(就是页面上闪过一句“正在跳转请稍候”的时候),带着这坨巨无霸 XML,发起一个 POST 请求狠狠地砸向你应用的 ACS URL。在这个过程里,浏览器纯粹就是个没有感情的快递员。
排查 SAML 的问题,千万别盯着自己后台那一两行不知所云的报错日志瞎猜。直接打开浏览器的开发者工具,按F12切到 Network 标签页,别嫌麻烦。找到那个打向你应用 ACS URL 的 POST 请求,把 Payload 里的 SAMLResponse 参数一整段复制出来。这时候它肯定是一堆 Base64 的乱码,随便找个在线的 Base64 解码工具把它解开,变成明文的 XML。答案全在这个明文里写着呢,里面的 StatusCode 节点如果不是 Success,底下的文字描述通常把错在哪写得明明白白的。我都习惯装个叫 SAML Tracer 的浏览器插件,它能自动把这些给解析出来,省得我复制粘贴去解码。
最要命的就是证书过期。SAML 这套机制全靠非对称加密的签名来维持微弱的信任关系。认证中心那边会有一套私钥用来签名,公钥就放在那个 Metadata XML 里提供给应用去验证。这个证书可不是永久的,通常就一年或者三年有效期。IdP(身份提供商)那边的运维悄悄把证书更了,但压根没通知你。应用这边也没有做什么高级的自动同步机制,还死死抱着存在本地的老证书在验签。那肯定是怎么验怎么错啊,日志里疯狂刷 Invalid Signature。碰上这种事,赶紧打电话把对方运维摇起来,要来新的公钥证书文件,手动替换到你服务器的配置目录里,重启服务完事。
有时候费了老鼻子劲抓了包,签名验过了,看那个 XML 里的 StatusCode 也是大大的 Success,结果用户一登录跳回业务系统,页面上弹个“查无此人”。这大概率是属性映射给搞岔劈了。在 SAML 的断言里有个核心字段叫 NameID,它还有个长得吓人的 Format 属性比如 urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress。IdP 在断言里把用户的员工编号当作唯一标识发了过来,觉得这就足够代表这个人了。但是你们的应用后台代码比较死板,非拿着传过来的这串数字去数据库里匹配用户的邮箱账号字段。这要能匹配上真是见了鬼了。遇到这种情况,得把两边的开发拉个会,把字段映射规则一行行对齐。
SCIM:别认错人了,这是搞账号同步的苦力
你在配那些 SaaS 软件单点登录后台的时候,除了看到 SSO 的一大堆选项,边上经常还会跟着个 SCIM 配置项。很多人看着这俩挨得那么近,就以为它们是一回事,甚至以为配了 SCIM 就能不用配 SSO 了。
SCIM 根本就不是用来处理登录验证的。它是用来管理用户账号生命周期的后台同步机制。你想啊,公司突然新入职了五百个员工,你总不能在公司大OA里建完账号之后,再专门派个小伙子跑到内部几十个系统的后台管理页面里,手动挨个去录入账号密码吧?那得配到什么时候去,人早晚得疯了。等员工离职的时候要是漏删了一个系统的账号,搞不好还会引发安全事故。
这时候 SCIM 就派上用场了。它其实就是一个定义得非常标准的 RESTful API 接口规范,规定了别人怎么向你发送创建用户、修改部门或者离职停用账号的 HTTP 请求。你要在系统里支持这玩意儿,就提供个 API 的 URL,再生成一个长期有效的访问 Token(Bearer Token)扔给对方就行了。对方的系统有人员变动,就会默默地在后台调你的接口推数据。
如果 SCIM 同步出了问题,排查思路和 SSO 完全是两码事。你就把它当成普通的接口联调去搞就行了。先去系统里查查那个长期的 Bearer Token 是不是失效了,再看看网络防火墙是不是把对方的请求 IP 给拦截了。如果都没问题,直接去服务器上抓一下 API 的 Request 日志,看看是不是那边推过来的 JSON 数据格式有问题。比如人家少传了你数据库底层硬性要求的某个非空必填字段,你的接口直接抛了个 400 错误回去,那当然怎么推都同步不过来了。把它当成业务 CRUD 接口去查,一查一个准。
把这些底层的协议逻辑理清楚了,平时再遇到对接各种乱七八糟的单点登录报错,你脑子里就有张地图了。到底是跳过去的路上丢了参数,还是带着错误的协议头跑回来了,或者是拿着旧的公钥在瞎忙活验签,只要顺着网络时序把每一步的请求拆开看,找到那个卡脖子的节点,这种问题解决起来也就是几分钟的事。
💡 总结
搞懂 OIDC、OAuth 2.0 和 SAML 2.0 的核心差别和排障逻辑,能解决你平时对接认证系统时 90% 以上的无头悬案。下次排查别只顾着挠头,抓包、解码、查日志、看时间,思路清晰了,故障就无处遁形。
如果你觉得这篇文章对你有启发,帮你省了排查问题的时间,欢迎关注、点赞、转发!大家在生产环境里遇到过什么奇葩的 SSO 坑,也欢迎在评论区一块吐槽交流。
引导关注 @运维躬行录 的话语都在这里了,咱们下期继续拆解实战硬核干货。
公众号:耕云躬行录
个人博客:躬行笔记