欢迎加入 ddzz用户交流群!
| 项目 | 说明 |
|---|---|
| 授权端点 | https://api.sso.zdzz.top/oauth/authorize |
| 令牌端点 | https://api.sso.zdzz.top/oauth/token |
| 用户信息端点 | https://api.sso.zdzz.top/oauth/userinfo |
| 令牌吊销端点 | https://api.sso.zdzz.top/oauth/revoke |
| OIDC 发现文档 | https://api.sso.zdzz.top/oauth/.well-known/openid-configuration |
| JWKS 公钥端点 | https://api.sso.zdzz.top/oauth/jwks.json |
| 授权类型 | 适用场景 |
|---|---|
authorization_code | Web 应用、SPA、移动端(推荐) |
refresh_token | 刷新过期的 access_token |
client_credentials | 服务端间通信(无用户参与) |
| Scope | 说明 | 返回字段 |
|---|---|---|
openid | 必须,标识 OIDC 请求 | sub(用户唯一 ID) |
profile | 用户基本信息 | name、picture |
email | 用户邮箱 | email、email_verified |
phone | 用户手机号 | phone_number、phone_number_verified |
offline_access | 请求 refresh_token | 返回 refresh_token 字段 |
| 字段 | 必填 | 说明 |
|---|---|---|
| 客户端名称 | 是 | 应用的显示名称 |
| 回调地址 | 是 | OAuth 授权后重定向的 URL,每行一个 |
| 权限域 | 是 | 应用需要的权限范围 |
| 申请说明 | 否 | 简要说明应用用途 |
| 应用地址 | 否 | 应用主页 URL |
| 应用图标地址 | 否 | 应用图标的 URL |
| 登出回调地址 | 否 | 用户登出后重定向的 URL |
| 隐私策略地址 | 否 | 隐私政策页面 URL |
| 服务条款地址 | 否 | 服务条款页面 URL |
| 公开客户端 | 否 | 勾选表示无法安全保管密钥(如 SPA、移动端) |
重要: client_secret仅在创建时显示一次,请妥善保管。如需重置,可在客户端详情页点击"重置密钥"。
GET https://api.sso.zdzz.top/oauth/authorize| 参数 | 必填 | 说明 |
|---|---|---|
response_type | 是 | 固定为 code |
client_id | 是 | 你的客户端 ID |
redirect_uri | 是 | 必须与注册时的回调地址完全一致 |
scope | 否 | 请求的权限域,空格分隔,默认 openid |
state | 推荐 | 随机字符串,防止 CSRF 攻击,原样返回 |
code_challenge | 是 | PKCE 挑战码 |
code_challenge_method | 是 | 固定为 S256 |
nonce | 否 | ID Token 用的随机值,防止重放攻击 |
https://api.sso.zdzz.top/oauth/authorize?response_type=code&client_id=your_client_id&redirect_uri=https://yourapp.com/callback&scope=openid%20profile%20email&state=random_state&code_challenge=xxxxxx&code_challenge_method=S256redirect_uri,并附带授权码:https://yourapp.com/callback?code=AUTHORIZATION_CODE&state=random_state| 参数 | 说明 |
|---|---|
error | 错误码,如 access_denied、invalid_scope |
state | 原样返回的 state 值 |
POST https://api.sso.zdzz.top/oauth/token
Content-Type: application/x-www-form-urlencoded| 参数 | 必填 | 说明 |
|---|---|---|
grant_type | 是 | 固定为 authorization_code |
code | 是 | 步骤三获取的授权码 |
redirect_uri | 是 | 必须与步骤二一致 |
code_verifier | 是 | 步骤一生成的 code_verifier |
client_id | 是 | 你的客户端 ID |
client_secret | 机密客户端必填 | 你的客户端密钥(公开客户端不需要) |
{
"access_token": "eyJhbGciOiJSUzI1NiIs...",
"refresh_token": "V1StGXR8_5VH3...",
"id_token": "eyJhbGciOiJSUzI1NiIs...",
"expires_in": 1800,
"token_type": "Bearer"
}| 字段 | 说明 |
|---|---|
access_token | 访问令牌,30 分钟有效 |
refresh_token | 刷新令牌,30 天有效(scope 含 offline_access 时返回) |
id_token | OIDC ID Token,JWT 格式(scope 含 openid 时返回) |
expires_in | access_token 有效期(秒) |
token_type | 固定为 Bearer |
GET https://api.sso.zdzz.top/oauth/userinfo
Authorization: Bearer <access_token>{
"sub": "1",
"name": "zhangsan",
"picture": "/avatars/1.png",
"email": "zhangsan@example.com",
"email_verified": true,
"phone_number": "138****1234",
"phone_number_verified": true
}POST https://api.sso.zdzz.top/oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=refresh_token&refresh_token=<refresh_token>&client_id=<client_id>{
"access_token": "新的 access_token",
"refresh_token": "新的 refresh_token",
"expires_in": 1800,
"token_type": "Bearer"
}注意: 每次刷新后,旧的 refresh_token 会立即失效,请保存新的 refresh_token。
POST https://api.sso.zdzz.top/oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials&client_id=<client_id>&client_secret=<client_secret>{
"access_token": "eyJhbGciOiJSUzI1NiIs...",
"expires_in": 1800,
"token_type": "Bearer"
}此模式不返回 refresh_token 和 id_token,scope 固定为 openid。
POST https://api.sso.zdzz.top/oauth/revoke
Content-Type: application/json
{
"token": "<access_token 或 refresh_token>"
}GET https://api.sso.zdzz.top/oauth/.well-known/openid-configurationGET https://api.sso.zdzz.top/oauth/jwks.jsonimport { useEffect, useState } from 'react';
const CLIENT_ID = 'your_client_id';
const REDIRECT_URI = window.location.origin + '/callback';
const SSO_BASE = 'https://api.sso.zdzz.top';
function base64urlEncode(buffer: Uint8Array) {
return btoa(String.fromCharCode(...buffer))
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/, '');
}
async function generatePKCE() {
const verifierArr = new Uint8Array(32);
crypto.getRandomValues(verifierArr);
const codeVerifier = base64urlEncode(verifierArr);
const encoder = new TextEncoder();
const hash = await crypto.subtle.digest('SHA-256', encoder.encode(codeVerifier));
const codeChallenge = base64urlEncode(new Uint8Array(hash));
return { codeVerifier, codeChallenge };
}
export default function LoginButton() {
const [user, setUser] = useState(null);
const handleLogin = async () => {
const state = crypto.randomUUID();
const { codeVerifier, codeChallenge } = await generatePKCE();
// 保存到 sessionStorage
sessionStorage.setItem('pkce_verifier', codeVerifier);
sessionStorage.setItem('oauth_state', state);
const params = new URLSearchParams({
response_type: 'code',
client_id: CLIENT_ID,
redirect_uri: REDIRECT_URI,
scope: 'openid profile email',
state,
code_challenge: codeChallenge,
code_challenge_method: 'S256',
});
window.location.href = `${SSO_BASE}/oauth/authorize?${params}`;
};
// 处理回调
useEffect(() => {
const params = new URLSearchParams(window.location.search);
const code = params.get('code');
const state = params.get('state');
if (!code) return;
const savedState = sessionStorage.getItem('oauth_state');
const codeVerifier = sessionStorage.getItem('pkce_verifier');
if (state !== savedState || !codeVerifier) {
console.error('State 或 PKCE 验证失败');
return;
}
// 用授权码换令牌(公开客户端不需要 client_secret)
fetch(`${SSO_BASE}/oauth/token`, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'authorization_code',
code,
redirect_uri: REDIRECT_URI,
code_verifier: codeVerifier,
client_id: CLIENT_ID,
}),
})
.then((res) => res.json())
.then((tokens) => {
// 获取用户信息
return fetch(`${SSO_BASE}/oauth/userinfo`, {
headers: { Authorization: `Bearer ${tokens.access_token}` },
}).then((res) => res.json());
})
.then((userInfo) => {
setUser(userInfo);
sessionStorage.removeItem('pkce_verifier');
sessionStorage.removeItem('oauth_state');
window.history.replaceState({}, '', '/');
});
}, []);
if (user) {
return <div>欢迎, {user.name}!</div>;
}
return <button onClick={handleLogin}>使用统一账号登录</button>;
}| 机密客户端 | 公开客户端 | |
|---|---|---|
| 适用场景 | 有后端服务器的 Web 应用 | SPA、移动端、桌面应用 |
| client_secret | 有,换令牌时需要 | 无 |
| 安全性 | 更高 | 依赖 PKCE 保护 |
redirect_uri 必须与注册时填写的回调地址完全一致(包括协议、域名、端口、路径)。POST https://api.sso.zdzz.top/oauth/revokeemail scope,也不会返回 email 字段。| ID Token | access_token | |
|---|---|---|
| 格式 | JWT | JWT |
| 用途 | 包含用户身份信息,供客户端验证 | 用于调用 API(如 userinfo) |
| audience | 你的 client_id | SSO 系统内部 |
| 是否应传给后端 | 不需要,仅前端验证 | 是,作为 API 请求凭证 |