锐淏统一认证服务
接入技术文档
OIDC / OAuth 2.1 单点登录 · 用户资料 API · 邮箱提醒 SDK
- 文档版本
- v1.0.0
- 发布日期
- 2026 年 10 月 18 日
- 服务地址
- https://sso.rayhaw.com
- 适用对象
- 接入系统开发者、运维与安全评审人员
接入锐淏统一认证
用标准 OIDC 协议接入锐淏 SSO:用户使用手机号登录一次,即可在你的业务系统中获得可信身份。资料由 SSO 统一维护,修改后全平台同步。
接入流程
在管理后台获取 client_id、client_secret,登记回调地址与 scope。
生成 state、nonce、PKCE,跳转 SSO 登录。
用授权码换取 ID Token / Access Token / Refresh Token。
校验 ID Token,调用 UserInfo,建立业务系统自己的登录态。
client_secret 放入前端代码。
1. 创建应用
联系锐淏科技开通接入权限后,登录 管理后台 → 应用管理 → 创建应用。需要准备:
| 配置 | 示例 | 说明 |
|---|---|---|
| 应用名称 | 锐淏云工作台 | 显示在授权页 |
| client_id | rayhaw-workbench | 3–64 位小写字母、数字、点、下划线、短横线 |
| 回调地址 | https://app.example.com/auth/rayhaw/callback | 精确匹配,生产必须 HTTPS |
| 登出回调 | https://app.example.com/ | 可选 |
| 授权范围 | openid profile email phone offline_access | 按业务最小化申请 |
创建成功后 client_secret 只显示一次,请保存到服务端环境变量。
RAYHAW_SSO_BASE=https://sso.rayhaw.com
RAYHAW_CLIENT_ID=rayhaw-workbench
RAYHAW_CLIENT_SECRET=创建时显示的密钥
RAYHAW_REDIRECT_URI=https://app.example.com/auth/rayhaw/callback
2. 发起登录(PHP 示例)
生成一次性 state、nonce 和 PKCE code_verifier,保存到 Session 后跳转:
<?php
session_start();
$base = 'https://sso.rayhaw.com';
$state = bin2hex(random_bytes(24));
$nonce = bin2hex(random_bytes(24));
$verifier = rtrim(strtr(base64_encode(random_bytes(48)), '+/', '-_'), '=');
$challenge = rtrim(strtr(base64_encode(hash('sha256', $verifier, true)), '+/', '-_'), '=');
$_SESSION['rayhaw_state'] = $state;
$_SESSION['rayhaw_nonce'] = $nonce;
$_SESSION['rayhaw_verifier'] = $verifier;
$params = [
'response_type' => 'code',
'client_id' => 'rayhaw-workbench',
'redirect_uri' => 'https://app.example.com/auth/rayhaw/callback',
'scope' => 'openid profile email phone offline_access',
'state' => $state,
'nonce' => $nonce,
'code_challenge' => $challenge,
'code_challenge_method' => 'S256',
];
header('Location: ' . $base . '/oauth/authorize?' . http_build_query($params));
exit;
回调时先校验 state,再用授权码在服务端换取令牌:
if (!hash_equals($_SESSION['rayhaw_state'] ?? '', $_GET['state'] ?? '')) {
exit('state 校验失败');
}
$ch = curl_init('https://sso.rayhaw.com/oauth/token');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => http_build_query([
'grant_type' => 'authorization_code',
'code' => $_GET['code'],
'redirect_uri' => 'https://app.example.com/auth/rayhaw/callback',
'code_verifier' => $_SESSION['rayhaw_verifier'],
]),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_USERPWD => 'rayhaw-workbench:' . getenv('RAYHAW_CLIENT_SECRET'),
CURLOPT_HTTPAUTH => CURLAUTH_BASIC,
]);
$tokens = json_decode(curl_exec($ch), true);
/oauth/jwks 取公钥,验证 RS256 签名,并校验 iss、aud、exp、nonce。不要只在浏览器里解析 JWT。OIDC 端点
| 端点 | 地址 | 方法 |
|---|---|---|
| Discovery | /.well-known/openid-configuration | GET |
| JWKS | /oauth/jwks | GET |
| Authorization | /oauth/authorize | GET |
| Token | /oauth/token | POST form |
| UserInfo | /oauth/userinfo | GET / POST Bearer |
| End Session | /oauth/logout | GET / POST |
| Revocation | /oauth/revoke | POST |
| Introspection | /oauth/introspect | POST 机密客户端 |
Issuer 固定为 https://sso.rayhaw.com。生产环境不允许使用其他域名签发令牌。
授权请求参数
| 参数 | 必填 | 说明 |
|---|---|---|
response_type | 是 | 固定 code |
client_id | 是 | 后台创建的应用标识 |
redirect_uri | 是 | 与后台登记值逐字符一致 |
scope | 是 | 必须包含 openid |
state | 强烈建议 | 随机值,回调校验,防 CSRF |
nonce | 强烈建议 | 写入 ID Token,防重放 |
code_challenge | 公共客户端必填 | PKCE S256 挑战值 |
code_challenge_method | 使用 PKCE 时必填 | 仅 S256 |
prompt | 否 | none / login / consent |
max_age | 否 | 最近认证最大秒数 |
授权码 60 秒有效、一次性使用,并绑定 client、回调地址、PKCE、用户与会话。
换取令牌
POST /oauth/token HTTP/1.1
Host: sso.rayhaw.com
Authorization: Basic base64(client_id:client_secret)
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code
&code=...
&redirect_uri=https%3A%2F%2Fapp.example.com%2Fauth%2Frayhaw%2Fcallback
&code_verifier=...
{
"access_token": "eyJhbGciOiJSUzI1NiIs...",
"token_type": "Bearer",
"expires_in": 3600,
"id_token": "eyJhbGciOiJSUzI1NiIs...",
"scope": "openid profile email phone offline_access",
"refresh_token": "..."
}
Access Token 1 小时;Refresh Token 30 天且每次刷新轮换。仅在 scope 含 offline_access 时返回刷新令牌。
UserInfo
GET /oauth/userinfo
Authorization: Bearer <access_token>
{
"sub": "e21895b7bf7cf8bc2c7b769f753c15454",
"nickname": "张三",
"picture": "https://sso.rayhaw.com/uploads/avatars/u1-xxxx.jpg",
"gender": "male",
"birthdate": "1990-01-01",
"email": "zhangsan@example.com",
"email_verified": true,
"email_bound": true,
"email_prompt_dismissed": false,
"phone_number": "+8613800138000",
"phone_number_verified": true
}
| scope | 返回字段 |
|---|---|
openid | sub |
profile | 昵称、头像、性别、生日、资料更新时间、邮箱提醒状态 |
email | 邮箱与验证状态 |
phone | 手机号与验证状态 |
sub 是稳定的用户标识,请作为业务系统主键,不要使用手机号或邮箱。
刷新令牌与登出
刷新
POST /oauth/token
Authorization: Basic base64(client_id:client_secret)
Content-Type: application/x-www-form-urlencoded
grant_type=refresh_token
&refresh_token=...
每次刷新都会返回新的 Refresh Token,旧令牌立即失效。若返回 invalid_grant,清除本地登录并让用户重新登录。
登出
https://sso.rayhaw.com/oauth/logout
?client_id=rayhaw-workbench
&post_logout_redirect_uri=https%3A%2F%2Fapp.example.com%2F
&state=...
登出回调必须已在后台登记。SSO 会清除统一会话并撤销当前会话的 Refresh Token。
用户资料 API
SSO 是资料唯一数据源。头像、昵称、邮箱、性别、出生日期、身份证号在任意接入应用修改后,全部平台同步。
GET /api/v1/me
Authorization: Bearer <access_token>
PATCH /api/v1/me
Authorization: Bearer <access_token>
Content-Type: application/json
{
"nickname": "新的昵称",
"gender": "female",
"birth_date": "1992-08-18"
}
| scope | 能力 |
|---|---|
profile:write | 修改昵称、头像、性别、生日、身份证、密码;关闭邮箱提醒 |
email:write | 发送/确认邮箱验证码 |
profile:write / email:write,并在用户授权页明确说明。不要在前端保存 client_secret。邮箱绑定
邮箱需完成验证码验证,流程为:
POST /api/v1/me/email/send-code,body{"email":"new@example.com"};- 用户查收 6 位验证码,15 分钟内有效;
POST /api/v1/me/email/confirm,body{"email":"new@example.com","code":"123456"}。
验证成功前原邮箱继续有效。绑定状态会通过 UserInfo 的 email、email_verified 返回。
头像上传
POST /api/v1/me/avatar
Authorization: Bearer <access_token>
Content-Type: application/json
{"avatar":"data:image/jpeg;base64,/9j/4AAQSk..."}
支持 JPG / PNG / WebP,≤ 2 MB;服务端重新编码为 512×512 JPEG,拒绝 SVG、伪造 MIME 与损坏图片。
邮箱绑定提醒 SDK
未绑定邮箱时,在任意接入站点显示提醒。用户点击“不再提醒”后,状态全局生效。
<link rel="stylesheet" href="https://sso.rayhaw.com/sdk/rayhaw-sso.css">
<script src="https://sso.rayhaw.com/sdk/rayhaw-sso.js" defer></script>
<script>
window.addEventListener('DOMContentLoaded', function () {
RayhawSSO.init({
ssoBaseUrl: 'https://sso.rayhaw.com',
clientId: 'rayhaw-workbench',
user: {
email_bound: false,
email_prompt_dismissed: false
},
bindEmailUrl: 'https://sso.rayhaw.com/account?tab=email&bind_email=1',
dismissEndpoint: '/api/sso/dismiss-email-reminder'
});
});
</script>
dismissEndpoint 是接入系统自己的同源代理接口,由服务端携带 Access Token 调 SSO API。不要把长期令牌暴露给浏览器。
错误码
| 错误码 | 场景 | 处理 |
|---|---|---|
invalid_request | 参数缺失、redirect_uri 不匹配、PKCE 不合法 | 检查参数与后台配置 |
invalid_client | client_id/secret 错误、应用停用 | 更新密钥或启用应用 |
invalid_grant | 授权码/刷新令牌无效、过期、已用、PKCE 失败 | 重新发起登录,不自动重试 |
invalid_scope | scope 未授权 | 后台勾选 scope,重新授权 |
login_required | prompt=none 且未登录 | 改为正常跳转登录 |
consent_required | prompt=none 且未授权应用 | 显示授权页或去掉 prompt=none |
access_denied | 用户拒绝授权 | 提示用户,可稍后重试 |
invalid_token | Access Token 无效/过期/会话已退出 | 重新登录 |
server_error | 服务端异常 | 稍后重试并联系技术支持 |
部署上线
public/ 目录,禁止把项目根目录直接暴露给 Web。cd /www/wwwroot/sso.rayhaw.com
php bin/install.php \
--url=https://sso.rayhaw.com \
--db-host=127.0.0.1 --db-port=3306 \
--db-name=rayhaw_sso --db-user=rayhaw_sso --db-pass='数据库密码' \
--admin-phone=13800138000 --admin-password='强密码' \
--admin-name='系统管理员' --demo
- PHP 8.0+,扩展:
pdo_mysql、openssl、mbstring、curl、gd;不依赖fileinfo; - MySQL 5.7+,字符集
utf8mb4; - nginx 根目录
public/,强制 HTTPS; .env属主必须是 PHP-FPM 运行用户(宝塔通常为www),权限600;只改权限不改属主仍会报 Permission denied;- 计划任务每 5 分钟:
php bin/cron.php; - 后台配置阿里云短信与 SMTP,并发送测试。
完整步骤见项目 docs/05-部署运维手册.md。
安全要求
每次随机,回调严格校验,防 CSRF 与重放。
公共客户端强制;机密客户端也建议启用。
client_secret 与令牌不进入浏览器。
禁止通配符;生产环境必须 HTTPS。
验签 + iss/aud/exp/nonce,不能只解析。
授权码 60 秒;Refresh Token 轮换。
常见问题
回调地址明明一样,为什么还是不一致?
逐字符检查协议、端口、末尾斜杠、大小写与 URL 编码。后台采用精确匹配,不支持通配符。
登录后一直循环跳转?
检查回调路由是否指向登录页、Session 是否保存、HTTPS 下 Cookie 是否 Secure、反向代理是否透传 Host/Proto。
UserInfo 没有昵称或邮箱?
Access Token 缺少 profile / email scope;在后台勾选后让用户重新授权。
为什么在业务系统改了昵称,SSO 也变了?
这是设计行为:SSO 是唯一资料源,业务系统通过资料写入 API 修改,全部平台同步。
邮箱提醒如何做到全平台不再弹?
用户点击“不再提醒”后写入 SSO 用户资料;所有接入应用读取 UserInfo 的 email_prompt_dismissed 后不再显示。
支持 CAS 或 SAML 吗?
v1.0.0 仅支持 OIDC / OAuth 2.1。老系统可增加 OIDC 适配层,或等待后续版本。