| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134 |
- /**
- * Fmode 用户鉴权
- *
- * 通过调用 Fmode 网关的 doApi 接口校验 Bearer token 是否有效。
- * 默认使用 /user/getProfile,不携带 guid 时上游会返回参数错误,
- * 但只要 token 有效就不会返回“认证失败”,从而在无副作用的情况下完成校验。
- */
- import { getFmodeGatewayUrl, getFmodeTokenValidateMethod } from './config.js';
- import { sha256Hash } from './crypto.js';
- export interface FmodeUser {
- userId: string;
- nickname?: string;
- mobile?: string;
- avatarUrl?: string;
- }
- const VALIDATE_TIMEOUT_MS = 15000;
- const MAX_RETRIES = 3;
- const RETRY_DELAY_MS = 800;
- function isAuthFailure(resp: Response, json: unknown): boolean {
- if (resp.status === 401) return true;
- if (!json || typeof json !== 'object') return false;
- const payload = json as Record<string, unknown>;
- const message = String(payload.mess || payload.msg || payload.message || payload.error || '');
- return /认证失败|unauthorized|invalid token|token.*无效|token.*失效|未授权/i.test(message);
- }
- function sleep(ms: number): Promise<void> {
- return new Promise((resolve) => setTimeout(resolve, ms));
- }
- async function tryValidateToken(
- token: string,
- deviceGuid?: string,
- ): Promise<{ resp: Response; json: unknown }> {
- const url = `${getFmodeGatewayUrl()}/doApi`;
- const method = getFmodeTokenValidateMethod();
- const body = {
- uid: `relay-${Date.now()}`,
- method,
- params: deviceGuid ? { guid: deviceGuid } : {},
- };
- const controller = new AbortController();
- const timer = setTimeout(() => controller.abort(), VALIDATE_TIMEOUT_MS);
- try {
- const resp = await fetch(url, {
- method: 'POST',
- headers: {
- Authorization: `Bearer ${token}`,
- 'Content-Type': 'application/json',
- Accept: 'application/json',
- },
- body: JSON.stringify(body),
- signal: controller.signal,
- });
- let json: unknown;
- try {
- json = await resp.json();
- } catch {
- json = undefined;
- }
- return { resp, json };
- } finally {
- clearTimeout(timer);
- }
- }
- export async function verifyFmodeToken(
- token: string,
- deviceGuid?: string,
- ): Promise<FmodeUser> {
- if (!token) {
- throw new Error('Missing Fmode token');
- }
- let lastError: Error | undefined;
- for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
- try {
- const { resp, json } = await tryValidateToken(token, deviceGuid);
- if (isAuthFailure(resp, json)) {
- throw new Error('Fmode token 无效');
- }
- const payload = json && typeof json === 'object' ? (json as Record<string, unknown>) : {};
- const data =
- payload.data && typeof payload.data === 'object'
- ? (payload.data as Record<string, unknown>)
- : {};
- let userId =
- (data.userId as string) ||
- (data.acctid as string) ||
- (data.user_id as string) ||
- (data.mobile as string);
- // 如果提供了 guid 仍拿不到 userId,降级为 token 哈希(保证一人一租户的风控)
- if (!userId) {
- userId = `token_${sha256Hash(token).slice(0, 24)}`;
- if (deviceGuid) {
- console.warn('[FmodeAuth] 已提供 guid 但未返回 userId,使用 token 哈希作为用户标识');
- }
- }
- return {
- userId,
- nickname: data.nickname as string | undefined,
- mobile: data.mobile as string | undefined,
- avatarUrl: data.avatarUrl as string | undefined,
- };
- } catch (err) {
- const message = err instanceof Error ? err.message : String(err);
- // 认证错误不重试
- if (/Fmode token 无效|Missing Fmode token/i.test(message)) {
- throw err;
- }
- lastError = err instanceof Error ? err : new Error(message);
- if (attempt < MAX_RETRIES) {
- console.warn(`[FmodeAuth] 校验失败,第 ${attempt} 次重试: ${message}`);
- await sleep(RETRY_DELAY_MS * attempt);
- }
- }
- }
- throw new Error(`Fmode 网关无法访问: ${lastError?.message || '未知错误'}`);
- }
|