跨服务鉴权(不走 Discord OAuth)
YouTeacher 里那个面向 Discord 的服务并不自己做登录。它虽然坐在 Discord 这一侧,却从不说 Discord OAuth。它借用的是整个平台已经建立好的身份:一个已经登录了主产品的浏览器会带着一个 session,而这个服务信任平台中心的鉴权服务来告诉它这个 session 属于谁。
一个请求的形状
请求到达时,服务先做一次本地检查:浏览器到底有没有带平台 session。没有 → 直接当匿名处理,什么都不转发。这是一次廉价的本地判断,不发网络请求。
如果有 session,服务就把浏览器的 session cookie 转发给鉴权服务的“当前用户”端点,取回答案:一个用户 id、email、一个 userType(talent / 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 的确切形状——不写进一篇公开笔记。
相关
- youteacher ——这个服务所属的平台