feat(oidc): 接入统一认证登录与身份自助绑定

- 登录页接入统一认证入口并处理认证回调
- 个人中心新增认证身份区块,支持自助绑定与解绑
- 头像菜单与应用中心新增认证身份入口
- 应用中心删除经营管理分组,业务系统卡片指向统一认证授权地址
- 用户管理页移除认证身份代绑入口与弹窗
- 身份关联接口从用户域移到认证域
This commit is contained in:
Ray.Hao
2026-10-28 08:21:27 +08:00
parent 9cddb95fb7
commit b5bacd2cbf
205 changed files with 6705 additions and 7079 deletions

View File

@@ -1,11 +1,12 @@
import axios, { type InternalAxiosRequestConfig, type AxiosResponse } from "axios";
import axios, { type AxiosResponse } from "axios";
import type { AxiosRequestConfig } from "axios";
import qs from "qs";
import { ApiCodeEnum } from "@/enums/api";
import { usePermissionStoreHook } from "@/stores/permission";
import { AuthStorage } from "@/utils/auth";
import type { ApiResult } from "@/api/common";
import { installTokenRefresh } from "@/utils/token-refresh";
import { readBinaryApiResult } from "@/utils/http-response";
const http = axios.create({
baseURL: import.meta.env.VITE_API_BASE,
@@ -15,44 +16,38 @@ const http = axios.create({
paramsSerializer: (params) => qs.stringify(params, { arrayFormat: "repeat" }),
});
// 注入访问令牌
http.interceptors.request.use(
(config: InternalAxiosRequestConfig) => {
if (!config.anonymous) {
const token = AuthStorage.getAccessToken();
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
}
return config;
},
(error) => Promise.reject(error)
);
// 令牌续期拦截器:响应拦截器按注册顺序执行,先于业务拦截器接管 401,业务拦截器只负责业务错误的提示
// 先注册续期拦截器,使令牌失效在业务错误提示之前处理。
installTokenRefresh(http);
// 解包业务数据
// 拦截器保留完整响应,解包仅在下方 request 入口执行一次。
http.interceptors.response.use(
(response: AxiosResponse<ApiResult>): AxiosResponse | any => {
async (response: AxiosResponse<ApiResult>): Promise<AxiosResponse<ApiResult>> => {
const { responseType } = response.config;
// 二进制数据直接透传
// 文件接口的业务失败仍按 JSON 处理,成功时保留完整文件响应。
if (responseType === "blob" || responseType === "arraybuffer") {
try {
const result = await readBinaryApiResult(response);
if (!result || result.code === ApiCodeEnum.SUCCESS) return response;
throw new Error(result.msg || "文件请求失败");
} catch (error) {
ElMessage.error(error instanceof Error ? error.message : "文件请求失败");
throw error;
}
}
const { code, msg } = response.data;
if (code === ApiCodeEnum.SUCCESS) {
return response;
}
const { code, data, msg } = response.data;
if (code === ApiCodeEnum.SUCCESS) {
return data;
}
ElMessage.error(msg || "系统出错");
return Promise.reject(new Error(msg || "系统出错"));
throw new Error(msg || "系统出错");
},
async (error) => {
if (axios.isCancel(error)) return Promise.reject(error);
// 令牌续期拦截器已终结的错误(Token Invalid 等)不再提示
if (!axios.isAxiosError(error)) {
return Promise.reject(error);
@@ -60,6 +55,10 @@ http.interceptors.response.use(
const { response } = error;
if (response && error.config?.silentErrorStatuses?.includes(response.status)) {
return Promise.reject(error);
}
if (!response) {
ElMessage.error("网络连接失败");
return Promise.reject(error);
@@ -67,7 +66,7 @@ http.interceptors.response.use(
const { code, msg } = response.data as ApiResult;
// 令牌失效:透传原始响应给续期拦截器处理,不在此提示
// 令牌失效已交由前面的续期拦截器处理,这里不重复提示。
if (code === ApiCodeEnum.ACCESS_TOKEN_INVALID || code === ApiCodeEnum.REFRESH_TOKEN_INVALID) {
return Promise.reject(error);
}
@@ -75,14 +74,36 @@ http.interceptors.response.use(
// 权限不足:刷新权限后再提示
if (code === ApiCodeEnum.PERMISSION_DENIED) {
const permissionStore = usePermissionStoreHook();
await permissionStore.refreshPermissions();
try {
await permissionStore.refreshPermissions();
} catch {
// 权限刷新失败不能覆盖原请求的 403。
return Promise.reject(error);
}
ElMessage.error(msg || "权限不足");
return Promise.reject(new Error(msg || "权限不足"));
return Promise.reject(error);
}
ElMessage.error(msg || "请求失败");
return Promise.reject(new Error(msg || "请求失败"));
return Promise.reject(error);
}
);
export default http;
/**
* 二进制保留响应头,JSON 返回业务 data;Axios 泛型适配集中在此处。
*/
function request<T = Blob>(
config: AxiosRequestConfig & { responseType: "blob" }
): Promise<AxiosResponse<T>>;
function request<T = ArrayBuffer>(
config: AxiosRequestConfig & { responseType: "arraybuffer" }
): Promise<AxiosResponse<T>>;
function request<T = unknown, R = T>(config: AxiosRequestConfig): Promise<R>;
async function request(config: AxiosRequestConfig): Promise<unknown> {
const response = await http.request<ApiResult>(config);
return config.responseType === "blob" || config.responseType === "arraybuffer"
? response
: response.data.data;
}
export default request;