Skip to content

Commit 9abf256

Browse files
PythonSmall-QCopilot
andcommitted
添加统一认证中心登录功能,并更新相关页面和逻辑
Co-authored-by: Copilot <copilot@github.com>
1 parent e410e24 commit 9abf256

2 files changed

Lines changed: 116 additions & 1 deletion

File tree

index.html

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,9 @@
5555
<li class="nav-item">
5656
<a class="nav-link" href="https://elxmoj.xmoj-bbs.me" target="_blank" rel="noopener noreferrer">ELXMOJ 客户端</a>
5757
</li>
58+
<li class="nav-item">
59+
<a class="nav-link" href="https://sso.xmoj-bbs.me" target="_blank" rel="noopener noreferrer">统一认证中心</a>
60+
</li>
5861
</ul>
5962
</div>
6063
</div>

messages.html

Lines changed: 113 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,9 @@ <h4 class="mb-1">登录</h4>
7373
<li class="nav-item">
7474
<button class="nav-link" data-tab="bookmarklet">书签登录(桌面端)</button>
7575
</li>
76+
<li class="nav-item">
77+
<button class="nav-link" data-tab="sso">统一认证登录</button>
78+
</li>
7679
</ul>
7780

7881
<!-- Tab: Manual (default) -->
@@ -216,6 +219,22 @@ <h6 class="card-title">第二步:在 XMOJ 上点击书签</h6>
216219
</div>
217220
</div>
218221
</div>
222+
223+
<!-- Tab: SSO Login -->
224+
<div id="tab-sso" style="display:none;">
225+
<div class="card border-0 shadow-sm">
226+
<div class="card-body">
227+
<h6 class="card-title mb-2">统一认证中心登录</h6>
228+
<p class="card-text small text-body-secondary mb-3">
229+
点击下方按钮跳转至统一认证中心授权,授权成功后将自动返回并完成登录。
230+
</p>
231+
<button class="btn btn-primary w-100" id="btn-sso-login">前往统一认证中心授权</button>
232+
<p class="small text-body-secondary mt-2 mb-0">
233+
授权地址:sso.xmoj-bbs.me,回调地址:xmoj-bbs.me/messages.html
234+
</p>
235+
</div>
236+
</div>
237+
</div>
219238
</div>
220239

221240
<!-- ==================== SCREEN: MAIL LIST ==================== -->
@@ -377,6 +396,11 @@ <h5 class="modal-title" id="user-info-modal-title">用户信息</h5>
377396
const SCROLL_THRESHOLD = 80;
378397
const MAX_IMAGE_BYTES = 5 * 1024 * 1024;
379398
const PREVIEW_LEN = 60;
399+
const OAUTH_AUTHORIZE_URL = 'https://sso.xmoj-bbs.me/authorize';
400+
const OAUTH_CLIENT_ID = 'app_0f4cbbddfa42c0c4576cba8f7598d0a2';
401+
const OAUTH_SCOPE = 'openid profile email xmoj_profile';
402+
const OAUTH_REDIRECT_URI = 'https://xmoj-bbs.me/messages.html';
403+
const STORAGE_OAUTH_STATE = 'xmoj-msg-oauth-state';
380404

381405
// ── State ──────────────────────────────────────────────────────────────────
382406
let currentUser = null; // { username, phpsessid }
@@ -931,6 +955,7 @@ <h5 class="modal-title" id="user-info-modal-title">用户信息</h5>
931955
});
932956
document.getElementById('tab-bookmarklet').style.display = tab === 'bookmarklet' ? '' : 'none';
933957
document.getElementById('tab-manual').style.display = tab === 'manual' ? '' : 'none';
958+
document.getElementById('tab-sso').style.display = tab === 'sso' ? '' : 'none';
934959
}
935960

936961
document.querySelectorAll('#loginTabs .nav-link').forEach(function(btn) {
@@ -939,6 +964,89 @@ <h5 class="modal-title" id="user-info-modal-title">用户信息</h5>
939964
});
940965
});
941966

