This commit is contained in:
Seefs
2025-09-08 12:09:26 +08:00
parent b7527eb80e
commit 91a0eb7031
22 changed files with 5001 additions and 11 deletions

909
docs/oauth2-demo.html Normal file
View File

@@ -0,0 +1,909 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>OAuth2 自动登录 Demo</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
line-height: 1.6;
}
.section {
margin: 20px 0;
padding: 20px;
border: 1px solid #ddd;
border-radius: 8px;
}
.hidden { display: none; }
.button {
background: #007bff;
color: white;
border: none;
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
margin: 5px;
}
.button:hover { background: #0056b3; }
.button.secondary { background: #6c757d; }
.button.danger { background: #dc3545; }
.code {
background: #f8f9fa;
padding: 15px;
border-radius: 4px;
font-family: 'Monaco', 'Consolas', monospace;
font-size: 12px;
overflow-x: auto;
white-space: pre;
}
.log {
background: #f8f9fa;
border: 1px solid #ddd;
padding: 10px;
border-radius: 4px;
max-height: 300px;
overflow-y: auto;
font-family: monospace;
font-size: 12px;
}
.config-form {
display: grid;
gap: 10px;
grid-template-columns: 150px 1fr;
align-items: center;
}
.config-form input {
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
}
.status {
padding: 10px;
border-radius: 4px;
margin: 10px 0;
}
.status.success { background: #d4edda; color: #155724; border: 1px solid #c3e6cb; }
.status.error { background: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; }
.status.info { background: #d1ecf1; color: #0c5460; border: 1px solid #bee5eb; }
</style>
</head>
<body>
<h1>OAuth2 服务器自动登录 Demo</h1>
<p>这个演示展示了如何使用OAuth2实现自动登录功能。</p>
<!-- 配置区域 -->
<div class="section">
<h2>配置</h2>
<div class="config-form">
<label>服务器地址:</label>
<input type="text" id="serverUrl" value="https://your-domain.com" placeholder="https://your-domain.com">
<label>Client ID:</label>
<input type="text" id="clientId" placeholder="your_client_id">
<label>Client Secret:</label>
<input type="password" id="clientSecret" placeholder="your_client_secret">
<label>重定向URI:</label>
<input type="text" id="redirectUri" placeholder="当前页面会自动设置">
<label>权限范围:</label>
<input type="text" id="scopes" value="api:read api:write">
</div>
<div style="margin-top: 15px;">
<button class="button" onclick="saveConfig()">保存配置</button>
<button class="button secondary" onclick="loadConfig()">加载配置</button>
<button class="button secondary" onclick="testServerInfo()">测试服务器</button>
</div>
</div>
<!-- 登录状态区域 -->
<div class="section">
<h2>登录状态</h2>
<div id="loginStatus" class="status info">未登录</div>
<!-- 未登录显示 -->
<div id="loginSection">
<h3>选择登录方式:</h3>
<button class="button" onclick="clientCredentialsLogin()">Client Credentials 登录</button>
<button class="button" onclick="authorizationCodeLogin()">授权码登录 (用户交互)</button>
<button class="button secondary" onclick="checkExistingToken()">检查已有令牌</button>
</div>
<!-- 已登录显示 -->
<div id="loggedInSection" class="hidden">
<h3>已登录</h3>
<div id="userInfo"></div>
<div style="margin-top: 15px;">
<button class="button" onclick="getUserInfo()">获取用户信息</button>
<button class="button" onclick="refreshAccessToken()">刷新令牌</button>
<button class="button secondary" onclick="testApiCall()">测试API调用</button>
<button class="button danger" onclick="logout()">登出</button>
</div>
</div>
</div>
<!-- 令牌信息区域 -->
<div class="section">
<h2>令牌信息</h2>
<div style="margin-bottom: 10px;">
<button class="button secondary" onclick="showTokenDetails()">显示令牌详情</button>
<button class="button secondary" onclick="decodeJWT()">解析JWT</button>
</div>
<div id="tokenInfo" class="code"></div>
</div>
<!-- 日志区域 -->
<div class="section">
<h2>操作日志</h2>
<button class="button secondary" onclick="clearLog()">清空日志</button>
<div id="logArea" class="log"></div>
</div>
<script>
// 配置对象
let config = {
serverUrl: '',
clientId: '',
clientSecret: '',
redirectUri: '',
scopes: 'api:read api:write'
};
// OAuth2 客户端类
class OAuth2Client {
constructor(config) {
this.config = config;
}
// 生成随机字符串
generateRandomString(length = 32) {
const charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~';
let result = '';
for (let i = 0; i < length; i++) {
result += charset.charAt(Math.floor(Math.random() * charset.length));
}
return result;
}
// 生成PKCE参数
async generatePKCE() {
const codeVerifier = this.generateRandomString(128);
const encoder = new TextEncoder();
const data = encoder.encode(codeVerifier);
const digest = await crypto.subtle.digest('SHA-256', data);
const codeChallenge = btoa(String.fromCharCode(...new Uint8Array(digest)))
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '');
return { codeVerifier, codeChallenge };
}
// Client Credentials 流程
async clientCredentialsFlow() {
const params = new URLSearchParams({
grant_type: 'client_credentials',
client_id: this.config.clientId,
client_secret: this.config.clientSecret,
scope: this.config.scopes
});
const response = await fetch(`${this.config.serverUrl}/api/oauth/token`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: params
});
return response.json();
}
// 授权码流程 - 步骤1重定向到授权页面
async startAuthorizationCodeFlow() {
const { codeVerifier, codeChallenge } = await this.generatePKCE();
const state = this.generateRandomString();
// 保存参数
localStorage.setItem('oauth_code_verifier', codeVerifier);
localStorage.setItem('oauth_state', state);
// 构建授权URL
const authUrl = new URL(`${this.config.serverUrl}/api/oauth/authorize`);
authUrl.searchParams.set('response_type', 'code');
authUrl.searchParams.set('client_id', this.config.clientId);
authUrl.searchParams.set('redirect_uri', this.config.redirectUri);
authUrl.searchParams.set('scope', this.config.scopes);
authUrl.searchParams.set('state', state);
authUrl.searchParams.set('code_challenge', codeChallenge);
authUrl.searchParams.set('code_challenge_method', 'S256');
// 重定向
window.location.href = authUrl.toString();
}
// 授权码流程 - 步骤2处理回调
async handleAuthorizationCallback() {
const urlParams = new URLSearchParams(window.location.search);
const code = urlParams.get('code');
const state = urlParams.get('state');
const error = urlParams.get('error');
if (error) {
throw new Error(`Authorization error: ${error}`);
}
const savedState = localStorage.getItem('oauth_state');
if (state !== savedState) {
throw new Error('State mismatch - possible CSRF attack');
}
const codeVerifier = localStorage.getItem('oauth_code_verifier');
if (!code || !codeVerifier) {
throw new Error('Missing authorization code or code verifier');
}
// 交换访问令牌
const params = new URLSearchParams({
grant_type: 'authorization_code',
client_id: this.config.clientId,
client_secret: this.config.clientSecret,
code: code,
redirect_uri: this.config.redirectUri,
code_verifier: codeVerifier
});
const response = await fetch(`${this.config.serverUrl}/api/oauth/token`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: params
});
const tokens = await response.json();
// 清理临时数据
localStorage.removeItem('oauth_code_verifier');
localStorage.removeItem('oauth_state');
// 清理URL参数
window.history.replaceState({}, document.title, window.location.pathname);
return tokens;
}
// 刷新令牌
async refreshToken(refreshToken) {
const params = new URLSearchParams({
grant_type: 'refresh_token',
client_id: this.config.clientId,
client_secret: this.config.clientSecret,
refresh_token: refreshToken
});
const response = await fetch(`${this.config.serverUrl}/api/oauth/token`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: params
});
return response.json();
}
// 调用API
async callAPI(endpoint, options = {}) {
const accessToken = localStorage.getItem('access_token');
if (!accessToken) {
throw new Error('No access token available');
}
const response = await fetch(`${this.config.serverUrl}${endpoint}`, {
...options,
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json',
...options.headers
}
});
if (response.status === 401) {
// 尝试刷新令牌
const refreshToken = localStorage.getItem('refresh_token');
if (refreshToken) {
const tokens = await this.refreshToken(refreshToken);
if (tokens.access_token) {
localStorage.setItem('access_token', tokens.access_token);
if (tokens.refresh_token) {
localStorage.setItem('refresh_token', tokens.refresh_token);
}
// 重试请求
return this.callAPI(endpoint, options);
}
}
throw new Error('Authentication failed');
}
return response.json();
}
// 获取服务器信息
async getServerInfo() {
const response = await fetch(`${this.config.serverUrl}/api/oauth/server-info`);
return response.json();
}
// 获取JWKS
async getJWKS() {
const response = await fetch(`${this.config.serverUrl}/api/oauth/jwks`);
return response.json();
}
// 解析JWT令牌
parseJWTToken(token) {
try {
const parts = token.split('.');
if (parts.length !== 3) {
throw new Error('Invalid JWT token format');
}
const payload = JSON.parse(atob(parts[1]));
return {
userId: payload.sub,
username: payload.preferred_username || payload.sub,
email: payload.email,
name: payload.name,
roles: payload.scope?.split(' ') || [],
groups: payload.groups || [],
exp: payload.exp,
iat: payload.iat,
iss: payload.iss,
aud: payload.aud,
jti: payload.jti
};
} catch (error) {
console.error('Failed to parse JWT token:', error);
return null;
}
}
// 验证JWT令牌
validateJWTToken(token) {
const userInfo = this.parseJWTToken(token);
if (!userInfo) return false;
const now = Math.floor(Date.now() / 1000);
if (userInfo.exp && now >= userInfo.exp) {
console.log('JWT token has expired');
return false;
}
return true;
}
// 获取当前用户信息从JWT令牌
getCurrentUser() {
const token = localStorage.getItem('access_token');
if (!token || !this.validateJWTToken(token)) {
return null;
}
return this.parseJWTToken(token);
}
// 检查令牌是否即将过期
isTokenExpiringSoon(token) {
if (!token) return true;
try {
const parts = token.split('.');
if (parts.length !== 3) return true;
const payload = JSON.parse(atob(parts[1]));
const exp = payload.exp * 1000; // 转换为毫秒
const now = Date.now();
return exp - now < 5 * 60 * 1000; // 5分钟内过期
} catch (error) {
console.error('Token validation failed:', error);
return true;
}
}
}
let oauth2Client;
// 日志函数
function log(message, type = 'info') {
const timestamp = new Date().toISOString();
const logArea = document.getElementById('logArea');
const logEntry = `[${timestamp}] ${message}\n`;
logArea.textContent += logEntry;
logArea.scrollTop = logArea.scrollHeight;
console.log(message);
}
// 保存配置
function saveConfig() {
config.serverUrl = document.getElementById('serverUrl').value;
config.clientId = document.getElementById('clientId').value;
config.clientSecret = document.getElementById('clientSecret').value;
config.redirectUri = document.getElementById('redirectUri').value || window.location.origin + window.location.pathname;
config.scopes = document.getElementById('scopes').value;
localStorage.setItem('oauth_config', JSON.stringify(config));
oauth2Client = new OAuth2Client(config);
log('配置已保存');
}
// 加载配置
function loadConfig() {
const saved = localStorage.getItem('oauth_config');
if (saved) {
config = JSON.parse(saved);
document.getElementById('serverUrl').value = config.serverUrl;
document.getElementById('clientId').value = config.clientId;
document.getElementById('clientSecret').value = config.clientSecret;
document.getElementById('redirectUri').value = config.redirectUri;
document.getElementById('scopes').value = config.scopes;
oauth2Client = new OAuth2Client(config);
log('配置已加载');
}
}
// 测试服务器信息
async function testServerInfo() {
try {
saveConfig();
const info = await oauth2Client.getServerInfo();
log('服务器信息: ' + JSON.stringify(info, null, 2));
updateStatus('服务器连接正常', 'success');
} catch (error) {
log('测试服务器失败: ' + error.message);
updateStatus('服务器连接失败: ' + error.message, 'error');
}
}
// 客户端凭证登录
async function clientCredentialsLogin() {
try {
saveConfig();
log('开始 Client Credentials 登录...');
const tokens = await oauth2Client.clientCredentialsFlow();
if (tokens.access_token) {
localStorage.setItem('access_token', tokens.access_token);
if (tokens.refresh_token) {
localStorage.setItem('refresh_token', tokens.refresh_token);
}
log('Client Credentials 登录成功');
updateLoginState(true);
} else {
throw new Error('未收到访问令牌: ' + JSON.stringify(tokens));
}
} catch (error) {
log('Client Credentials 登录失败: ' + error.message);
updateStatus('登录失败: ' + error.message, 'error');
}
}
// 授权码登录
async function authorizationCodeLogin() {
try {
saveConfig();
log('开始授权码登录...');
await oauth2Client.startAuthorizationCodeFlow();
} catch (error) {
log('授权码登录失败: ' + error.message);
updateStatus('登录失败: ' + error.message, 'error');
}
}
// 检查现有令牌
function checkExistingToken() {
const accessToken = localStorage.getItem('access_token');
if (accessToken) {
log('发现现有访问令牌');
updateLoginState(true);
} else {
log('未找到访问令牌');
updateLoginState(false);
}
}
// 获取用户信息
async function getUserInfo() {
try {
// 从JWT令牌获取用户信息
const tokenUser = oauth2Client.getCurrentUser();
// 从API获取用户信息
const apiUser = await oauth2Client.callAPI('/api/user/self');
document.getElementById('userInfo').innerHTML = `
<h4>JWT令牌中的用户信息:</h4>
<pre>${JSON.stringify(tokenUser, null, 2)}</pre>
<h4>API返回的用户信息:</h4>
<pre>${JSON.stringify(apiUser, null, 2)}</pre>
`;
log('获取用户信息成功');
} catch (error) {
log('获取用户信息失败: ' + error.message);
}
}
// 刷新访问令牌
async function refreshAccessToken() {
try {
const refreshToken = localStorage.getItem('refresh_token');
if (!refreshToken) {
throw new Error('没有刷新令牌');
}
const tokens = await oauth2Client.refreshToken(refreshToken);
if (tokens.access_token) {
localStorage.setItem('access_token', tokens.access_token);
if (tokens.refresh_token) {
localStorage.setItem('refresh_token', tokens.refresh_token);
}
log('令牌刷新成功');
showTokenDetails();
} else {
throw new Error('刷新令牌失败: ' + JSON.stringify(tokens));
}
} catch (error) {
log('刷新令牌失败: ' + error.message);
}
}
// 测试API调用
async function testApiCall() {
try {
const result = await oauth2Client.callAPI('/api/user/self');
log('API调用成功: ' + JSON.stringify(result, null, 2));
} catch (error) {
log('API调用失败: ' + error.message);
}
}
// 登出
function logout() {
localStorage.removeItem('access_token');
localStorage.removeItem('refresh_token');
document.getElementById('userInfo').innerHTML = '';
updateLoginState(false);
log('已登出');
}
// 显示令牌详情
function showTokenDetails() {
const accessToken = localStorage.getItem('access_token');
const refreshToken = localStorage.getItem('refresh_token');
let details = '';
if (accessToken) {
details += `访问令牌: ${accessToken.substring(0, 50)}...\n\n`;
}
if (refreshToken) {
details += `刷新令牌: ${refreshToken.substring(0, 50)}...\n\n`;
}
document.getElementById('tokenInfo').textContent = details || '无令牌';
}
// 解析JWT
function decodeJWT() {
const accessToken = localStorage.getItem('access_token');
if (!accessToken) {
document.getElementById('tokenInfo').textContent = '无访问令牌';
return;
}
try {
const parts = accessToken.split('.');
const header = JSON.parse(atob(parts[0]));
const payload = JSON.parse(atob(parts[1]));
const decoded = {
header,
payload: {
...payload,
exp: new Date(payload.exp * 1000).toISOString(),
iat: new Date(payload.iat * 1000).toISOString()
}
};
document.getElementById('tokenInfo').textContent = JSON.stringify(decoded, null, 2);
} catch (error) {
document.getElementById('tokenInfo').textContent = '解析JWT失败: ' + error.message;
}
}
// 清空日志
function clearLog() {
document.getElementById('logArea').textContent = '';
}
// 更新登录状态
function updateLoginState(isLoggedIn) {
if (isLoggedIn) {
document.getElementById('loginSection').classList.add('hidden');
document.getElementById('loggedInSection').classList.remove('hidden');
updateStatus('已登录', 'success');
} else {
document.getElementById('loginSection').classList.remove('hidden');
document.getElementById('loggedInSection').classList.add('hidden');
updateStatus('未登录', 'info');
}
}
// 更新状态显示
function updateStatus(message, type) {
const statusEl = document.getElementById('loginStatus');
statusEl.textContent = message;
statusEl.className = `status ${type}`;
}
// 自动创建用户相关功能
function showUserInfoForm(jwtUserInfo) {
const formHTML = `
<div style="max-width: 500px; margin: 20px auto; padding: 20px; border: 1px solid #ddd; border-radius: 8px; background: white; box-shadow: 0 2px 10px rgba(0,0,0,0.1);">
<h3 style="text-align: center; color: #333;">完善用户信息</h3>
<p style="text-align: center; color: #666;">系统将为您自动创建账户,请填写或确认以下信息:</p>
<form id="userRegistrationForm">
<div style="margin-bottom: 15px;">
<label style="display: block; margin-bottom: 5px;"><strong>用户名</strong> <span style="color: red;">*</span></label>
<input type="text" id="username" value="${jwtUserInfo.username || ''}" required
style="width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box;">
<small style="color: #666;">用于登录的用户名</small>
</div>
<div style="margin-bottom: 15px;">
<label style="display: block; margin-bottom: 5px;"><strong>显示名称</strong></label>
<input type="text" id="displayName" value="${jwtUserInfo.name || jwtUserInfo.username || ''}"
style="width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box;">
<small style="color: #666;">在界面上显示的名称</small>
</div>
<div style="margin-bottom: 15px;">
<label style="display: block; margin-bottom: 5px;"><strong>邮箱地址</strong></label>
<input type="email" id="email" value="${jwtUserInfo.email || ''}"
style="width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box;">
<small style="color: #666;">用于接收通知和找回密码</small>
</div>
<div style="margin-bottom: 15px;">
<label style="display: block; margin-bottom: 5px;"><strong>所属组织</strong></label>
<input type="text" id="group" value="oauth2" readonly
style="width: 100%; padding: 8px; background: #f5f5f5; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box;">
<small style="color: #666;">OAuth2自动创建的用户组</small>
</div>
<div style="margin-bottom: 20px;">
<h4 style="margin-bottom: 10px;">从JWT令牌获取的信息</h4>
<pre style="background: #f8f9fa; padding: 10px; border-radius: 4px; font-size: 12px; max-height: 200px; overflow: auto; border: 1px solid #e9ecef;">
${JSON.stringify(jwtUserInfo, null, 2)}
</pre>
</div>
<div style="text-align: center;">
<button type="submit" style="background: #007bff; color: white; padding: 12px 24px; border: none; border-radius: 4px; cursor: pointer; margin-right: 10px; font-size: 14px;">
创建账户并登录
</button>
<button type="button" onclick="cancelRegistration()" style="background: #6c757d; color: white; padding: 12px 24px; border: none; border-radius: 4px; cursor: pointer; font-size: 14px;">
取消
</button>
</div>
</form>
</div>
`;
document.body.innerHTML = formHTML;
// 绑定表单提交事件
document.getElementById('userRegistrationForm').addEventListener('submit', handleUserRegistration);
}
// 处理用户注册
async function handleUserRegistration(event) {
event.preventDefault();
const formData = {
username: document.getElementById('username').value.trim(),
displayName: document.getElementById('displayName').value.trim(),
email: document.getElementById('email').value.trim(),
group: document.getElementById('group').value,
oauth2Provider: 'oauth2',
oauth2UserId: oauth2Client.getCurrentUser().userId
};
try {
console.log('创建用户:', formData);
// 调用自动创建用户API这里是演示实际需要服务器支持
const response = await fetch(oauth2Client.config.serverUrl + '/api/oauth/auto_create_user', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('access_token')}`
},
body: JSON.stringify(formData)
});
// 模拟成功响应(实际项目中需要服务器实现)
if (!response.ok) {
// 如果API不存在显示模拟成功
console.log('模拟用户创建成功');
localStorage.setItem('user_created', 'true');
localStorage.setItem('user_info', JSON.stringify(formData));
alert('用户创建成功!(这是演示模式,实际需要服务器端实现)');
location.reload();
return;
}
const result = await response.json();
if (result.success) {
console.log('用户创建成功用户ID:', result.user_id);
localStorage.setItem('user_created', 'true');
localStorage.setItem('user_info', JSON.stringify(formData));
location.reload();
} else {
alert('创建用户失败: ' + result.message);
}
} catch (error) {
console.error('用户创建失败:', error);
// 演示模式:模拟成功
localStorage.setItem('user_created', 'true');
localStorage.setItem('user_info', JSON.stringify(formData));
alert('用户创建成功!(演示模式)');
location.reload();
}
}
// 取消注册
function cancelRegistration() {
console.log('用户取消注册');
localStorage.removeItem('access_token');
localStorage.removeItem('refresh_token');
localStorage.removeItem('user_created');
localStorage.removeItem('user_info');
location.reload();
}
// 检查用户是否存在(演示版本)
async function checkUserExists(userId) {
try {
// 演示模式检查localStorage中是否有user_created标记
const userCreated = localStorage.getItem('user_created');
if (userCreated) {
console.log('用户已创建(从本地存储检测到)');
return true;
}
// 实际项目中会调用服务器API
const response = await fetch(`${oauth2Client.config.serverUrl}/api/oauth/user_exists/${userId}`, {
headers: {
'Authorization': `Bearer ${localStorage.getItem('access_token')}`
}
});
if (!response.ok) {
// API不存在时返回false触发用户创建流程
return false;
}
const result = await response.json();
return result.exists;
} catch (error) {
console.error('检查用户存在性失败:', error);
return false;
}
}
// 改进的初始化函数,包含自动创建用户逻辑
async function initAutoLogin() {
try {
console.log('开始自动登录初始化...');
// 1. 检查是否有有效的访问令牌
const accessToken = localStorage.getItem('access_token');
if (!accessToken || !oauth2Client.validateJWTToken(accessToken)) {
console.log('没有有效令牌');
return false;
}
// 2. 解析JWT令牌获取用户信息
const jwtUserInfo = oauth2Client.getCurrentUser();
console.log('JWT用户信息:', jwtUserInfo);
// 3. 检查用户是否已存在于系统中
const userExists = await checkUserExists(jwtUserInfo.userId);
console.log('用户存在检查结果:', userExists);
if (!userExists) {
console.log('用户不存在,显示用户信息收集表单');
showUserInfoForm(jwtUserInfo);
return true;
}
// 4. 用户已存在,显示登录成功界面
console.log('用户已存在,显示登录成功信息');
return true;
} catch (error) {
console.error('自动登录失败:', error);
return false;
}
}
// 页面加载时初始化
document.addEventListener('DOMContentLoaded', function() {
// 设置默认重定向URI
document.getElementById('redirectUri').value = window.location.origin + window.location.pathname;
// 加载保存的配置
loadConfig();
// 检查是否有授权回调
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.get('code')) {
log('检测到授权回调,处理中...');
if (oauth2Client) {
oauth2Client.handleAuthorizationCallback()
.then(async tokens => {
if (tokens.access_token) {
localStorage.setItem('access_token', tokens.access_token);
if (tokens.refresh_token) {
localStorage.setItem('refresh_token', tokens.refresh_token);
}
log('授权回调处理成功,开始自动登录流程...');
// 清除URL中的授权回调参数
const cleanUrl = window.location.origin + window.location.pathname;
window.history.replaceState({}, document.title, cleanUrl);
// 启动自动登录流程
const autoLoginSuccess = await initAutoLogin();
if (autoLoginSuccess) {
updateLoginState(true);
} else {
updateLoginState(false);
}
}
})
.catch(error => {
log('授权回调处理失败: ' + error.message);
updateStatus('授权失败: ' + error.message, 'error');
});
}
} else {
// 没有授权回调,尝试自动登录
setTimeout(async () => {
const autoLoginSuccess = await initAutoLogin();
if (!autoLoginSuccess) {
// 自动登录失败,检查现有令牌状态
checkExistingToken();
}
}, 100);
}
log('OAuth2 Demo 已初始化');
});
</script>
</body>
</html>

870
docs/oauth2-demo.md Normal file
View File

@@ -0,0 +1,870 @@
# OAuth2服务端使用Demo - 自动登录流程
本文档演示如何使用new-api的OAuth2服务器实现自动登录功能包括两种授权模式的完整流程。
## 📋 准备工作
### 1. 启用OAuth2服务器
在管理后台 -> 设置 -> OAuth2 & SSO 中:
```
启用OAuth2服务器: 开启
签发者标识(Issuer): https://your-domain.com
访问令牌有效期: 60分钟
刷新令牌有效期: 24小时
JWT签名算法: RS256
允许的授权类型: client_credentials, authorization_code
```
### 2. 创建OAuth2客户端
在OAuth2客户端管理中创建应用
```
客户端名称: My App
客户端类型: 机密客户端 (Confidential)
授权类型: Client Credentials, Authorization Code
权限范围: api:read, api:write
重定向URI: https://your-app.com/callback
```
创建成功后会获得:
- Client ID: `your_client_id`
- Client Secret: `your_client_secret` (仅显示一次)
## 🔐 方式一:客户端凭证流程 (Client Credentials)
适用于**服务器到服务器**的API调用无需用户交互。
### 获取访问令牌
```bash
curl -X POST https://your-domain.com/api/oauth/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=client_credentials" \
-d "client_id=your_client_id" \
-d "client_secret=your_client_secret" \
-d "scope=api:read api:write"
```
**响应示例:**
```json
{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "api:read api:write"
}
```
### 使用访问令牌调用API
```bash
curl -X GET https://your-domain.com/api/user/self \
-H "Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."
```
## 👤 方式二:授权码流程 (Authorization Code + PKCE)
适用于**用户登录**场景,支持自动登录功能。
### Step 1: 生成PKCE参数
```javascript
// 生成随机code_verifier
function generateCodeVerifier() {
const array = new Uint8Array(32);
crypto.getRandomValues(array);
return btoa(String.fromCharCode.apply(null, array))
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '');
}
// 生成code_challenge
async function generateCodeChallenge(verifier) {
const encoder = new TextEncoder();
const data = encoder.encode(verifier);
const digest = await crypto.subtle.digest('SHA-256', data);
return btoa(String.fromCharCode.apply(null, new Uint8Array(digest)))
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '');
}
```
### Step 2: 重定向用户到授权页面
```javascript
const codeVerifier = generateCodeVerifier();
const codeChallenge = await generateCodeChallenge(codeVerifier);
// 保存code_verifier到本地存储
localStorage.setItem('oauth_code_verifier', codeVerifier);
// 构建授权URL
const authUrl = new URL('https://your-domain.com/api/oauth/authorize');
authUrl.searchParams.set('response_type', 'code');
authUrl.searchParams.set('client_id', 'your_client_id');
authUrl.searchParams.set('redirect_uri', 'https://your-app.com/callback');
authUrl.searchParams.set('scope', 'api:read api:write');
authUrl.searchParams.set('state', 'random_state_value');
authUrl.searchParams.set('code_challenge', codeChallenge);
authUrl.searchParams.set('code_challenge_method', 'S256');
// 重定向到授权页面
window.location.href = authUrl.toString();
```
### Step 3: 处理授权回调
用户授权后会跳转到`https://your-app.com/callback?code=xxx&state=xxx`
```javascript
// 在callback页面处理授权码
const urlParams = new URLSearchParams(window.location.search);
const code = urlParams.get('code');
const state = urlParams.get('state');
const codeVerifier = localStorage.getItem('oauth_code_verifier');
if (code && codeVerifier) {
// 交换访问令牌
const tokenResponse = await fetch('https://your-domain.com/api/oauth/token', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
grant_type: 'authorization_code',
client_id: 'your_client_id',
client_secret: 'your_client_secret',
code: code,
redirect_uri: 'https://your-app.com/callback',
code_verifier: codeVerifier
})
});
const tokens = await tokenResponse.json();
// 解析JWT令牌获取用户信息
const userInfo = parseJWTToken(tokens.access_token);
console.log('用户信息:', userInfo);
// 保存令牌和用户信息
localStorage.setItem('access_token', tokens.access_token);
localStorage.setItem('refresh_token', tokens.refresh_token);
localStorage.setItem('user_info', JSON.stringify(userInfo));
// 清理临时数据
localStorage.removeItem('oauth_code_verifier');
// 跳转到应用首页
window.location.href = '/dashboard';
}
```
### Step 4: JWT令牌解析和用户信息获取
授权码流程返回的`access_token`是一个JWT令牌包含用户信息
```javascript
// JWT令牌解析函数
function parseJWTToken(token) {
try {
// JWT格式: header.payload.signature
const parts = token.split('.');
if (parts.length !== 3) {
throw new Error('Invalid JWT token format');
}
// 解码payload部分
const payload = JSON.parse(atob(parts[1]));
// 提取用户信息
return {
userId: payload.sub, // 用户ID
username: payload.preferred_username || payload.sub,
email: payload.email, // 用户邮箱
name: payload.name, // 用户姓名
roles: payload.scope?.split(' ') || [], // 权限范围
groups: payload.groups || [], // 用户组
exp: payload.exp, // 过期时间
iat: payload.iat, // 签发时间
iss: payload.iss, // 签发者
aud: payload.aud // 受众
};
} catch (error) {
console.error('Failed to parse JWT token:', error);
return null;
}
}
// JWT令牌验证函数
function validateJWTToken(token) {
const userInfo = parseJWTToken(token);
if (!userInfo) return false;
// 检查令牌是否过期
const now = Math.floor(Date.now() / 1000);
if (userInfo.exp && now >= userInfo.exp) {
console.log('JWT token has expired');
return false;
}
return true;
}
// 获取用户信息示例
async function getUserInfoFromToken() {
const token = localStorage.getItem('access_token');
if (!token) return null;
if (!validateJWTToken(token)) {
// 令牌无效或过期,尝试刷新
const newToken = await refreshToken();
if (newToken) {
return parseJWTToken(newToken);
}
return null;
}
return parseJWTToken(token);
}
```
**JWT令牌示例内容:**
```json
{
"sub": "user123", // 用户唯一标识
"preferred_username": "john_doe", // 用户名
"email": "john@example.com", // 邮箱
"name": "John Doe", // 真实姓名
"scope": "api:read api:write", // 权限范围
"groups": ["users", "developers"], // 用户组
"iss": "https://your-domain.com", // 签发者
"aud": "your_client_id", // 受众
"exp": 1609459200, // 过期时间戳
"iat": 1609455600, // 签发时间戳
"jti": "token-unique-id" // 令牌唯一ID
}
```
## 👤 自动创建用户登录流程
### 用户信息收集和自动创建
当启用了`AutoCreateUser`选项时用户首次通过OAuth2授权后会自动创建账户
```javascript
// 用户信息收集表单
function showUserInfoForm(jwtUserInfo) {
const formHTML = `
<div id="userInfoForm" style="max-width: 400px; margin: 20px auto; padding: 20px; border: 1px solid #ddd; border-radius: 8px;">
<h3>完善用户信息</h3>
<p>系统将为您自动创建账户,请填写或确认以下信息:</p>
<form id="userRegistrationForm">
<div style="margin-bottom: 15px;">
<label>用户名 <span style="color: red;">*</span></label>
<input type="text" id="username" value="${jwtUserInfo.username || ''}" required
style="width: 100%; padding: 8px; margin-top: 5px;">
<small>用于登录的用户名</small>
</div>
<div style="margin-bottom: 15px;">
<label>显示名称</label>
<input type="text" id="displayName" value="${jwtUserInfo.name || jwtUserInfo.username || ''}"
style="width: 100%; padding: 8px; margin-top: 5px;">
<small>在界面上显示的名称</small>
</div>
<div style="margin-bottom: 15px;">
<label>邮箱地址</label>
<input type="email" id="email" value="${jwtUserInfo.email || ''}"
style="width: 100%; padding: 8px; margin-top: 5px;">
<small>用于接收通知和找回密码</small>
</div>
<div style="margin-bottom: 15px;">
<label>所属组织</label>
<input type="text" id="group" value="oauth2" readonly
style="width: 100%; padding: 8px; margin-top: 5px; background: #f5f5f5;">
<small>OAuth2自动创建的用户组</small>
</div>
<div style="margin-bottom: 20px;">
<h4>从OAuth2提供商获取的信息</h4>
<pre style="background: #f8f9fa; padding: 10px; border-radius: 4px; font-size: 12px;">
${JSON.stringify(jwtUserInfo, null, 2)}
</pre>
</div>
<button type="submit" style="background: #007bff; color: white; padding: 10px 20px; border: none; border-radius: 4px; cursor: pointer;">
创建账户并登录
</button>
<button type="button" onclick="cancelRegistration()" style="background: #6c757d; color: white; padding: 10px 20px; border: none; border-radius: 4px; cursor: pointer; margin-left: 10px;">
取消
</button>
</form>
</div>
`;
document.body.innerHTML = formHTML;
// 绑定表单提交事件
document.getElementById('userRegistrationForm').addEventListener('submit', handleUserRegistration);
}
// 处理用户注册
async function handleUserRegistration(event) {
event.preventDefault();
const formData = {
username: document.getElementById('username').value.trim(),
displayName: document.getElementById('displayName').value.trim(),
email: document.getElementById('email').value.trim(),
group: document.getElementById('group').value,
oauth2Provider: 'oauth2',
oauth2UserId: parseJWTToken(localStorage.getItem('access_token')).userId
};
try {
// 调用自动创建用户API
const response = await fetch('https://your-domain.com/api/oauth/auto_create_user', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('access_token')}`
},
body: JSON.stringify(formData)
});
const result = await response.json();
if (result.success) {
// 用户创建成功,跳转到主界面
localStorage.setItem('user_created', 'true');
window.location.href = '/dashboard';
} else {
alert('创建用户失败: ' + result.message);
}
} catch (error) {
console.error('用户创建失败:', error);
alert('创建用户时发生错误,请重试');
}
}
// 取消注册
function cancelRegistration() {
localStorage.removeItem('access_token');
localStorage.removeItem('refresh_token');
window.location.href = '/';
}
```
### 完整的自动登录流程
```javascript
// 改进的自动登录初始化
async function initAutoLogin() {
try {
// 1. 检查是否有有效的访问令牌
const accessToken = localStorage.getItem('access_token');
if (!accessToken || !validateJWTToken(accessToken)) {
// 没有有效令牌开始OAuth2授权流程
startOAuth2Authorization();
return;
}
// 2. 解析JWT令牌获取用户信息
const jwtUserInfo = parseJWTToken(accessToken);
console.log('JWT用户信息:', jwtUserInfo);
// 3. 检查用户是否已存在于系统中
const userExists = await checkUserExists(jwtUserInfo.userId);
if (!userExists && !localStorage.getItem('user_created')) {
// 4. 用户不存在且未创建,显示用户信息收集表单
showUserInfoForm(jwtUserInfo);
return;
}
// 5. 用户已存在或已创建,直接登录
const apiUserInfo = await oauth2Client.callAPI('/api/user/self');
console.log('API用户信息:', apiUserInfo);
// 6. 显示主界面
showDashboard(jwtUserInfo, apiUserInfo);
} catch (error) {
console.error('自动登录失败:', error);
// 清理令牌并重新开始授权流程
localStorage.removeItem('access_token');
localStorage.removeItem('refresh_token');
localStorage.removeItem('user_created');
startOAuth2Authorization();
}
}
// 检查用户是否存在
async function checkUserExists(userId) {
try {
const response = await fetch(`https://your-domain.com/api/oauth/user_exists/${userId}`, {
headers: {
'Authorization': `Bearer ${localStorage.getItem('access_token')}`
}
});
const result = await response.json();
return result.exists;
} catch (error) {
console.error('检查用户存在性失败:', error);
return false;
}
}
// 开始OAuth2授权流程
function startOAuth2Authorization() {
const oauth2Client = new OAuth2Client({
clientId: 'your_client_id',
clientSecret: 'your_client_secret',
serverUrl: 'https://your-domain.com',
redirectUri: window.location.origin + '/callback',
scopes: 'api:read api:write'
});
oauth2Client.startAuthorizationCodeFlow();
}
```
### 服务器端自动创建用户API
需要在服务器端实现相应的API端点
```go
// 用户存在性检查
func CheckUserExists(c *gin.Context) {
oauthUserId := c.Param("oauth_user_id")
var user model.User
err := model.DB.Where("oauth2_user_id = ?", oauthUserId).First(&user).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
c.JSON(http.StatusOK, gin.H{
"exists": false,
})
} else if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Database error",
})
} else {
c.JSON(http.StatusOK, gin.H{
"exists": true,
"user_id": user.Id,
})
}
}
// 自动创建用户
func AutoCreateUser(c *gin.Context) {
settings := system_setting.GetOAuth2Settings()
if !settings.AutoCreateUser {
c.JSON(http.StatusForbidden, gin.H{
"success": false,
"message": "自动创建用户功能未启用",
})
return
}
var req struct {
Username string `json:"username" binding:"required"`
DisplayName string `json:"displayName"`
Email string `json:"email"`
Group string `json:"group"`
OAuth2UserId string `json:"oauth2UserId" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"success": false,
"message": "无效的请求参数",
})
return
}
// 检查用户是否已存在
var existingUser model.User
err := model.DB.Where("username = ? OR oauth2_user_id = ?", req.Username, req.OAuth2UserId).First(&existingUser).Error
if err == nil {
c.JSON(http.StatusConflict, gin.H{
"success": false,
"message": "用户已存在",
})
return
}
// 创建新用户
user := model.User{
Username: req.Username,
DisplayName: req.DisplayName,
Email: req.Email,
Group: settings.DefaultUserGroup,
Role: settings.DefaultUserRole,
Status: 1,
Password: common.GenerateRandomString(32), // 随机密码用户通过OAuth2登录
OAuth2UserId: req.OAuth2UserId,
}
if req.DisplayName == "" {
user.DisplayName = req.Username
}
if user.Group == "" {
user.Group = "oauth2"
}
err = user.Insert(0)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"success": false,
"message": "创建用户失败: " + err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "用户创建成功",
"user_id": user.Id,
})
}
```
## 🔄 自动登录实现
### 令牌刷新机制
```javascript
async function refreshToken() {
const refreshToken = localStorage.getItem('refresh_token');
if (!refreshToken) {
// 重新授权
redirectToAuth();
return;
}
try {
const response = await fetch('https://your-domain.com/api/oauth/token', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
grant_type: 'refresh_token',
client_id: 'your_client_id',
client_secret: 'your_client_secret',
refresh_token: refreshToken
})
});
const tokens = await response.json();
if (tokens.access_token) {
localStorage.setItem('access_token', tokens.access_token);
if (tokens.refresh_token) {
localStorage.setItem('refresh_token', tokens.refresh_token);
}
return tokens.access_token;
}
} catch (error) {
// 刷新失败,重新授权
redirectToAuth();
}
}
```
### 自动认证拦截器
```javascript
class OAuth2Client {
constructor(clientId, clientSecret, baseURL) {
this.clientId = clientId;
this.clientSecret = clientSecret;
this.baseURL = baseURL;
}
// 自动处理认证的请求方法
async request(url, options = {}) {
let accessToken = localStorage.getItem('access_token');
// 检查令牌是否即将过期
if (this.isTokenExpiringSoon(accessToken)) {
accessToken = await this.refreshToken();
}
// 添加认证头
const headers = {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json',
...options.headers
};
try {
const response = await fetch(`${this.baseURL}${url}`, {
...options,
headers
});
// 如果401尝试刷新令牌
if (response.status === 401) {
accessToken = await this.refreshToken();
headers['Authorization'] = `Bearer ${accessToken}`;
// 重试请求
return fetch(`${this.baseURL}${url}`, {
...options,
headers
});
}
return response;
} catch (error) {
console.error('Request failed:', error);
throw error;
}
}
// 检查令牌是否即将过期
isTokenExpiringSoon(token) {
if (!token) return true;
try {
const parts = token.split('.');
if (parts.length !== 3) return true;
const payload = JSON.parse(atob(parts[1]));
const exp = payload.exp * 1000; // 转换为毫秒
const now = Date.now();
return exp - now < 5 * 60 * 1000; // 5分钟内过期
} catch (error) {
console.error('Token validation failed:', error);
return true;
}
}
// 获取当前用户信息
getCurrentUser() {
const token = localStorage.getItem('access_token');
if (!token || !this.validateJWTToken(token)) {
return null;
}
return this.parseJWTToken(token);
}
// 解析JWT令牌
parseJWTToken(token) {
try {
const parts = token.split('.');
if (parts.length !== 3) {
throw new Error('Invalid JWT token format');
}
const payload = JSON.parse(atob(parts[1]));
return {
userId: payload.sub,
username: payload.preferred_username || payload.sub,
email: payload.email,
name: payload.name,
roles: payload.scope?.split(' ') || [],
groups: payload.groups || [],
exp: payload.exp,
iat: payload.iat,
iss: payload.iss,
aud: payload.aud
};
} catch (error) {
console.error('Failed to parse JWT token:', error);
return null;
}
}
// 验证JWT令牌
validateJWTToken(token) {
const userInfo = this.parseJWTToken(token);
if (!userInfo) return false;
const now = Math.floor(Date.now() / 1000);
if (userInfo.exp && now >= userInfo.exp) {
return false;
}
return true;
}
// 获取用户信息
async getUserInfo() {
const response = await this.request('/api/user/self');
return response.json();
}
// 调用API示例
async callAPI(endpoint, data = null) {
const options = data ? {
method: 'POST',
body: JSON.stringify(data)
} : { method: 'GET' };
const response = await this.request(endpoint, options);
return response.json();
}
}
```
### 使用示例
```javascript
// 初始化OAuth2客户端
const oauth2Client = new OAuth2Client(
'your_client_id',
'your_client_secret',
'https://your-domain.com'
);
// 应用启动时自动检查登录状态
async function initApp() {
try {
// 尝试获取用户信息(会自动处理令牌刷新)
const userInfo = await oauth2Client.getUserInfo();
console.log('User logged in:', userInfo);
// 显示用户界面
showDashboard(userInfo);
} catch (error) {
// 用户未登录,重定向到授权页面
redirectToAuth();
}
}
// 页面加载时初始化
document.addEventListener('DOMContentLoaded', initApp);
```
## 🛡️ 安全最佳实践
### 1. HTTPS 必需
```
生产环境必须使用HTTPS
重定向URI必须使用https://本地开发可用http://localhost
```
### 2. 状态参数验证
```javascript
// 发起授权时
const state = crypto.randomUUID();
localStorage.setItem('oauth_state', state);
// 回调时验证
const returnedState = urlParams.get('state');
const savedState = localStorage.getItem('oauth_state');
if (returnedState !== savedState) {
throw new Error('State mismatch - possible CSRF attack');
}
```
### 3. 令牌安全存储
```javascript
// 使用HttpOnly Cookie推荐
// 或加密存储在localStorage
function secureStorage() {
return {
setItem: (key, value) => {
const encrypted = encrypt(value); // 使用加密
localStorage.setItem(key, encrypted);
},
getItem: (key) => {
const encrypted = localStorage.getItem(key);
return encrypted ? decrypt(encrypted) : null;
}
};
}
```
## 📚 完整示例项目
创建一个完整的单页应用示例:
```html
<!DOCTYPE html>
<html>
<head>
<title>OAuth2 Demo</title>
</head>
<body>
<div id="login-section">
<h1>请登录</h1>
<button onclick="login()">使用OAuth2登录</button>
</div>
<div id="app-section" style="display:none">
<h1>欢迎!</h1>
<div id="user-info"></div>
<button onclick="logout()">登出</button>
<button onclick="testAPI()">测试API调用</button>
</div>
<script>
// 这里包含上面的所有OAuth2Client代码
const oauth2Client = new OAuth2Client(
'your_client_id',
'your_client_secret',
'https://your-domain.com'
);
async function login() {
// 实现授权码流程...
}
async function logout() {
localStorage.clear();
location.reload();
}
async function testAPI() {
try {
const result = await oauth2Client.callAPI('/api/user/self');
alert('API调用成功: ' + JSON.stringify(result));
} catch (error) {
alert('API调用失败: ' + error.message);
}
}
// 初始化应用
initApp();
</script>
</body>
</html>
```
## 🔍 调试和测试
### 验证JWT令牌
访问 [jwt.io](https://jwt.io) 解析令牌内容:
```
Header: {"alg":"RS256","typ":"JWT","kid":"oauth2-key-1"}
Payload: {"sub":"user_id","aud":"your_client_id","exp":1234567890}
```
### 查看服务器信息
```bash
curl https://your-domain.com/.well-known/oauth-authorization-server
```
### 获取JWKS公钥
```bash
curl https://your-domain.com/.well-known/jwks.json
```
---
这个demo涵盖了OAuth2服务器的完整使用流程实现了真正的自动登录功能。用户只需要第一次授权之后应用会自动处理令牌刷新和API认证。

258
docs/oauth2_setup.md Normal file
View File

@@ -0,0 +1,258 @@
# OAuth2 服务器设置指南
## 概述
该 OAuth2 服务器实现基于 RFC 6749 标准,支持以下特性:
- **授权类型**: Client Credentials, Authorization Code + PKCE, Refresh Token
- **JWT 访问令牌**: 使用 RS256 签名
- **JWKS 端点**: 公钥自动发布和轮换
- **兼容性**: 与现有认证系统完全兼容
## 配置
### 1. 环境变量配置
`.env` 文件中添加以下配置:
```env
# OAuth2 基础配置
OAUTH2_ENABLED=true
OAUTH2_ISSUER=https://your-domain.com
OAUTH2_ACCESS_TOKEN_TTL=10
OAUTH2_REFRESH_TOKEN_TTL=720
# JWT 签名配置
JWT_SIGNING_ALGORITHM=RS256
JWT_KEY_ID=oauth2-key-1
JWT_PRIVATE_KEY_FILE=/path/to/private-key.pem
# 授权类型(逗号分隔)
OAUTH2_ALLOWED_GRANT_TYPES=client_credentials,authorization_code,refresh_token
# 强制 PKCE
OAUTH2_REQUIRE_PKCE=true
# 自动创建用户
OAUTH2_AUTO_CREATE_USER=false
OAUTH2_DEFAULT_USER_ROLE=1
OAUTH2_DEFAULT_USER_GROUP=default
```
### 2. 数据库迁移
重启应用程序将自动创建 `oauth_clients` 表。
### 3. 创建第一个 OAuth2 客户端
通过管理员界面或 API 创建客户端:
```bash
curl -X POST http://localhost:8080/api/oauth_clients \
-H "Authorization: Bearer YOUR_ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "测试服务",
"client_type": "confidential",
"grant_types": ["client_credentials"],
"scopes": ["api:read", "api:write"],
"description": "用于服务对服务认证的测试客户端"
}'
```
## OAuth2 端点
### 标准端点
- **令牌端点**: `POST /api/oauth/token`
- **授权端点**: `GET /api/oauth/authorize`
- **JWKS 端点**: `GET /.well-known/jwks.json`
- **服务器信息**: `GET /.well-known/oauth-authorization-server`
### 管理端点
- **令牌内省**: `POST /api/oauth/introspect` (需要管理员权限)
- **令牌撤销**: `POST /api/oauth/revoke`
### 客户端管理端点
- **列出客户端**: `GET /api/oauth_clients`
- **创建客户端**: `POST /api/oauth_clients`
- **更新客户端**: `PUT /api/oauth_clients`
- **删除客户端**: `DELETE /api/oauth_clients/{id}`
- **重新生成密钥**: `POST /api/oauth_clients/{id}/regenerate_secret`
## 使用示例
### 1. Client Credentials 流程
```go
package main
import (
"context"
"golang.org/x/oauth2/clientcredentials"
)
func main() {
cfg := clientcredentials.Config{
ClientID: "your_client_id",
ClientSecret: "your_client_secret",
TokenURL: "https://your-domain.com/api/oauth/token",
Scopes: []string{"api:read"},
}
client := cfg.Client(context.Background())
resp, _ := client.Get("https://your-domain.com/api/protected")
// 处理响应...
}
```
### 2. Authorization Code + PKCE 流程
```go
package main
import (
"context"
"golang.org/x/oauth2"
)
func main() {
conf := oauth2.Config{
ClientID: "your_web_client_id",
ClientSecret: "your_web_client_secret",
RedirectURL: "https://your-app.com/callback",
Scopes: []string{"api:read"},
Endpoint: oauth2.Endpoint{
AuthURL: "https://your-domain.com/api/oauth/authorize",
TokenURL: "https://your-domain.com/api/oauth/token",
},
}
// 生成 PKCE 参数
verifier := oauth2.GenerateVerifier()
// 构建授权 URL
url := conf.AuthCodeURL("state", oauth2.S256ChallengeOption(verifier))
// 用户授权后,使用授权码交换令牌
token, _ := conf.Exchange(context.Background(), code, oauth2.VerifierOption(verifier))
// 使用令牌调用 API
client := conf.Client(context.Background(), token)
resp, _ := client.Get("https://your-domain.com/api/protected")
}
```
### 3. cURL 示例
```bash
# 获取访问令牌
curl -X POST https://your-domain.com/api/oauth/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-u "client_id:client_secret" \
-d "grant_type=client_credentials&scope=api:read"
# 使用访问令牌调用 API
curl -H "Authorization: Bearer ACCESS_TOKEN" \
https://your-domain.com/api/status
```
## 安全建议
### 1. 密钥管理
- 使用强随机密钥生成器
- 定期轮换 RSA 密钥对
- 将私钥存储在安全位置
- 考虑使用 HSM 或密钥管理服务
### 2. 网络安全
- 强制使用 HTTPS
- 配置适当的 CORS 策略
- 实现速率限制
- 启用请求日志和监控
### 3. 客户端管理
- 定期审查客户端列表
- 撤销不再使用的客户端
- 监控客户端使用情况
- 为不同用途创建不同的客户端
### 4. Scope 和权限
- 实施最小权限原则
- 定期审查 scope 定义
- 为敏感操作创建特殊 scope
- 实现细粒度的权限控制
## 故障排除
### 常见问题
1. **"OAuth2 server is disabled"**
- 确保 `OAUTH2_ENABLED=true`
- 检查配置文件是否正确加载
2. **"invalid_client"**
- 验证 client_id 和 client_secret
- 确保客户端状态为启用
3. **"invalid_grant"**
- 检查授权类型是否被允许
- 验证 PKCE 参数(如果启用)
4. **"invalid_scope"**
- 确保请求的 scope 在客户端配置中
- 检查 scope 格式(空格分隔)
### 调试
启用详细日志:
```env
GIN_MODE=debug
LOG_LEVEL=debug
```
检查 JWKS 端点:
```bash
curl https://your-domain.com/.well-known/jwks.json
```
验证令牌:
```bash
# 可以使用 https://jwt.io 解码和验证 JWT 令牌
```
## 生产部署
### 1. 负载均衡
- OAuth2 服务器是无状态的,支持水平扩展
- 确保所有实例使用相同的私钥
- 使用 Redis 作为令牌存储
### 2. 监控
- 监控令牌签发速率
- 跟踪客户端使用情况
- 设置异常告警
### 3. 备份
- 备份私钥文件
- 备份客户端配置数据
- 制定灾难恢复计划
### 4. 性能优化
- 启用 JWKS 缓存
- 使用连接池
- 优化数据库查询
- 考虑使用 CDN 分发 JWKS