单点登录(SSO)
工作原理
- 读者登录你的网站期间,你的服务器会签发一个短期令牌,说明读者的身份。
- 页面通过
sso-token属性把这个令牌传给评论组件。 - Crowment 校验签名,并以该身份让读者登录评论区。
令牌使用只有你的服务器和 Crowment 知道的密钥签名,因此没人能冒用他人身份让读者登录。所有套餐均可使用单点登录。
1. 开启单点登录
打开 Site Console → 你的组件 → Single Sign-On。所有者和管理员可以修改设置。
创建签名密钥
只有组件所有者可以执行此操作。密钥只显示一次:请把它保存在服务器的环境变量中,例如 CROWMENT_SSO_SECRET。切勿把它放进浏览器端代码、应用安装包或公开代码仓库。
选择模式
- Off:读者只能用 Crowment 登录。
- Optional:已登录你网站的读者会自动登录;Crowment 登录方式依然可用。
- Required:只有你网站的账号可以登录。已有的评论和会话保持不变。
设置登录页和退出页(可选)
两者都必须位于组件的域名下。组件的登录按钮会把读者带到你的登录页,并附带 return_to 参数,记录读者来自哪个页面。重定向前请确认 return_to 指向你自己的网站。
2. 在服务器上签发令牌
读者处于登录状态时,每次页面浏览都要用 HS256 和你的密钥签发一个 JWT。
| Claim | 必填 | 值 |
|---|---|---|
sub | 是 | 读者在你网站上的用户 ID,必须是字符串,最多 128 个字符。请使用永不改变、永不复用的 ID;邮箱地址不是好的选择。 |
name | 是 | 显示在其评论旁的名称,最多 100 个字符。HTML 会被移除。 |
avatar | 否 | 以 https:// 开头的图片 URL。其他值会被忽略。 |
badges | 否 | 读者持有的徽章键,例如 ["vip"],需与 Badges 页面中设置的 SSO 键一致。省略时徽章保持不变;[] 会移除通过此方式获得的徽章。在控制台中授予的徽章会保留。 |
aud | 是 | 组件的 app key,必须与嵌入代码中的完全一致。 |
iat、exp | 是 | 签发时间和过期时间(秒)。令牌有效期最长一小时,允许一分钟的时钟误差。 |
不需要也不会使用 email claim:Crowment 从不通过邮箱匹配账号。
// npm install jsonwebtoken
import jwt from 'jsonwebtoken'
const ssoToken = jwt.sign(
{ sub: String(user.id), name: user.displayName, avatar: user.avatarUrl },
process.env.CROWMENT_SSO_SECRET,
{ algorithm: 'HS256', audience: CROWMENT_APP_KEY, expiresIn: '1h' },
)
// composer require firebase/php-jwt
use Firebase\JWT\JWT;
$now = time();
$ssoToken = JWT::encode([
'sub' => (string) $user->id,
'name' => $user->display_name,
'avatar' => $user->avatar_url,
'aud' => CROWMENT_APP_KEY,
'iat' => $now,
'exp' => $now + 3600,
], getenv('CROWMENT_SSO_SECRET'), 'HS256');
# pip install pyjwt
import os
from datetime import datetime, timedelta, timezone
import jwt
now = datetime.now(timezone.utc)
sso_token = jwt.encode(
{
"sub": str(user.id),
"name": user.display_name,
"avatar": user.avatar_url,
"aud": CROWMENT_APP_KEY,
"iat": now,
"exp": now + timedelta(hours=1),
},
os.environ["CROWMENT_SSO_SECRET"],
algorithm="HS256",
)
// go get github.com/golang-jwt/jwt/v5
now := time.Now()
ssoToken, err := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"sub": strconv.FormatInt(user.ID, 10),
"name": user.DisplayName,
"avatar": user.AvatarURL,
"aud": crowmentAppKey,
"iat": now.Unix(),
"exp": now.Add(time.Hour).Unix(),
}).SignedString([]byte(os.Getenv("CROWMENT_SSO_SECRET")))
3. 把令牌传给组件
对于服务器端渲染的页面,把令牌写进嵌入代码:
<script defer src="https://embeds.crowment.com/comments.js"></script>
<!-- signed in: pass the token your backend signed for this page view -->
<crow-comments app-key="YOUR_APP_KEY" sso-token="SIGNED_TOKEN"></crow-comments>
<!-- signed out: leave sso-token out -->
<crow-comments app-key="YOUR_APP_KEY"></crow-comments>
读者处于登录状态的每一个页面都要传入令牌。没有令牌的页面会告诉组件读者已退出登录,组件也会让读者退出评论区。浏览更多页面不会每次都重新登录:组件会保持会话,只有当读者变化时才换用新令牌。
在单页应用中,请随登录状态的变化设置或移除该属性,并监听组件的事件:
const widget = document.querySelector('crow-comments')
// after your site signs the reader in
widget.setAttribute('sso-token', await fetchCrowmentToken())
// after your site signs the reader out
widget.removeAttribute('sso-token')
// the token expired while the page was open: hand over a fresh one
widget.addEventListener('crow:sso-expired', async () => {
widget.setAttribute('sso-token', await fetchCrowmentToken())
})
// Required mode without a sign-in page: show your own login
widget.addEventListener('crow:login-required', () => openLoginDialog())
crow:sso-expired:组件需要为读者登录,但令牌已过期。请设置新令牌。crow:login-required:处于 Required 模式、未设置登录页,且读者点击了登录按钮。
通过你的网站登录的读者,也通过你的网站退出。未设置退出页时,组件会隐藏自身的退出按钮,因为下一个页面不传令牌本身就会让读者退出。
4. 账号关联
已经拥有 Crowment 账号的读者,可以在评论区点击头像旁的链接图标进行关联。读者会在弹出窗口中登录 Crowment,并在 Crowment 页面上确认。此后,你的网站会以其 Crowment 账号为其登录,他用网站账号写过的评论也会转移到这个账号。
关联后,你的网站只能在你自己的页面上以其 Crowment 账号为其登录;确认页面会向读者说明这一点。读者可以随时在 User Console 的 Profile → Linked sites 中解除关联,评论仍归读者所有。你也可以在 SSO 设置中通过 Account linking 关闭关联功能。
令牌中的名称和头像会在每次登录时更新读者的评论资料。已关联的 Crowment 账号则保留自己的名称和头像。
安全
- 把密钥保存在服务器上。拿到密钥的人可以在你的网站上以任何人的身份让读者登录。
- 如果密钥泄露,请点击 Replace secret。用旧密钥签发的令牌会立即失效,因此请马上部署新密钥。已登录评论区的读者会保持登录。
- 只为当前请求的读者签发令牌,并保持令牌短期有效。
故障排查
| 现象 | 可能原因 |
|---|---|
invalid_sso_token | 密钥错误或已被更换、aud 不是 app key、使用了 HS256 以外的算法、sub 以数字形式发送、name 为空,或有效期超过一小时。 |
sso_token_expired | 令牌的 exp 已过,或服务器时钟不准。请在每次页面浏览时签发新令牌。 |
| SSO is not enabled for this app | 模式为 Off,或尚未创建密钥。 |
| Request origin does not match | 页面不在组件的域名或其子域名下。 |