Skip to content

单点登录(JWT 跳转)

已经登录你产品的用户,无需为了提交反馈再次登录。使用 FeedLog SSO 时,你的后端用只保存在服务器上的密钥签发短期 JWT,FeedLog 根据令牌中的身份信息登录用户。

这种接入方式不需要同步通讯录、提供 SAML 元数据或注册回调地址。

让 Coding Agent 完成接入

打开 SSO 接入 Prompt,复制到已经打开项目的 Coding Agent 中。Agent 会添加产品入口和服务端跳转,并分别处理用户已登录和未登录的情况。

一套密钥,两种接入方式

两种接入方式使用相同的签名密钥和令牌格式。

浏览器跳转反馈组件
接口GET /api/sso/jwtPOST /api/widget/auth/exchange
请求跳转请求,令牌放在 query 中JSON { "jwt": "…" }
响应设置当前域名的 session cookie,再以 HTTP 302 跳转到 return_toJSON,包含 bearer token、过期时间和用户资料
调用方用户的浏览器反馈组件 SDK

配置其中一种接入方式后,另一种接入方式可以直接使用同一套密钥和令牌格式。

创建签名密钥

在后台打开 开发者 → 单点登录,点击 新建密钥。只有工作区所有者可以访问密钥列表,其他成员会看到「仅限所有者」的提示。

密钥由 64 个十六进制字符组成。可以添加 ProductionStaging 等标签,方便在轮换时区分。标签不影响验签,可以随时修改。一个工作区最多可以保存 5 个密钥,之后仍可重新查看和复制,并非只显示一次。

密钥只能保存在服务器端

密钥仅用于在后端签发令牌。任何持有密钥的人都可以为任意邮箱创建有效令牌,并以该用户身份登录。请将密钥保存在密钥管理服务或服务器环境变量中,不要包含在前端产物中,也不要提交到代码仓库。

未登录用户也可以继续访问

SSO 只在你的产品已经识别出用户时携带身份,不应成为访问 FeedLog 的前提。

把产品中的反馈入口指向你自己的服务端跳转接口。用户点击后,该接口读取当前 session:

  • 用户已登录时,签发 JWT,并通过 FeedLog SSO 地址跳转。
  • 用户未登录时,不带 JWT,直接跳转到同一个 FeedLog 页面。

第二种情况不应打开产品自己的登录页面。访客可以匿名访问 FeedLog;需要登录才能执行某项操作时,再使用 FeedLog 自己的登录流程。读取 session 失败也不能直接当作用户未登录,应按照产品现有的错误处理方式处理,避免在故障时悄悄丢失用户身份。

JWT 要求

FeedLog 只接受 HS256 算法。签名时,直接使用后台显示的密钥字符串,并按 UTF-8 读取其字节。不要先对密钥进行 hex 或 base64 解码,否则签名使用的密钥会发生变化,导致验签失败。

字段必填说明
email身份标识,必须包含 @。匹配前会去除首尾空格并转为小写
exp过期时间,以 Unix 秒表示,必须是数字。上限为 24 小时,建议设置为一小时
name显示名。缺失或为空字符串时使用邮箱地址
picture头像 URL

FeedLog 不会读取其他字段,也不支持自定义字段或 kid。验签时,FeedLog 会依次尝试该工作区中所有启用的密钥,直到签名匹配。因此,令牌不需要标明由哪个密钥签发。

exp 的时钟容差为 ±60 秒。如果 exp 超过当前时间 24 小时,FeedLog 会拒绝该令牌,不会将其调整为最大值。

js
import jwt from 'jsonwebtoken'

// Server-side only.
export async function feedbackRedirect(request, returnTo = '/') {
  const baseUrl = new URL(process.env.FEEDLOG_BASE_URL)
  const requestedUrl = new URL(returnTo, baseUrl)
  const safeReturnTo = requestedUrl.origin === baseUrl.origin
    ? `${requestedUrl.pathname}${requestedUrl.search}${requestedUrl.hash}`
    : '/'

  // This function must distinguish a signed-out user from a session error.
  const user = await currentUser(request)
  if (!user) {
    return Response.redirect(new URL(safeReturnTo, baseUrl), 302)
  }

  const token = jwt.sign(
    { email: user.email, name: user.name, picture: user.avatarUrl },
    process.env.FEEDLOG_SSO_SECRET,
    { algorithm: 'HS256', expiresIn: '1h' },
  )

  const handoffUrl = new URL('/api/sso/jwt', baseUrl)
  handoffUrl.searchParams.set('jwt', token)
  handoffUrl.searchParams.set('return_to', safeReturnTo)
  return Response.redirect(handoffUrl, 302)
}

用普通的 <a href> 指向产品中的这个跳转接口。用户点击时再签发令牌,不要在构建静态页面或渲染长期存在的页面时提前签发。这样可以缩短令牌实际存在的时间,也能让同一个入口在请求时判断应该携带身份还是匿名访问。

浏览器跳转

GET /api/sso/jwt 接受两个 query 参数:jwt(必填)和 return_to(可选,默认为 /)。验签通过后,FeedLog 会在当前域名下设置 session cookie,再返回 HTTP 302,将用户重定向到 return_to

return_to 必须指向同一个域名。它可以是 /b/feature-requests 这样的相对路径,也可以是 FeedLog 域名下的完整 URL。对于其他值,包括 //example.com 这样的协议相对 URL,FeedLog 会将其替换为 /,而不会返回错误。如果用户没有进入指定页面,而是进入首页看板,请先检查该参数。

