import CryptoJS from 'crypto-js' import useStore from '../store' import type { AuthPayload } from '../store' export type { AuthPayload } from '../store' export { AUTH_TOKEN_KEY, AUTH_DISPLAY_NAME_KEY, AUTH_ROLE_KEY } from '../store' /** 认证工具:生成授权链接、管理 PKCE 临时会话,以及读写当前标签页的登录信息。 */ /** 是否使用本地 Mock 账号登录,由构建环境配置决定。 */ export const mockLogin = import.meta.env.VITE_USE_MOCK === 'true' /** 保留参考实现的原始值;不作为 PKCE verifier 或随机数种子使用。 */ export const originRandomString = 'ABCDEFGHIJ' /** 使用密码学安全随机数生成字符串,默认 128 位,可用于 PKCE verifier。 */ export function generateRandomString(length = 128) { if (!Number.isInteger(length) || length < 1 || length > 1024) throw new Error('随机字符串长度必须为 1 到 1024 的整数') const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' let result = '' while (result.length < length) { const bytes = crypto.getRandomValues(new Uint8Array(length - result.length)) for (const byte of bytes) { if (byte < 248) result += characters[byte % characters.length] } } return result } /** 登录成功后保存令牌、名称和角色,并解除自动认证暂停标记。 */ export function setAuthInfo(payload : AuthPayload) { useStore().SET_AUTH_INFO(payload) } /** 获取访问令牌并去掉已有的 Bearer 前缀,由请求拦截器统一添加前缀。 */ export function getToken() { const store = useStore() return store.accessToken || store.token || '' } /** 获取用于界面展示的用户名称,未登录时返回空字符串。 */ export const getDisplayName = () => useStore().displayName || '' /** 读取缓存的角色列表;仅供前端使用,实际操作权限必须由后端校验。 */ export function getRole() : string[] { return useStore().role } /** 清除本应用的登录信息,不会注销认证中心的单点登录会话。 */ export function clearAuthInfo() { useStore().CLEAR_AUTH_INFO() } /** 优先使用配置的回调地址,否则使用 forward.html 中转页,避免 history 路由 404。 */ export function getOAuthRedirectUri() { return String(import.meta.env.VITE_OAUTH_REDIRECT_URI || '').trim() || `${location.origin}/forward.html` } /** 读取有效的 PKCE 缓存:verifier 长度为 43~128 位,不设置本地过期时间。 */ function readTransaction() { try { const value = useStore().oauthTransaction if (!value || typeof value.verifier !== 'string' || !/^[A-Za-z0-9._~-]{43,128}$/.test(value.verifier)) return null return { ...value } } catch { return null } } /** 获取缓存的 code_verifier,供回调换取令牌使用;无有效缓存时返回空字符串。 */ export const getCachedVerifier = () => readTransaction()?.verifier || '' /** 获取发起授权时生成的 state,用于校验回调是否属于本次登录。 */ export const getCachedOAuthState = () => readTransaction()?.state || '' export const getCachedOAuthNonce = () => readTransaction()?.nonce || '' /** 读取并校验授权地址、令牌地址、客户端 ID 和回调地址,缺少配置时停止登录。 */ export function getOAuthConfig() { const productionBase = String(import.meta.env.VITE_API_AUTH_URL || '').trim() const baseURL = productionBase.replace(/\/+$/, '') const authorizeURL = `${baseURL}/oauth2/authorize` const tokenURL = `${baseURL}/oauth2/token` const responseMode = String(import.meta.env.VITE_OAUTH_RESPONSE_MODE || 'query').trim() if (!['query', 'form_post'].includes(responseMode)) throw new Error('回调模式仅支持 query 或 form_post') const redirectURI = getOAuthRedirectUri() if (!authorizeURL || !tokenURL) throw new Error('请配置认证地址、令牌地址') for (const value of [authorizeURL, tokenURL, redirectURI]) { const url = new URL(value, location.origin) if (!['http:', 'https:'].includes(url.protocol)) throw new Error('认证地址必须使用 HTTP 或 HTTPS') } if (new URL(redirectURI).hash) throw new Error('OAuth 回调地址不能包含 #') const clientId = 'client' return { authorizeURL, tokenURL, clientId, redirectURI, responseMode } } /** 同步计算 SHA-256 摘要并编码为 Base64URL;不是可逆加密。 */ export function sha256(message : string) { return CryptoJS.SHA256(message).toString(CryptoJS.enc.Base64) .replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '') } /** 保留原有方法名称,与 sha256 使用同一套 S256 计算逻辑。 */ export const createChallenge = sha256 /** 生成新的 verifier 和 state,缓存本次授权会话并返回 challenge;再次调用会覆盖旧会话。 */ export function generatePKCE() { const verifier = generateRandomString() const challenge = sha256(verifier) useStore().SET_OAUTH_TRANSACTION({ verifier, state: generateRandomString(10), nonce: generateRandomString(12), redirectURI: getOAuthRedirectUri() }) return challenge } /** 生成授权链接;form_post 模式必须有服务端接收并转交回调,静态前端默认使用 query。 */ export function getOAuthLoginUrl() { const config = getOAuthConfig() const challenge = generatePKCE() const state = getCachedOAuthState() const nonce = getCachedOAuthNonce() const url = new URL(config.authorizeURL, location.origin) url.search = new URLSearchParams({ client_id: config.clientId, redirect_uri: config.redirectURI, response_type: 'code', response_mode: config.responseMode, scope: 'openid', state, nonce, code_challenge: challenge, code_challenge_method: 'S256' }).toString() return url.toString() } /** 兼容原有 URL 大写命名。 */ export const getOAuthLoginURL = getOAuthLoginUrl /** 校验回调 state 和回调地址,返回换取令牌所需的 verifier 与回调地址。 */ export function consumeOAuthTransaction(state : string | null) { const transaction = readTransaction() // 无论校验成功与否都删除缓存,禁止重复使用;失败后需重新发起认证。 useStore().CLEAR_OAUTH_TEMP() if (!state || !transaction || transaction.state !== state || typeof transaction.verifier !== 'string' || transaction.redirectURI !== getOAuthConfig().redirectURI) { throw new Error('登录会话已失效或状态校验失败,请重新认证') } return { verifier: transaction.verifier as string, redirectURI: transaction.redirectURI as string } } /** 清除 PKCE 临时会话,用于认证失败、退出登录或登录失效后的清理。 */ export function clearOAuthTemp() { useStore().CLEAR_OAUTH_TEMP() } /** 生成 OIDC 登出链接,跳转到认证服务器登出后再回到应用。 */ export function getOAuthLogoutUrl() { const config = getOAuthConfig() const baseURL = config.authorizeURL.replace(/\/oauth2\/authorize$/, '') const postLogoutRedirectUri = getOAuthRedirectUri() return `${baseURL}/logout?post_logout_redirect_uri=${encodeURIComponent(postLogoutRedirectUri)}` }