967+
function generateUuidV4() {
968+
if (window.crypto && window.crypto.randomUUID) return window.crypto.randomUUID();
969+
if (window.crypto && window.crypto.getRandomValues) {
970+
var bytes = new Uint8Array(16);
971+
window.crypto.getRandomValues(bytes);
972+
bytes[6] = (bytes[6] & 0x0f) | 0x40;
973+
bytes[8] = (bytes[8] & 0x3f) | 0x80;
974+
var hex = Array.from(bytes).map(function(b) { return b.toString(16).padStart(2, '0'); }).join('');
975+
return [hex.slice(0, 8), hex.slice(8, 12), hex.slice(12, 16), hex.slice(16, 20), hex.slice(20)].join('-');
976+
}
977+
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
978+
var r = Math.random() * 16 | 0;
979+
var v = c === 'x' ? r : (r & 0x3 | 0x8);
980+
return v.toString(16);
981+
});
982+
}
983+
984+
function startSsoLogin() {
985+
var state = generateUuidV4() + '.' + generateUuidV4();
986+
localStorage.setItem(STORAGE_OAUTH_STATE, state);
987+
var params = new URLSearchParams({
988+
response_type: 'code',
989+
client_id: OAUTH_CLIENT_ID,
990+
redirect_uri: OAUTH_REDIRECT_URI,
991+
scope: OAUTH_SCOPE,
992+
state: state
993+
});
994+
location.href = OAUTH_AUTHORIZE_URL + '?' + params.toString();
995+
}
996+
997+
async function exchangeSsoCode(code, state) {
998+
var res = await fetch(API_BASE + 'ExchangeSSOCode', {
999+
method: 'POST',
1000+
headers: { 'Content-Type': 'application/json' },
1001+
body: JSON.stringify({
1002+
Code: code,
1003+
State: state,
1004+
RedirectURI: OAUTH_REDIRECT_URI,
1005+
ClientID: OAUTH_CLIENT_ID
1006+
})
1007+
});
1008+
if (!res.ok) throw new Error('HTTP ' + res.status);
1009+
return res.json();
1010+
}
1011+
1012+
async function handleSsoCallback() {
1013+
var url = new URL(location.href);
1014+
var code = url.searchParams.get('code');
1015+
var state = url.searchParams.get('state');
1016+
if (!code) return false;
1017+
1018+
var expectedState = localStorage.getItem(STORAGE_OAUTH_STATE);
1019+
if (!state || !expectedState || state !== expectedState) {
1020+
showToast('统一认证登录失败:state 校验失败', 'danger');
1021+
url.searchParams.delete('code');
1022+
url.searchParams.delete('state');
1023+
history.replaceState(null, '', url.pathname + (url.searchParams.toString() ? '?' + url.searchParams.toString() : ''));
1024+
return false;
1025+
}
1026+
1027+
try {
1028+
var result = await exchangeSsoCode(code, state);
1029+
if (!result || !result.Success || !result.Data) {
1030+
throw new Error((result && result.Message) || '后端未返回有效登录数据');
1031+
}
1032+
if (!result.Data.Username || !result.Data.SessionID) {
1033+
throw new Error('返回数据缺少 Username 或 SessionID');
1034+
}
1035+
1036+
localStorage.removeItem(STORAGE_OAUTH_STATE);
1037+
saveSession(result.Data.Username, result.Data.SessionID);
1038+
url.searchParams.delete('code');
1039+
url.searchParams.delete('state');
1040+
history.replaceState(null, '', url.pathname + (url.searchParams.toString() ? '?' + url.searchParams.toString() : ''));
1041+
showToast('统一认证登录成功', 'success');
1042+
onLoggedIn();
1043+
return true;
1044+
} catch (err) {
1045+
showToast('统一认证登录失败:' + err.message, 'danger');
1046+
return false;
1047+
}
1048+
}
1049+
9421050
// ── Event Wiring ───────────────────────────────────────────────────────────
9431051
document.getElementById('btn-manual-login').addEventListener('click', function() {
9441052
var username = document.getElementById('input-username').value.trim();
@@ -948,6 +1056,8 @@ <h5 class="modal-title" id="user-info-modal-title">用户信息</h5>
9481056
onLoggedIn();
9491057
});
9501058

1059+
document.getElementById('btn-sso-login').addEventListener('click', startSsoLogin);
1060+
9511061
document.getElementById('btn-logout').addEventListener('click', logout);
9521062

9531063
document.getElementById('btn-refresh-list').addEventListener('click', loadMailList);
@@ -1034,7 +1144,7 @@ <h5 class="modal-title" id="user-info-modal-title">用户信息</h5>
10341144
});
10351145

10361146
// ── Boot ───────────────────────────────────────────────────────────────────
1037-
(function init() {
1147+
(async function init() {
10381148
initTheme();
10391149
initBookmarklet();
10401150

@@ -1050,6 +1160,8 @@ <h5 class="modal-title" id="user-info-modal-title">用户信息</h5>
10501160
return;
10511161
}
10521162

1163+
if (await handleSsoCallback()) return;
1164+
10531165
if (loadSession()) {
10541166
onLoggedIn();
10551167
} else {

0 commit comments

Comments
 (0)