FeedLog 根据请求域名确定工作区,并使用该工作区的密钥验签。请求应使用用户需要访问的工作区所对应的域名。

反馈组件

反馈组件 SDK 会完成令牌交换。实现 auth.getToken() 并返回使用同一密钥签发的 JWT 后,SDK 会将其 POST 到 /api/widget/auth/exchange,按邮箱地址缓存返回的 bearer token,并在 bearer token 过期后再次交换。接入代码不需要直接调用该接口。参见安装反馈组件

每次交换都会创建一条会话记录,因此该接口按 IP 限制为每分钟 30 次请求。SDK 会缓存 bearer token,正常使用不会达到该限制。如果接入时触发限流,请检查是否在每次调用时都重新签发并交换 JWT。

身份匹配与权限

FeedLog 在全局范围内按邮箱地址匹配用户。某个邮箱第一次出现在令牌中时,FeedLog 会创建终端用户账号,不再显示其他登录页面。

后续登录会复用已有账号。namepicture 只在创建账号时读取一次,之后登录不会更新这些字段。因此,在你的产品中修改显示名或头像,不会更新 FeedLog 中的用户资料。

如果用户在你的产品中更换邮箱,FeedLog 会将新邮箱视为一个新账号。原有反馈、投票和评论仍与旧邮箱关联。

SSO 会话仅具有终端用户权限。用户可以提交反馈、投票和评论,但不能进入后台,不能设置或修改密码、邮箱和个人资料,也不能管理工作区;这些操作均返回 403。该会话还绑定到签发它的域名。

轮换密钥

验签接受所有启用中的密钥,因此部署新密钥时,仍可继续接受由旧密钥签发的令牌:

  1. 新建第二个密钥,起好标签。
  2. 把后端换成新密钥部署上去。
  3. 确认所有令牌签发服务都已改用新密钥,再停用旧密钥。
  4. 过几天再删掉。

停用密钥后可以重新启用。重新启用后,由该密钥签发且尚未过期的令牌会再次通过验签。删除密钥无法撤销,由该密钥签发的令牌会立即停止通过验签。

本地开发中的令牌签发

生产环境接入需要由后端保存密钥。本地开发时,可以手动签发一个令牌并添加到 URL 中:

bash
SECRET=paste-a-secret-here node -e "const jwt=require('jsonwebtoken');\
console.log(jwt.sign({email:'[email protected]',name:'Dev User'},\
process.env.SECRET,{algorithm:'HS256',expiresIn:'1h'}))"

将结果填入 http://localhost:3000/api/sso/jwt?jwt=<token> 并打开该 URL。开发反馈组件接入时,也可以让 auth.getToken() 直接返回这个固定字符串。

仅限开发环境

请在终端或开发服务器上签发令牌,不要在浏览器代码中签发,否则密钥会包含在前端产物中。开发密钥仍然可以为所属工作区中的任意邮箱创建有效令牌。请使用隔离的测试工作区,或者创建单独的开发密钥,并在测试结束后停用。

常见错误

未登录访客被带到产品自己的登录页面。 产品中的反馈入口要求必须登录。这个跳转接口应该允许没有 session 的请求:确认用户未登录后,不签发 JWT,直接跳转到目标 FeedLog 页面。

用户进入看板后仍未登录,页面也没有显示错误。 浏览器跳转失败时,/api/sso/jwt 不会向访问者显示原始错误。它会重定向到「我们没能帮你登录」页面,并在三秒后继续进入看板。请在 FeedLog 服务端日志中查找以 [sso] login failed: 开头的记录,确认具体原因。反馈组件的交换接口会以 JSON 返回错误原因。

Invalid or expired SSO token(400)。 签名与所有启用的密钥均不匹配,或者 exp 已过期。这两种情况会返回相同的消息。请依次检查:是否使用了其他环境的密钥、密钥是否已停用或删除、签名前是否对密钥进行了 hex 解码,以及服务器时钟偏差是否超过 60 秒。

SSO token must carry an exp claim(400)。 exp 缺失或不是数字。部分库只有在显式传入过期参数时才会添加该字段。

SSO token exp is too far in the future(400)。 exp 超过 24 小时上限和 60 秒容差。FeedLog 会拒绝令牌,不会将过期时间调整为最大值。如果链接需要保持更长时间有效,例如邮件中的链接,请先将它指向你的应用中的跳转页,并在用户访问时签发令牌。

SSO token must carry a valid email claim(400)。 email 缺失、不是字符串或不包含 @

SSO is not configured for this organization(404)。 该工作区没有启用的密钥。轮换密钥时,请确认新密钥已经部署,再停用旧密钥。

Organization not found(404)。 请求域名无法解析到工作区。请检查跳转或交换请求使用的域名。

Widget is not enabled for this organization(403)。 反馈组件已在工作区设置中停用。该错误不是由 SSO 配置引起的。

Too many token exchanges, try again shortly(429)。 同一个 IP 在一分钟内发起了超过 30 次交换请求。

Widget token exchange failed with status 400 SDK 只报告状态码,具体消息位于响应体中。请在浏览器网络面板中打开失败的请求进行查看。

用户的名字或头像没有更新。 FeedLog 只在创建账号时读取资料字段,之后的 SSO 登录不会更新这些字段。

开源的反馈收集工具。可以自己部署,也可以用我们托管的版本。