Archived
85 lines
3.2 KiB
TypeScript
85 lines
3.2 KiB
TypeScript
/**
|
|
* 统一鉴权中心(Keycloak)的前端落点(ADR-012;手册 `docs/keycloak-frontend-guide` §4)。
|
|
*
|
|
* 后端是**资源服务器**:不做登录、没有会话 cookie,每个请求带
|
|
* `Authorization: Bearer <access_token>`。token 的取 / 刷 / 登出全部在这里,
|
|
* 业务代码只见 `freshToken()` 一个口子。
|
|
*
|
|
* 硬性约束(手册 §16):
|
|
* · token 只放内存(keycloak-js 默认),**不进 localStorage** —— 刷新靠 SSO 会话静默恢复;
|
|
* · 授权 URL 与 PKCE 一律由 keycloak-js 完成,不自己拼;
|
|
* · 登出必须走 `keycloak.logout()` 结束 SSO 会话,只清本地状态会「登不出去」。
|
|
*/
|
|
import Keycloak from 'keycloak-js';
|
|
|
|
export const keycloak = new Keycloak({
|
|
url: import.meta.env.VITE_KEYCLOAK_URL,
|
|
realm: import.meta.env.VITE_KEYCLOAK_REALM,
|
|
clientId: import.meta.env.VITE_KEYCLOAK_CLIENT_ID,
|
|
});
|
|
|
|
const REQUIRED_ROLE = import.meta.env.VITE_REQUIRED_ROLE as string;
|
|
|
|
/** init 是否已跑过 —— 测试环境(无 Keycloak)下 freshToken 据此直接空手而归 */
|
|
let ready = false;
|
|
|
|
export type AuthState = { authenticated: boolean; authorized: boolean };
|
|
|
|
/**
|
|
* 初始化:静默探测浏览器是否已有 Keycloak 会话(隐藏 iframe,用户无感)。
|
|
* 不发跳转 —— 未登录交回 `authenticated: false`,由壳层决定摆登录页。
|
|
*/
|
|
export async function initAuth(): Promise<AuthState> {
|
|
const authenticated = await keycloak.init({
|
|
onLoad: 'check-sso',
|
|
silentCheckSsoRedirectUri: `${window.location.origin}/silent-check-sso.html`,
|
|
pkceMethod: 'S256',
|
|
checkLoginIframe: false,
|
|
});
|
|
ready = true;
|
|
|
|
if (!authenticated) return { authenticated: false, authorized: false };
|
|
|
|
// access token 只有 5 分钟:到期前用 refresh token 悄悄续,续不上就重新登录
|
|
keycloak.onTokenExpired = () => {
|
|
keycloak.updateToken(30).catch(() => keycloak.login());
|
|
};
|
|
|
|
return { authenticated: true, authorized: keycloak.hasRealmRole(REQUIRED_ROLE) };
|
|
}
|
|
|
|
export function login(): Promise<void> {
|
|
return keycloak.login();
|
|
}
|
|
|
|
/** 必须用这个登出:只清本地状态的话 Keycloak 的 SSO 会话还在,下次会静默登回来 */
|
|
export function logout(): Promise<void> {
|
|
return keycloak.logout({ redirectUri: window.location.origin });
|
|
}
|
|
|
|
/**
|
|
* 请求前调用,拿回一个保证没过期的 token(剩余有效期不足 30 秒才真刷新)。
|
|
* 未到阈值时 `updateToken` 不发网络请求,每个请求都调是安全的。
|
|
* 未登录 / 未初始化(测试)时回 `undefined`,请求就不带头 —— 让后端照实回 401。
|
|
*/
|
|
export async function freshToken(): Promise<string | undefined> {
|
|
if (!ready || !keycloak.authenticated) return undefined;
|
|
await keycloak.updateToken(30);
|
|
return keycloak.token;
|
|
}
|
|
|
|
/**
|
|
* 401 之后的强刷(typed-fetch 用):常规 `freshToken` 在阈值内不发请求,
|
|
* 而后端已经明说这一枚不行 —— 传一个极大的 minValidity 逼它真换一枚。
|
|
* 刷不出来(SSO 会话也没了)回 false,调用方照旧把 401 抛给页面。
|
|
*/
|
|
export async function forceRefreshToken(): Promise<boolean> {
|
|
if (!ready || !keycloak.authenticated) return false;
|
|
try {
|
|
await keycloak.updateToken(Number.MAX_SAFE_INTEGER);
|
|
return keycloak.token !== undefined;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|