安全指南
加密算法
密码哈希
CaelLabID 使用 Argon2id 作为密码哈希算法:
$argon2id$v=19$m=65536,t=3,p=4$salt$hash
| 参数 | 值 | 说明 |
|---|---|---|
| 内存成本 | 65536 KiB(64 MB) | 抵御 GPU/ASIC 攻击 |
| 时间成本 | 3 次迭代 | 平衡安全性与性能 |
| 并行度 | 4 线程 | 利用多核 CPU |
兼容性:系统自动兼容旧版 bcrypt 哈希,用户下次修改密码时自动升级为 Argon2id。
Salt(盐值)
Argon2id 内置自动生成随机盐值(128 位),每个用户每次哈希的盐值都不同,集成在哈希字符串中:
$argon2id$v=19$m=65536,t=3,p=4$VGhpcyBpcyBhIHNhbHQ$hash
- ❌ 不需要单独存储或管理 salt
- ✅ 相同密码产生完全不同的哈希值
- ✅ 防止彩虹表攻击
Pepper(胡椒)
在 Argon2id 之上,系统额外使用 Pepper 机制:
// 注册
$passwordHash = password_hash($password . $pepper, PASSWORD_ARGON2ID);
// 验证
$passwordWithPepper = $password . $pepper;
password_verify($passwordWithPepper, $storedHash);
Pepper 与 Salt 的区别:
| 特性 | Salt | Pepper |
|---|---|---|
| 存储位置 | 哈希字符串内 | 服务器环境变量 APP_PEPPER |
| 是否保密 | 公开 | 严格保密 |
| 生成方式 | 自动随机生成 | 运维手动设置 |
| 共享范围 | 每个用户不同 | 所有用户共用 |
- Pepper 存储在服务端配置文件
config/app.php,通过环境变量APP_PEPPER设置 - 即使数据库泄露,攻击者没有 Pepper 也无法破解密码
- Pepper 泄露后只需更改环境变量,所有密码即失效(用户需重置密码)
- 推荐使用 64 字节以上随机字符串作为 Pepper
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 |