PKCE接入指南
核心流程
1. PKCE 参数生成
1.1 Code Verifier 生成
// 生成43-128字符的随机字符串
function generateCodeVerifier(length = 128) {
const chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-._~';
const array = new Uint8Array(length);
window.crypto.getRandomValues(array);
return Array.from(array, byte => chars[byte % 66]).join('');
}1.2 Code Challenge 生成
// 使用SHA-256哈希并Base64URL编码
async function generateCodeChallenge(codeVerifier) {
const encoder = new TextEncoder();
const data = encoder.encode(codeVerifier);
const hash = await window.crypto.subtle.digest('SHA-256', data);
return base64URLEncode(String.fromCharCode(...new Uint8Array(hash)));
}
function base64URLEncode(str) {
return btoa(str)
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '');
}2. 授权请求
2.1 构建授权URL
GET {issuer}/请求参数:
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| client_id | string | 是 | 客户端ID |
| redirect_uri | string | 是 | 回调地址 |
| response_type | string | 是 | 固定值: “code” |
| scope | string | 是 | 权限范围,如: “openid” |
| state | string | 是 | 随机状态参数,防CSRF攻击 |
| code_challenge | string | 是 | PKCE代码挑战 |
| code_challenge_method | string | 是 | 固定值: “S256” |
示例:
http://login.mindoffice.lan:8888/?client_id=4df1390c2b5e90e3c782&redirect_uri=http://localhost:3000/callback&response_type=code&scope=openid+profile+email&state=abc123&code_challenge=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk&code_challenge_method=S2562.2 用户授权
用户访问授权URL,完成登录和授权操作。
2.3 授权回调
授权成功后,服务器重定向到回调地址:
{redirect_uri}?code={authorization_code}&state={state}回调参数:
| 参数名 | 类型 | 说明 |
|---|---|---|
| code | string | 授权码 |
| state | string | 状态参数,需与请求时一致 |
3. Token交换
3.1 请求Token
POST {issuer}:8888/account/api/token
Content-Type: application/x-www-form-urlencoded请求参数:
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| grant_type | string | 是 | 固定值: “authorization_code” |
| client_id | string | 是 | 客户端ID |
| code | string | 是 | 授权码 |
| redirect_uri | string | 是 | 回调地址 |
| code_verifier | string | 是 | PKCE代码验证器 |
请求示例:
grant_type=authorization_code&client_id=4df1390c2b5e90e3c782&code=abc123&redirect_uri=http://localhost:3000/callback&code_verifier=mKoBdDPrVmr84ZR6WsAwUq0sw_dv9nCdgxlWK-u_u7c3.2 Token响应格式
标准OAuth 2.0格式:
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"id_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"scope": "openid profile email"
}包装格式(推荐):
{
"code": 0,
"data": {
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "Bearer",
"id_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"expires_in": 2592000,
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"scope": "openid profile email",
"uid": "30403123228430913"
},
"msg": "操作成功",
"trace_id": "fa4cb500-06b2-410c-a682-7d88e534e420"
}4. 用户信息获取
4.1 请求用户信息
POST {issuer}:8888/account/api/userInfo
Authorization: Bearer {access_token}
Content-Type: application/json请求体:
{
"userId": ""
}4.2 用户信息响应格式
包装格式:
{
"code": 0,
"data": {
"sub": "bskj3qdn93d73mft_qjr9jox5fifctnp5",
"name": "测试用户",
"email": "test@example.com",
"preferred_username": "testuser",
"given_name": "测试",
"family_name": "用户",
"phone_number": "+86-138-0000-0000",
"picture": "https://example.com/avatar.jpg",
"department": "技术部",
"position": "软件工程师",
"company": "示例公司",
"updated_at": 1640995200
},
"msg": "操作成功"
}标准OIDC格式:
{
"sub": "bskj3qdn93d73mft_qjr9jox5fifctnp5",
"name": "测试用户",
"email": "test@example.com",
"preferred_username": "testuser",
"email_verified": true,
"updated_at": 1640995200
}完整实现示例
JavaScript/TypeScript 实现
class OIDCPKCEValidator {
constructor(config) {
this.issuer = config.issuer;
this.clientId = config.clientId;
this.redirectUri = config.redirectUri;
this.scope = config.scope || 'openid profile email';
}
// 生成PKCE参数
async generatePKCEParams() {
const codeVerifier = this.generateCodeVerifier(128);
const codeChallenge = await this.generateCodeChallenge(codeVerifier);
return {
codeVerifier,
codeChallenge,
codeChallengeMethod: 'S256'
};
}
// 生成Code Verifier
generateCodeVerifier(length = 128) {
const chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-._~';
const array = new Uint8Array(length);
window.crypto.getRandomValues(array);
return Array.from(array, byte => chars[byte % 66]).join('');
}
// 生成Code Challenge
async generateCodeChallenge(codeVerifier) {
const encoder = new TextEncoder();
const data = encoder.encode(codeVerifier);
const hash = await window.crypto.subtle.digest('SHA-256', data);
return this.base64URLEncode(String.fromCharCode(...new Uint8Array(hash)));
}
// Base64URL编码
base64URLEncode(str) {
return btoa(str)
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '');
}
// 构建授权URL
buildAuthorizationUrl(pkceParams) {
const state = this.generateRandomString(32);
const params = new URLSearchParams({
client_id: this.clientId,
redirect_uri: this.redirectUri,
response_type: 'code',
scope: this.scope,
state: state,
code_challenge: pkceParams.codeChallenge,
code_challenge_method: pkceParams.codeChallengeMethod
});
return {
url: `${this.issuer}/auth?${params.toString()}`,
state: state
};
}
// 交换Token
async exchangeToken(authorizationCode, codeVerifier) {
const tokenEndpoint = `${this.issuer}:8888/account/api/token`;
const params = new URLSearchParams({
grant_type: 'authorization_code',
client_id: this.clientId,
code: authorizationCode,
redirect_uri: this.redirectUri,
code_verifier: codeVerifier
});
const response = await fetch(tokenEndpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: params.toString()
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.error_description || data.error || 'Token交换失败');
}
return this.parseTokenResponse(data);
}
// 解析Token响应
parseTokenResponse(response) {
// 包装格式
if (response.code !== undefined && response.data) {
if (response.code === 0) {
return response.data;
}
throw new Error(response.msg || 'Token获取失败');
}
// 标准格式
return response;
}
// 获取用户信息
async fetchUserInfo(accessToken) {
const userInfoEndpoint = `${this.issuer}:8888/account/api/userInfo`;
const response = await fetch(userInfoEndpoint, {
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
userId: ""
})
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.msg || data.error || '用户信息获取失败');
}
return this.parseUserInfoResponse(data);
}
// 解析用户信息响应
parseUserInfoResponse(response) {
// 包装格式
if (response.code !== undefined && response.data) {
if (response.code === 0) {
return response.data;
}
throw new Error(response.msg || '用户信息获取失败');
}
// 标准格式
return response;
}
// 生成随机字符串
generateRandomString(length) {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
let result = '';
for (let i = 0; i < length; i++) {
result += chars.charAt(Math.floor(Math.random() * chars.length));
}
return result;
}
}
// 使用示例
const validator = new OIDCPKCEValidator({
issuer: 'http://login.mindoffice.lan:8888',
clientId: '4df1390c2b5e90e3c782',
redirectUri: 'http://localhost:3000/callback',
scope: 'openid profile email'
});
// 完整流程示例
async function completePKCEFlow() {
try {
// 1. 生成PKCE参数
const pkceParams = await validator.generatePKCEParams();
console.log('PKCE参数:', pkceParams);
// 2. 构建授权URL
const authData = validator.buildAuthorizationUrl(pkceParams);
console.log('授权URL:', authData.url);
// 3. 用户访问授权URL进行授权
// window.location.href = authData.url;
// 4. 从回调中获取授权码
const authorizationCode = '从回调URL中获取的code';
// 5. 交换Token
const tokenData = await validator.exchangeToken(authorizationCode, pkceParams.codeVerifier);
console.log('Token数据:', tokenData);
// 6. 获取用户信息
const userInfo = await validator.fetchUserInfo(tokenData.access_token);
console.log('用户信息:', userInfo);
return { tokenData, userInfo };
} catch (error) {
console.error('PKCE流程失败:', error);
throw error;
}
}配置参数
必需配置
| 参数名 | 类型 | 说明 | 示例 |
|---|---|---|---|
| issuer | string | OIDC服务器地址 | http://login.mindoffice.lan:8888 |
| clientId | string | 客户端ID | 4df1390c2b5e90e3c782 |
| redirectUri | string | 回调地址 | http://localhost:3000/callback |
可选配置
| 参数名 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| scope | string | openid |
权限范围 |
| responseFormat | string | auto |
Token响应格式 |
错误处理
常见错误码
| 错误码 | 说明 | 解决方案 |
|---|---|---|
| 400 | 请求参数错误 | 检查请求参数格式 |
| 401 | 未授权 | 检查客户端ID和密钥 |
| 403 | 禁止访问 | 检查权限配置 |
| 404 | 接口不存在 | 检查接口地址 |
| 500 | 服务器错误 | 联系服务器管理员 |
错误响应格式
{
"code": 400,
"msg": "参数错误",
"error": "invalid_request",
"error_description": "缺少必需参数"
}安全注意事项
1. PKCE安全
- Code Verifier必须是43-128字符的随机字符串
- 使用密码学安全的随机数生成器
- Code Challenge必须使用SHA-256算法
2. 状态参数
- 每次授权请求必须生成唯一的state参数
- 验证回调中的state参数与请求时一致
- 防止CSRF攻击
3. Token安全
- 安全存储access_token
- 设置合适的token过期时间
- 使用HTTPS传输
4. 回调地址
- 使用HTTPS协议
- 验证回调地址的域名
- 防止开放重定向攻击
总结
OIDC PKCE验证工具提供了完整的身份验证解决方案,支持:
- ✅ 标准协议: 完全符合OAuth 2.0和OIDC标准
- ✅ 安全机制: PKCE扩展防止授权码拦截攻击
- ✅ 灵活配置: 支持多种响应格式和自定义配置
- ✅ 完整流程: 从授权到用户信息获取的完整实现
- ✅ 错误处理: 详细的错误信息和异常处理
- ✅ 易于集成: 提供完整的代码示例和文档
核心接口总结
| 接口 | 方法 | 地址 | 说明 |
|---|---|---|---|
| 授权 | GET | {issuer}/ |
用户授权登录 |
| Token | POST | {issuer}:8888/account/api/token |
交换访问令牌 |
| 用户信息 | POST | {issuer}:8888/account/api/userInfo |
获取用户详细信息 |
通过本文档,您可以轻松地将OIDC PKCE验证功能集成到您的应用程序中。