做统一认证登录
This commit is contained in:
1 parent
1a4471e34d
commit
fa281363fb
78 files changed
+8127
-1442
No files matched your search
@@ -0,0 +1,197 @@
|
||||
/**
|
||||
* 统一认证中心(UAC)客户端 —— OIDC Authorization Code + PKCE
|
||||
*
|
||||
* 接入方式:在 .env 中配置
|
||||
* VITE_AUTH_MODE=sso 开启统一认证(不配置则走原有 /backend/login)
|
||||
* VITE_AUTH_BASE=https://api.yunzer.cn/auth
|
||||
* VITE_AUTH_CLIENT_ID=yz-backend
|
||||
* VITE_AUTH_REDIRECT_URI=https://back.yunzer.cn/#/auth/callback
|
||||
*
|
||||
* 说明:token 仍存在 localStorage(沿用现有 utils/request.js 与 stores/auth.js),
|
||||
* 因此接入后业务代码无需改动即可带上 Authorization 头。
|
||||
*/
|
||||
|
||||
const AUTH_BASE = import.meta.env.VITE_AUTH_BASE || "";
|
||||
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`;
|
||||
|
||||
const ACCESS_KEY = "token"; // 与现有代码保持一致
|
||||
const REFRESH_KEY = "auth_refresh_token";
|
||||
const SID_KEY = "auth_sid";
|
||||
|
||||
/** 是否启用统一认证(默认关闭,保持原有登录方式) */
|
||||
export function isSSOEnabled() {
|
||||
return import.meta.env.VITE_AUTH_MODE === "sso" && !!AUTH_BASE;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- PKCE
|
||||
|
||||
function randomString(len = 64) {
|
||||
const arr = new Uint8Array(len);
|
||||
crypto.getRandomValues(arr);
|
||||
return Array.from(arr, (b) => ("0" + b.toString(16)).slice(-2)).join("").slice(0, len);
|
||||
}
|
||||
|
||||
async function sha256(plain) {
|
||||
return crypto.subtle.digest("SHA-256", new TextEncoder().encode(plain));
|
||||
}
|
||||
|
||||
function base64url(buf) {
|
||||
return btoa(String.fromCharCode(...new Uint8Array(buf)))
|
||||
.replace(/\+/g, "-")
|
||||
.replace(/\//g, "_")
|
||||
.replace(/=+$/, "");
|
||||
}
|
||||
|
||||
async function generatePKCE() {
|
||||
const verifier = randomString(64);
|
||||
const challenge = base64url(await sha256(verifier));
|
||||
return { verifier, challenge };
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- 令牌存储
|
||||
|
||||
export function setTokens(data) {
|
||||
if (data?.access_token) localStorage.setItem(ACCESS_KEY, data.access_token);
|
||||
if (data?.refresh_token) localStorage.setItem(REFRESH_KEY, data.refresh_token);
|
||||
if (data?.sid) localStorage.setItem(SID_KEY, data.sid);
|
||||
}
|
||||
|
||||
export function getRefreshToken() {
|
||||
return localStorage.getItem(REFRESH_KEY) || "";
|
||||
}
|
||||
|
||||
export function clearTokens() {
|
||||
localStorage.removeItem(ACCESS_KEY);
|
||||
localStorage.removeItem(REFRESH_KEY);
|
||||
localStorage.removeItem(SID_KEY);
|
||||
}
|
||||
|
||||
export function getAccessToken() {
|
||||
return localStorage.getItem(ACCESS_KEY) || "";
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- 主流程
|
||||
|
||||
/** 跳转认证中心登录(带 PKCE 与 state) */
|
||||
export async function redirectToAuthorize() {
|
||||
const { verifier, challenge } = await generatePKCE();
|
||||
sessionStorage.setItem("pkce_verifier", verifier);
|
||||
const state = randomString(24);
|
||||
sessionStorage.setItem("oidc_state", state);
|
||||
|
||||
const params = new URLSearchParams({
|
||||
client_id: CLIENT_ID,
|
||||
redirect_uri: REDIRECT_URI,
|
||||
response_type: "code",
|
||||
scope: "openid profile tenant",
|
||||
state,
|
||||
code_challenge: challenge,
|
||||
code_challenge_method: "S256",
|
||||
});
|
||||
window.location.href = `${AUTH_BASE}/authorize?${params.toString()}`;
|
||||
}
|
||||
|
||||
/** 处理认证中心回跳:用 code 换令牌 */
|
||||
export async function handleAuthorizeCallback(query = {}) {
|
||||
const code = query.code;
|
||||
const state = query.state;
|
||||
const savedState = sessionStorage.getItem("oidc_state");
|
||||
const verifier = sessionStorage.getItem("pkce_verifier");
|
||||
|
||||
if (!code) throw new Error("缺少授权码");
|
||||
if (savedState && state !== savedState) throw new Error("state 校验失败");
|
||||
if (!verifier) throw new Error("PKCE 校验信息丢失,请重新登录");
|
||||
|
||||
const body = new URLSearchParams({
|
||||
grant_type: "authorization_code",
|
||||
code,
|
||||
client_id: CLIENT_ID,
|
||||
code_verifier: verifier,
|
||||
redirect_uri: REDIRECT_URI,
|
||||
});
|
||||
|
||||
const res = await fetch(`${AUTH_BASE}/token`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||
body,
|
||||
});
|
||||
const data = await res.json().catch(() => ({}));
|
||||
if (!res.ok || !data.access_token) {
|
||||
throw new Error(data.error_description || data.error || "换取令牌失败");
|
||||
}
|
||||
|
||||
sessionStorage.removeItem("pkce_verifier");
|
||||
sessionStorage.removeItem("oidc_state");
|
||||
setTokens(data);
|
||||
return data;
|
||||
}
|
||||
|
||||
/** 静默刷新访问令牌 */
|
||||
export async function refreshAccessToken() {
|
||||
const refresh = getRefreshToken();
|
||||
if (!refresh) throw new Error("无刷新令牌");
|
||||
|
||||
const body = new URLSearchParams({
|
||||
grant_type: "refresh_token",
|
||||
refresh_token: refresh,
|
||||
client_id: CLIENT_ID,
|
||||
});
|
||||
const res = await fetch(`${AUTH_BASE}/token`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||
body,
|
||||
});
|
||||
const data = await res.json().catch(() => ({}));
|
||||
if (!res.ok || !data.access_token) throw new Error("刷新令牌失败");
|
||||
setTokens(data);
|
||||
return data.access_token;
|
||||
}
|
||||
|
||||
/** 单点登出:吊销令牌后回认证中心登出页 */
|
||||
export async function logoutSSO() {
|
||||
const token = getAccessToken();
|
||||
try {
|
||||
await fetch(`${AUTH_BASE}/revoke`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||
body: new URLSearchParams({ token, refresh_token: getRefreshToken() }),
|
||||
});
|
||||
} catch (e) {
|
||||
// 吊销失败也要继续登出,避免卡在本端
|
||||
}
|
||||
clearTokens();
|
||||
const params = new URLSearchParams({
|
||||
client_id: CLIENT_ID,
|
||||
post_logout_redirect_uri: POST_LOGOUT_URI,
|
||||
});
|
||||
window.location.href = `${AUTH_BASE}/logout?${params.toString()}`;
|
||||
}
|
||||
|
||||
/** 拉取当前登录用户(认证中心视角) */
|
||||
export async function fetchUserInfo() {
|
||||
const res = await fetch(`${AUTH_BASE}/userinfo`, {
|
||||
headers: { Authorization: `Bearer ${getAccessToken()}` },
|
||||
});
|
||||
if (!res.ok) throw new Error("获取用户信息失败");
|
||||
return res.json();
|
||||
}
|
||||
|
||||
/** 切换企业(一人多企业场景) */
|
||||
export async function switchTenant(tid) {
|
||||
const res = await fetch(`${AUTH_BASE}/switch-tenant`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${getAccessToken()}`,
|
||||
},
|
||||
body: JSON.stringify({ tid, client_id: CLIENT_ID }),
|
||||
});
|
||||
const data = await res.json().catch(() => ({}));
|
||||
if (!res.ok || data.code !== 200) throw new Error(data.msg || "切换企业失败");
|
||||
if (data.data?.tokens) setTokens(data.data.tokens);
|
||||
return data.data;
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import axios from 'axios';
|
||||
import { isSSOEnabled, refreshAccessToken, clearTokens } from '@/utils/authClient';
|
||||
|
||||
// 获取API基础URL,添加调试信息
|
||||
const apiBaseURL = import.meta.env.VITE_API_BASE_URL;
|
||||
@@ -40,13 +41,27 @@ service.interceptors.response.use(
|
||||
response => {
|
||||
return response.data;
|
||||
},
|
||||
error => {
|
||||
async error => {
|
||||
if (error.response) {
|
||||
// 后端统一返回 {code, msg},优先透出业务提示(如唯一性冲突、无权操作),
|
||||
// 避免页面只显示 "Request failed with status code xxx"
|
||||
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');
|
||||
window.location.href = '#/login';
|
||||
return Promise.reject(new Error('token无效'));
|
||||
}
|
||||
}
|
||||
console.error('未授权,请重新登录');
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('userInfo');
|
||||
|
||||
Reference in new issue
Block a user