| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160 |
- 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)}`
- }
|