安全指南
加密算法
密码哈希
CaelLabID 使用 Argon2ID 作为密码哈希算法:
$argon2id$v=19$m=65536,t=3,p=4$salt$hash
| 参数 | 值 | 说明 |
|---|---|---|
| 内存成本 | 65536 KiB | 抵御 GPU 攻击 |
| 时间成本 | 3 次迭代 | 平衡安全性与性能 |
| 并行度 | 4 线程 | 利用多核 CPU |
兼容性:系统自动兼容旧版 bcrypt 哈希,用户下次修改密码时自动升级为 Argon2ID。
Token 哈希
Access Token 和 Refresh Token 使用 SHA-512 哈希存储:
token_hash VARCHAR(128) -- SHA-512 哈希值
兼容性:验证时会依次尝试 SHA-512 和 SHA-256,旧的 SHA-256 记录无需迁移即可正常使用。
Client Secret 哈希
Client Secret 使用 SHA-512 哈希存储:
兼容性:验证时会依次尝试明文、SHA-256、SHA-512,旧记录完全兼容。
Token 存储安全
服务端存储
CaelLabID 在数据库中只存储 Token 的 SHA-512 哈希,不存储明文:
-- oauth_access_tokens 表
token_hash VARCHAR(128) -- SHA-512 哈希,而非明文 token
即使数据库泄露,攻击者也无法还原出原始 Token。
客户端存储
在你的应用中存储 Token 的建议:
| 方式 | 安全性 | 说明 |
|---|---|---|
| 服务端 Session | 高 | Token 存在服务端 Session 中,浏览器只有 Session ID |
| HttpOnly Cookie | 高 | Cookie 设置 HttpOnly + Secure + SameSite |
| 内存变量 | 中 | SPA 应用存 JS 变量中,页面刷新后丢失 |
| localStorage | 低 | 容易被 XSS 攻击窃取 |
| sessionStorage | 低 | 同上 |
不要将 Token 存储在:
- 浏览器 localStorage/sessionStorage(XSS 风险)
- URL 查询参数中
- 日志文件中
- 前端代码的变量中(可被查看源码获取)
Client Secret 安全
Client Secret仅在创建时显示一次- 只在服务端代码中使用
- 不要提交到 Git 仓库
- 不要在前端 JavaScript 中使用
- 不要在日志中打印
- 定期轮换(建议每 3-6 个月)
Refresh Token 安全
- Refresh Token 有效期 30 天
- 每次 使用都会轮换(旧的失效,返回新的)
- 重置 Client Secret 会撤销所有活跃的 Refresh Token
- 存储建议与 Access Token 相同
CSRF 防护
state 参数
在授权请求中始终使用 state 参数:
// 1. 生成随机 state
$state = bin2hex(random_bytes(16));
// 2. 存储到 Session
$_SESSION['oauth_state'] = $state;
// 3. 授权 URL 带上 state
$url = "https://id.caellab.com/oauth/authorize?..." . "&state={$state}";
// 4. 回调时验证
if ($_GET['state'] !== $_SESSION['oauth_state']) {
die('CSRF 检测失败');
}
CSRF Token
所有 POST 表单(授权确认、登录、注册等)都要求 _csrf 参数。CaelLabID 的授权页面会自动包含 CSRF Token。
推荐实践
1. 最小权限
只申请你需要的 Scope:
# 仅获取用户标识
scope=openid
# 获取用户标识 + 用户名头像
scope=openid profile
信息
profile 属于中危权限,用户在授权页面会看到确认提示,可以自由决定是否授予。
2. Token 过期处理
Access Token 有效期 1 小时。你的应用应该:
async function apiCall(url) {
let res = await fetch(url, {
headers: { Authorization: `Bearer ${accessToken}` }
});
// Token 过期,自动刷新
if (res.status === 401) {
const newTokens = await refreshAccessToken(refreshToken);
if (newTokens) {
saveTokens(newTokens);
res = await fetch(url, {
headers: { Authorization: `Bearer ${newTokens.access_token}` }
});
} else {
// Refresh Token 也过期了,需要重新授权
redirectToAuthPage();
}
}
return res.json();
}
3. 退出登录
用户退出时,撤销 Token:
// 撤销 Access Token
curl -X POST https://id.caellab.com/oauth/revoke \
-d "token=" . $accessToken;
// 清除本地 Session
session_destroy();
4. 安全退出流程
1. 调用 /oauth/revoke 撤销 Token
2. 清除本地 Session/Cookie
3. 可选:重定向到 CaelLabID 的 /logout
频率限制
所有 API 端点都有频率限制,超限会返回 HTTP 429。频率限制基于请求方 IP(用户/客户端的 IP 地址),不是服务器级别的全局限制。
| 端点 | 限制 | 维度 |
|---|---|---|
/oauth/token | 30 次/分钟 | 请求方 IP |
/api/userinfo | 60 次/分钟 | 请求方 IP |
/api/token-info | 30 次/分钟 | 请求方 IP |
| 登录 | 5 次/分钟 | 请求方 IP |
| 注册 | 3 次/小时 | 请求方 IP |
| 创建应用 | 1 次/分钟 | 开发者 |
| 重置密钥 | 3 次/小时 | 开发者 |
被限流时,响应头包含 Retry-After 字段,告诉你多少秒后可以重试。
常见攻击防护
| 攻击 | 防护措施 |
|---|---|
| CSRF | state 参数 + CSRF Token |
| XSS | HttpOnly Cookie + 服务端渲染 |
| 暴力破解 | IP 频率限制(5 次/分钟) |
| 授权码枚举 | 授权码 64 字符 hex + 10 分钟过期 + 一次性使用 |
| Token 泄露 | SHA-512 哈希存储 + 过期机制 + 撤销机制 |
| 重放攻击 | 授权码一次性使用 + Refresh Token 轮换 |
| 跨应用关联 | 应用级 subject_id |