cross-service-auth

跨服务鉴权(不走 Discord OAuth)

YouTeacher 里那个面向 Discord 的服务并不自己做登录。它虽然坐在 Discord 这一侧,却从不说 Discord OAuth。它借用的是整个平台已经建立好的身份:一个已经登录了主产品的浏览器会带着一个 session,而这个服务信任平台中心的鉴权服务来告诉它这个 session 属于谁。

一个请求的形状

请求到达时,服务先做一次本地检查:浏览器到底有没有带平台 session。没有 → 直接当匿名处理,什么都不转发。这是一次廉价的本地判断,不发网络请求。

如果有 session,服务就把浏览器的 session cookie 转发给鉴权服务的“当前用户”端点,取回答案:一个用户 id、email、一个 userTypetalent / employer / recruiter / admin),以及可选的 name。如果鉴权服务拒绝,或者这个账号还没有 userType(注册没完成),调用方就被解析成 null——同样按匿名处理。session cookie 才是证明到底是哪个人的东西;这个服务自己不持有密码、不持有 OAuth token、也没有自己的 session 存储。

两份凭据,两个职责

一个转发出去的请求其实带着两样东西,各自回答不同的问题:

  • session cookie 回答是哪个用户——它是人的身份,由中心登录流程签发,这里只是原样传递。
  • 一枚短时效的服务令牌(签名令牌、有效期在一分钟量级、标注了发起服务的名字)回答是哪个服务在调用。它是内部调用方的工牌,让鉴权服务和 profile 服务能把可信的兄弟服务和任意互联网流量区分开。

这两者相互独立。服务令牌绝不替代用户;它只为服务背书。一个需要以用户身份行事的调用,仍然必须转发那个用户的 session。

展示身份来自另一个服务

用户 id 和 email 足以给一个动作授权,但不足以显示一个友好的名字和头像。这些来自独立的 profile 服务。Discord 服务在这里同样转发 session cookie,因为 profile 查询是按 profile id 而不是 auth user id 来定位的——所以光靠服务令牌无法定位到正确的记录。查询会按一串可能的展示字段(显示名、用户名、机构名或学校名)逐一尝试;如果 profile 服务不可达或返回空,就回退到用户的 name,再不行就回退到 email 的本地部分。头像是可选的,缺失时直接省略。一句话概括:身份以 auth 为权威,展示以 profile 尽力而为。

路由如何区分读与写

同一个身份 helper 暴露了两种姿态,路由处理器各取所需:

  • Extract(提取)——有用户就解析出来,没有就返回空。公开读走这条:匿名访客照样拿到响应,只是不做个性化。
  • Require(要求)——要么解析出用户,要么把请求拒为未授权。需要鉴权的写走这条:没有有效 session,就没有动作。

所以“公开读、鉴权写”并不是靠散落在各路由里的检查来强制的;它是一个决定——这个处理器要哪种姿态——落在同一个身份解析器上。路由路径本身也不硬编码,而是从配置注入,因此这个服务可以指向不同的 auth / profile 部署而不用改代码。

为什么这么设计

单点登录只活在一个地方,每个卫星服务都靠它,而不是在每个界面各自重新实现一遍 OAuth、token 刷新和 session 存储。代价是一条信任边界:这个服务必须是 session cookie 和内部令牌可以安全流经的地方。这也正是为什么其中的机制细节——密钥、令牌和 cookie 的确切形状——不写进一篇公开笔记。

相关

about this entry

One of sijie's wiki entries. The AI on this site is grounded in the same corpus and answers in sijie's voice, with citations back to entries like this one — answering costs sijie money, so it waits behind a code: enter an access code →