2025-07-18 17:08:09 +08:00
|
|
|
|
<!DOCTYPE html>
|
|
|
|
|
|
<html lang="zh-CN">
|
|
|
|
|
|
<head>
|
|
|
|
|
|
<meta charset="UTF-8">
|
|
|
|
|
|
<title>系统B - OIDC登录</title>
|
|
|
|
|
|
<style>
|
|
|
|
|
|
body {
|
|
|
|
|
|
font-family: Arial, sans-serif;
|
|
|
|
|
|
max-width: 800px;
|
|
|
|
|
|
margin: 0 auto;
|
|
|
|
|
|
padding: 20px;
|
|
|
|
|
|
background-color: #f5f5f5;
|
|
|
|
|
|
}
|
|
|
|
|
|
.container {
|
|
|
|
|
|
background: white;
|
|
|
|
|
|
padding: 30px;
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
|
|
|
|
|
}
|
|
|
|
|
|
.status {
|
|
|
|
|
|
padding: 15px;
|
|
|
|
|
|
margin: 10px 0;
|
|
|
|
|
|
border-radius: 5px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.success { background-color: #d4edda; color: #155724; border: 1px solid #c3e6cb; }
|
|
|
|
|
|
.error { background-color: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; }
|
|
|
|
|
|
.info { background-color: #d1ecf1; color: #0c5460; border: 1px solid #bee5eb; }
|
|
|
|
|
|
button {
|
|
|
|
|
|
background-color: #007bff;
|
|
|
|
|
|
color: white;
|
|
|
|
|
|
border: none;
|
|
|
|
|
|
padding: 10px 20px;
|
|
|
|
|
|
border-radius: 5px;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
margin: 5px;
|
|
|
|
|
|
}
|
|
|
|
|
|
button:hover { background-color: #0056b3; }
|
|
|
|
|
|
.logout { background-color: #dc3545; }
|
|
|
|
|
|
.logout:hover { background-color: #c82333; }
|
|
|
|
|
|
.api-result {
|
|
|
|
|
|
background-color: #f8f9fa;
|
|
|
|
|
|
padding: 15px;
|
|
|
|
|
|
border-radius: 5px;
|
|
|
|
|
|
margin: 10px 0;
|
|
|
|
|
|
white-space: pre-wrap;
|
|
|
|
|
|
}
|
|
|
|
|
|
</style>
|
|
|
|
|
|
</head>
|
|
|
|
|
|
<body>
|
|
|
|
|
|
<div class="container">
|
|
|
|
|
|
<h1>系统B - OIDC登录</h1>
|
|
|
|
|
|
|
|
|
|
|
|
<div id="status" class="status info">
|
|
|
|
|
|
正在检查登录状态...
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div id="userInfo" style="display: none;">
|
|
|
|
|
|
<h2>用户信息</h2>
|
|
|
|
|
|
<div id="userDetails"></div>
|
|
|
|
|
|
<button onclick="callApi()">调用API</button>
|
|
|
|
|
|
<button class="logout" onclick="logout()">退出登录</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div id="loginSection" style="display: none;">
|
|
|
|
|
|
<h2>请登录</h2>
|
|
|
|
|
|
<button onclick="login()">登录到OIDC</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div id="apiResult" class="api-result" style="display: none;"></div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<script>
|
|
|
|
|
|
// OIDC配置
|
|
|
|
|
|
const oidcConfig = {
|
|
|
|
|
|
clientId: 'b-client',
|
|
|
|
|
|
clientSecret: 'b-secret',
|
|
|
|
|
|
redirectUri: 'https://b.local.com/callback',
|
|
|
|
|
|
authorizationEndpoint: 'https://oidc.local.com/oauth2/authorize',
|
|
|
|
|
|
tokenEndpoint: 'https://oidc.local.com/oauth2/token',
|
|
|
|
|
|
userInfoEndpoint: 'https://oidc.local.com/userinfo',
|
|
|
|
|
|
scope: 'openid read'
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 检查URL参数中的授权码
|
|
|
|
|
|
const urlParams = new URLSearchParams(window.location.search);
|
|
|
|
|
|
const code = urlParams.get('code');
|
|
|
|
|
|
const state = urlParams.get('state');
|
|
|
|
|
|
|
|
|
|
|
|
// 页面加载时执行
|
|
|
|
|
|
window.onload = function() {
|
|
|
|
|
|
if (code) {
|
|
|
|
|
|
// 有授权码,处理回调
|
|
|
|
|
|
handleCallback(code, state);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 检查现有token
|
|
|
|
|
|
checkAuthStatus();
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 检查认证状态
|
|
|
|
|
|
function checkAuthStatus() {
|
|
|
|
|
|
const token = localStorage.getItem('access_token');
|
|
|
|
|
|
if (token) {
|
|
|
|
|
|
// 验证token是否有效
|
|
|
|
|
|
validateToken(token);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
showLoginSection();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 验证token
|
|
|
|
|
|
function validateToken(token) {
|
|
|
|
|
|
fetch('https://oidc.local.com/userinfo', {
|
|
|
|
|
|
headers: {
|
|
|
|
|
|
'Authorization': `Bearer ${token}`
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
.then(response => {
|
|
|
|
|
|
if (response.ok) {
|
|
|
|
|
|
return response.json();
|
|
|
|
|
|
} else {
|
|
|
|
|
|
throw new Error('Token无效');
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
.then(userInfo => {
|
|
|
|
|
|
showUserInfo(userInfo);
|
|
|
|
|
|
})
|
|
|
|
|
|
.catch(error => {
|
|
|
|
|
|
console.error('Token验证失败:', error);
|
|
|
|
|
|
localStorage.removeItem('access_token');
|
|
|
|
|
|
localStorage.removeItem('refresh_token');
|
|
|
|
|
|
showLoginSection();
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 显示用户信息
|
|
|
|
|
|
function showUserInfo(userInfo) {
|
|
|
|
|
|
document.getElementById('status').className = 'status success';
|
|
|
|
|
|
document.getElementById('status').textContent = '登录成功!';
|
|
|
|
|
|
|
|
|
|
|
|
document.getElementById('userInfo').style.display = 'block';
|
|
|
|
|
|
document.getElementById('loginSection').style.display = 'none';
|
|
|
|
|
|
|
|
|
|
|
|
document.getElementById('userDetails').innerHTML = `
|
|
|
|
|
|
<p><strong>用户名:</strong> ${userInfo.sub || 'user'}</p>
|
|
|
|
|
|
<p><strong>Token:</strong> ${localStorage.getItem('access_token').substring(0, 50)}...</p>
|
|
|
|
|
|
`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 显示登录部分
|
|
|
|
|
|
function showLoginSection() {
|
|
|
|
|
|
document.getElementById('status').className = 'status info';
|
|
|
|
|
|
document.getElementById('status').textContent = '请登录以继续';
|
|
|
|
|
|
|
|
|
|
|
|
document.getElementById('userInfo').style.display = 'none';
|
|
|
|
|
|
document.getElementById('loginSection').style.display = 'block';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 登录
|
|
|
|
|
|
function login() {
|
|
|
|
|
|
const state = generateRandomString();
|
|
|
|
|
|
localStorage.setItem('oauth_state', state);
|
|
|
|
|
|
|
|
|
|
|
|
const authUrl = new URL(oidcConfig.authorizationEndpoint);
|
|
|
|
|
|
authUrl.searchParams.set('response_type', 'code');
|
|
|
|
|
|
authUrl.searchParams.set('client_id', oidcConfig.clientId);
|
|
|
|
|
|
authUrl.searchParams.set('redirect_uri', oidcConfig.redirectUri);
|
|
|
|
|
|
authUrl.searchParams.set('scope', oidcConfig.scope);
|
|
|
|
|
|
authUrl.searchParams.set('state', state);
|
|
|
|
|
|
|
2025-07-19 14:00:11 +08:00
|
|
|
|
window.location.href = authUrl.toString();
|
2025-07-18 17:08:09 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 处理回调
|
|
|
|
|
|
function handleCallback(code, state) {
|
|
|
|
|
|
const savedState = localStorage.getItem('oauth_state');
|
|
|
|
|
|
if (state !== savedState) {
|
|
|
|
|
|
document.getElementById('status').className = 'status error';
|
|
|
|
|
|
document.getElementById('status').textContent = '状态验证失败';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 交换授权码为token
|
|
|
|
|
|
exchangeCodeForToken(code);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 交换授权码为token
|
|
|
|
|
|
function exchangeCodeForToken(code) {
|
|
|
|
|
|
const tokenData = new URLSearchParams();
|
|
|
|
|
|
tokenData.append('grant_type', 'authorization_code');
|
|
|
|
|
|
tokenData.append('code', code);
|
|
|
|
|
|
tokenData.append('redirect_uri', oidcConfig.redirectUri);
|
|
|
|
|
|
|
|
|
|
|
|
// 使用Basic认证
|
|
|
|
|
|
const credentials = btoa(oidcConfig.clientId + ':' + oidcConfig.clientSecret);
|
|
|
|
|
|
|
|
|
|
|
|
fetch(oidcConfig.tokenEndpoint, {
|
|
|
|
|
|
method: 'POST',
|
|
|
|
|
|
headers: {
|
|
|
|
|
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
|
|
|
|
'Authorization': 'Basic ' + credentials
|
|
|
|
|
|
},
|
|
|
|
|
|
body: tokenData
|
|
|
|
|
|
})
|
|
|
|
|
|
.then(response => response.json())
|
|
|
|
|
|
.then(data => {
|
|
|
|
|
|
if (data.access_token) {
|
|
|
|
|
|
localStorage.setItem('access_token', data.access_token);
|
|
|
|
|
|
if (data.refresh_token) {
|
|
|
|
|
|
localStorage.setItem('refresh_token', data.refresh_token);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 获取用户信息
|
|
|
|
|
|
return fetch(oidcConfig.userInfoEndpoint, {
|
|
|
|
|
|
headers: {
|
|
|
|
|
|
'Authorization': `Bearer ${data.access_token}`
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
} else {
|
|
|
|
|
|
throw new Error('获取token失败: ' + JSON.stringify(data));
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
.then(response => response.json())
|
|
|
|
|
|
.then(userInfo => {
|
|
|
|
|
|
// 清除URL中的参数
|
|
|
|
|
|
window.history.replaceState({}, document.title, window.location.pathname);
|
|
|
|
|
|
showUserInfo(userInfo);
|
|
|
|
|
|
})
|
|
|
|
|
|
.catch(error => {
|
|
|
|
|
|
console.error('Token交换失败:', error);
|
|
|
|
|
|
document.getElementById('status').className = 'status error';
|
|
|
|
|
|
document.getElementById('status').textContent = '登录失败: ' + error.message;
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 调用API
|
|
|
|
|
|
function callApi() {
|
|
|
|
|
|
const token = localStorage.getItem('access_token');
|
|
|
|
|
|
if (!token) {
|
|
|
|
|
|
alert('请先登录');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
document.getElementById('apiResult').style.display = 'block';
|
|
|
|
|
|
document.getElementById('apiResult').textContent = '正在调用API...';
|
|
|
|
|
|
|
|
|
|
|
|
fetch('/api/hello', {
|
|
|
|
|
|
headers: {
|
|
|
|
|
|
'Authorization': `Bearer ${token}`
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
.then(response => {
|
|
|
|
|
|
if (response.ok) {
|
|
|
|
|
|
return response.json();
|
|
|
|
|
|
} else {
|
|
|
|
|
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
.then(data => {
|
|
|
|
|
|
document.getElementById('apiResult').textContent = 'API调用成功:\n' + JSON.stringify(data, null, 2);
|
|
|
|
|
|
})
|
|
|
|
|
|
.catch(error => {
|
|
|
|
|
|
document.getElementById('apiResult').textContent = 'API调用失败:\n' + error.message;
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 退出登录
|
|
|
|
|
|
function logout() {
|
|
|
|
|
|
// 清理本地token
|
|
|
|
|
|
localStorage.removeItem('access_token');
|
|
|
|
|
|
localStorage.removeItem('refresh_token');
|
|
|
|
|
|
localStorage.removeItem('oauth_state');
|
|
|
|
|
|
// 获取id_token(如果有)
|
|
|
|
|
|
const idToken = localStorage.getItem('id_token'); // 登录时保存id_token
|
|
|
|
|
|
// 退出后跳转到首页
|
|
|
|
|
|
const redirectUri = encodeURIComponent('https://b.local.com');
|
|
|
|
|
|
// 拼接logout url
|
|
|
|
|
|
let logoutUrl = `https://b.local.com/oidc-logout?post_logout_redirect_uri=${redirectUri}`;
|
|
|
|
|
|
if (idToken) {
|
|
|
|
|
|
logoutUrl += `&id_token_hint=${idToken}`;
|
|
|
|
|
|
}
|
|
|
|
|
|
// 跳转到OIDC logout端点
|
|
|
|
|
|
window.location.href = logoutUrl;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 生成随机字符串
|
|
|
|
|
|
function generateRandomString() {
|
|
|
|
|
|
const array = new Uint32Array(28);
|
|
|
|
|
|
window.crypto.getRandomValues(array);
|
|
|
|
|
|
return Array.from(array, dec => ('0' + dec.toString(16)).substr(-2)).join('');
|
|
|
|
|
|
}
|
|
|
|
|
|
</script>
|
|
|
|
|
|
</body>
|
|
|
|
|
|
</html>
|