修复本地开发不走统一登陆
This commit is contained in:
1 parent
c49e8d0d3f
commit
1685b22e1e
12 files changed
+161
-139
No files matched your search
@@ -1,30 +1,47 @@
|
||||
/**
|
||||
* 统一认证中心(UAC)客户端 —— OIDC Authorization Code + PKCE
|
||||
*
|
||||
* 接入方式:在 .env 中配置
|
||||
* VITE_AUTH_MODE=sso 开启统一认证(不配置则走原有 /backend/login)
|
||||
* VITE_AUTH_BASE=https://api.yunzer.cn/auth
|
||||
* 本项目已停用本地账号密码登录(/backend/login、/backend/register 等入口均已下线),
|
||||
* 登录、登出、令牌续期一律走认证中心。
|
||||
*
|
||||
* 环境变量(见 .env.development / .env.production):
|
||||
* VITE_AUTH_BASE=https://api.yunzer.cn/auth 认证中心地址,缺省回退到 ${VITE_API_BASE_URL}/auth
|
||||
* VITE_AUTH_CLIENT_ID=yz-backend
|
||||
* VITE_AUTH_REDIRECT_URI=https://back.yunzer.cn/#/auth/callback
|
||||
* VITE_AUTH_POST_LOGOUT_URI=https://back.yunzer.cn/#/
|
||||
*
|
||||
* 说明:token 仍存在 localStorage(沿用现有 utils/request.js 与 stores/auth.js),
|
||||
* 因此接入后业务代码无需改动即可带上 Authorization 头。
|
||||
* 因此业务代码无需改动即可带上 Authorization 头。
|
||||
*/
|
||||
|
||||
const AUTH_BASE = import.meta.env.VITE_AUTH_BASE || "";
|
||||
// 未显式配置时回退到「接口地址 + /auth」,避免跳转到空地址或前端自身域名
|
||||
const API_BASE = (import.meta.env.VITE_API_BASE_URL || "").replace(/\/+$/, "");
|
||||
const AUTH_BASE = (import.meta.env.VITE_AUTH_BASE || (API_BASE ? `${API_BASE}/auth` : "")).replace(
|
||||
/\/+$/,
|
||||
""
|
||||
);
|
||||
const CLIENT_ID = import.meta.env.VITE_AUTH_CLIENT_ID || "yz-backend";
|
||||
const REDIRECT_URI =
|
||||
import.meta.env.VITE_AUTH_REDIRECT_URI || `${window.location.origin}/#/auth/callback`;
|
||||
const POST_LOGOUT_URI =
|
||||
import.meta.env.VITE_AUTH_POST_LOGOUT_URI || `${window.location.origin}/#/login`;
|
||||
import.meta.env.VITE_AUTH_POST_LOGOUT_URI || `${window.location.origin}/#/`;
|
||||
|
||||
const ACCESS_KEY = "token"; // 与现有代码保持一致
|
||||
const REFRESH_KEY = "auth_refresh_token";
|
||||
const SID_KEY = "auth_sid";
|
||||
|
||||
/** 是否启用统一认证(默认关闭,保持原有登录方式) */
|
||||
/**
|
||||
* 是否启用统一认证。
|
||||
* 本地登录已停用,此处恒为 true;保留函数是为了兼容既有调用点,
|
||||
* 后续清理时可直接删除所有分支判断。
|
||||
*/
|
||||
export function isSSOEnabled() {
|
||||
return import.meta.env.VITE_AUTH_MODE === "sso" && !!AUTH_BASE;
|
||||
return true;
|
||||
}
|
||||
|
||||
/** 认证中心地址是否可用(排查配置缺失用) */
|
||||
export function hasAuthBase() {
|
||||
return !!AUTH_BASE;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- PKCE
|
||||
@@ -76,14 +93,26 @@ export function getAccessToken() {
|
||||
|
||||
// ---------------------------------------------------------------- 主流程
|
||||
|
||||
/** 跳转认证中心登录(带 PKCE 与 state) */
|
||||
/**
|
||||
* 跳转认证中心登录(带 PKCE 与 state)。
|
||||
*
|
||||
* 入口取认证中心登录页而不是直接打 authorize:
|
||||
* GET {AUTH_BASE}/login?client_id=xxx&redirect=<base64url(authorize URL)>
|
||||
* 认证中心侧约定:
|
||||
* - 未登录 → 展示登录页,登录成功后 302 回 redirect 指向的 authorize;
|
||||
* - 已登录 → 立即 302 回 authorize,由 authorize 识别会话后签发 code;
|
||||
* 最终仍由 authorize 校验 redirect_uri 白名单并携带 code 回跳本端。
|
||||
*/
|
||||
export async function redirectToAuthorize() {
|
||||
if (!AUTH_BASE) {
|
||||
throw new Error("统一认证地址未配置:请检查 VITE_AUTH_BASE 或 VITE_API_BASE_URL");
|
||||
}
|
||||
const { verifier, challenge } = await generatePKCE();
|
||||
sessionStorage.setItem("pkce_verifier", verifier);
|
||||
const state = randomString(24);
|
||||
sessionStorage.setItem("oidc_state", state);
|
||||
|
||||
const params = new URLSearchParams({
|
||||
const authorizeURL = `${AUTH_BASE}/authorize?${new URLSearchParams({
|
||||
client_id: CLIENT_ID,
|
||||
redirect_uri: REDIRECT_URI,
|
||||
response_type: "code",
|
||||
@@ -91,8 +120,13 @@ export async function redirectToAuthorize() {
|
||||
state,
|
||||
code_challenge: challenge,
|
||||
code_challenge_method: "S256",
|
||||
});
|
||||
window.location.href = `${AUTH_BASE}/authorize?${params.toString()}`;
|
||||
}).toString()}`;
|
||||
|
||||
// redirect 参数由认证中心按 base64.RawURLEncoding 解析(无 = 填充)
|
||||
const redirectParam = base64url(new TextEncoder().encode(authorizeURL));
|
||||
window.location.href =
|
||||
`${AUTH_BASE}/login?client_id=${encodeURIComponent(CLIENT_ID)}` +
|
||||
`&redirect=${redirectParam}`;
|
||||
}
|
||||
|
||||
/** 处理认证中心回跳:用 code 换令牌 */
|
||||
@@ -153,6 +187,7 @@ export async function refreshAccessToken() {
|
||||
|
||||
/** 单点登出:吊销令牌后回认证中心登出页 */
|
||||
export async function logoutSSO() {
|
||||
if (!AUTH_BASE) return;
|
||||
const token = getAccessToken();
|
||||
try {
|
||||
await fetch(`${AUTH_BASE}/revoke`, {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import axios from 'axios';
|
||||
import { isSSOEnabled, refreshAccessToken, clearTokens, redirectToAuthorize } from '@/utils/authClient';
|
||||
import { refreshAccessToken, clearTokens, redirectToAuthorize } from '@/utils/authClient';
|
||||
|
||||
// 统一认证模式下 401 后正在跳转登录的标志,避免并发请求同时触发跳转造成死循环
|
||||
let ssoRedirecting = false;
|
||||
@@ -51,31 +51,25 @@ service.interceptors.response.use(
|
||||
const bizMsg = error.response.data?.msg || error.response.data?.message;
|
||||
switch (error.response.status) {
|
||||
case 401:
|
||||
// 统一认证模式:先用 refresh_token 静默换取新令牌并重放原请求,
|
||||
// 刷新失败(令牌过期/被吊销)才真正登出。
|
||||
if (isSSOEnabled()) {
|
||||
try {
|
||||
const newToken = await refreshAccessToken();
|
||||
error.config.headers['Authorization'] = `Bearer ${newToken}`;
|
||||
return service.request(error.config);
|
||||
} catch (e) {
|
||||
// 续期失败:清空登录态并跳认证中心重新登录(只跳一次,避免死循环)
|
||||
clearTokens();
|
||||
localStorage.removeItem('userInfo');
|
||||
if (!ssoRedirecting) {
|
||||
ssoRedirecting = true;
|
||||
redirectToAuthorize();
|
||||
}
|
||||
return Promise.reject(new Error('token无效,请重新登录'));
|
||||
// 统一认证:先用 refresh_token 静默换取新令牌并重放原请求,
|
||||
// 刷新失败(令牌过期/被吊销/旧本地登录遗留 token)才真正登出。
|
||||
try {
|
||||
const newToken = await refreshAccessToken();
|
||||
error.config.headers['Authorization'] = `Bearer ${newToken}`;
|
||||
return service.request(error.config);
|
||||
} catch (e) {
|
||||
// 续期失败:清空登录态并跳认证中心重新登录(只跳一次,避免死循环)
|
||||
clearTokens();
|
||||
localStorage.removeItem('userInfo');
|
||||
if (!ssoRedirecting) {
|
||||
ssoRedirecting = true;
|
||||
redirectToAuthorize().catch((err) => {
|
||||
console.error('跳转统一认证失败:', err);
|
||||
ssoRedirecting = false;
|
||||
});
|
||||
}
|
||||
return Promise.reject(new Error('token无效,请重新登录'));
|
||||
}
|
||||
console.error('未授权,请重新登录');
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('userInfo');
|
||||
if (window.location.hash !== '#/login') {
|
||||
window.location.href = '#/login';
|
||||
}
|
||||
return Promise.reject(new Error('token无效'));
|
||||
case 404:
|
||||
console.error('请求的资源不存在');
|
||||
break;
|
||||
|
||||
Reference in new issue
Block a user