import Bowser from "bowser";
import type { SessionMetadataPayload } from "../models/AuthModels";
const LOCATION_TIMEOUT_MS = 2500;
const LOCATION_MAX_AGE_MS = 5 * 60 * 1000;
const LOCATION_CACHE_KEY = "dpz.auth.location.cache";
const LOCATION_CACHE_TTL_MS = 10 * 60 * 1000;
interface CachedLocation {
latitude?: number;
longitude?: number;
locationAccuracy?: number;
locationSource?: string;
cachedAt: number;
}
let warmUpInFlight: Promise<void> | null = null;
export class SessionMetadataService {
async collect(): Promise<SessionMetadataPayload> {
const parser = Bowser.getParser(navigator.userAgent);
const browser = parser.getBrowser();
const os = parser.getOS();
const platform = parser.getPlatform();
const deviceType = platform.type || "desktop";
return {
deviceName: this.toDeviceName(browser.name, os.name, deviceType),
deviceType,
browserName: browser.name,
browserVersion: browser.version,
osName: os.name,
osVersion: os.version,
platform: platform.vendor || platform.model || platform.type,
screenWidth: window.screen?.width,
screenHeight: window.screen?.height,
language: navigator.language,
...this.getCachedLocation(),
};
}
warmUp(): void {
if (typeof navigator === "undefined" || !("geolocation" in navigator)) {
return;
}
if (warmUpInFlight !== null) {
return;
}
warmUpInFlight = this.warmUpInternal().finally(() => {
warmUpInFlight = null;
});
}
private async warmUpInternal(): Promise<void> {
try {
if (this.isLocationCacheFresh()) {
return;
}
const permission = await this.queryGeolocationPermission();
if (permission === "denied") {
return;
}
const location = await this.getBrowserLocation(true);
this.saveLocationCache(location);
} catch {
// 预热失败静默忽略,登录时 collect() 会退化为仅静态信息
}
}
private async queryGeolocationPermission(): Promise<PermissionState | null> {
if (typeof navigator.permissions === "undefined") {
return null;
}
try {
const result = await navigator.permissions.query({ name: "geolocation" });
return result.state;
} catch {
return null;
}
}
private getCachedLocation(): Partial<SessionMetadataPayload> {
const cached = this.readLocationCache();
if (cached === null) {
return {};
}
return {
latitude: cached.latitude,
longitude: cached.longitude,
locationAccuracy: cached.locationAccuracy,
locationSource: cached.locationSource,
};
}
private isLocationCacheFresh(): boolean {
const cached = this.readLocationCache();
return (
cached !== null &&
typeof cached.cachedAt === "number" &&
Date.now() - cached.cachedAt <= LOCATION_CACHE_TTL_MS
);
}
private readLocationCache(): CachedLocation | null {
try {
const raw = window.localStorage.getItem(LOCATION_CACHE_KEY);
if (raw === null) {
return null;
}
const cached = JSON.parse(raw) as CachedLocation;
if (
typeof cached !== "object" ||
cached === null ||
typeof cached.cachedAt !== "number"
) {
return null;
}
return cached;
} catch {
return null;
}
}
private saveLocationCache(location: Partial<SessionMetadataPayload>): void {
if (location.latitude === undefined || location.longitude === undefined) {
return;
}
const cached: CachedLocation = {
latitude: location.latitude,
longitude: location.longitude,
locationAccuracy: location.locationAccuracy,
locationSource: location.locationSource,
cachedAt: Date.now(),
};
try {
window.localStorage.setItem(LOCATION_CACHE_KEY, JSON.stringify(cached));
} catch {
// 存储不可用时静默忽略
}
}
private getBrowserLocation(waitForUser: boolean): Promise<Partial<SessionMetadataPayload>> {
if (!("geolocation" in navigator)) {
return Promise.resolve({});
}
const timeoutMs = waitForUser ? 0 : LOCATION_TIMEOUT_MS;
return new Promise((resolve) => {
let settled = false;
const finish = (value: Partial<SessionMetadataPayload>) => {
if (settled) {
return;
}
settled = true;
resolve(value);
};
let timer = 0;
if (timeoutMs > 0) {
timer = window.setTimeout(() => finish({}), timeoutMs);
}
navigator.geolocation.getCurrentPosition(
(position) => {
if (timer > 0) {
window.clearTimeout(timer);
}
finish({
latitude: position.coords.latitude,
longitude: position.coords.longitude,
locationAccuracy: position.coords.accuracy,
locationSource: "browser",
});
},
() => {
if (timer > 0) {
window.clearTimeout(timer);
}
finish({});
},
{
enableHighAccuracy: false,
maximumAge: LOCATION_MAX_AGE_MS,
timeout: timeoutMs > 0 ? timeoutMs : undefined,
},
);
});
}
private toDeviceName(
browserName: string | undefined,
osName: string | undefined,
deviceType: string,
) {
const browser = browserName || "Browser";
const os = osName || this.toDeviceTypeName(deviceType);
return `${os} · ${browser}`;
}
private toDeviceTypeName(deviceType: string) {
switch (deviceType) {
case "mobile":
return "Mobile";
case "tablet":
return "Tablet";
case "tv":
return "TV";
default:
return "Desktop";
}
}
}
⚠⚠ 以下内容为AI分析的结果,请根据实际情况进行判断。
代码解释
这是一个 SessionMetadataService(会话元数据服务) 类,用于收集用户设备和浏览器的元数据信息,包括设备信息、浏览器信息、操作系统信息以及地理位置等。主要用于身份验证场景中标识用户会话。
核心功能
1. 常量定义
const LOCATION_TIMEOUT_MS = 2500; // 地理位置获取超时时间
const LOCATION_MAX_AGE_MS = 5 * 60 * 1000; // 地理位置最大缓存时间(5分钟)
const LOCATION_CACHE_KEY = "dpz.auth.location.cache"; // localStorage缓存键
const LOCATION_CACHE_TTL_MS = 10 * 60 * 1000; // 位置缓存有效期(10分钟)
2. 主要方法
collect() - 收集会话元数据
- 使用 Bowser 库解析 User-Agent 获取浏览器、操作系统、平台信息
- 收集设备类型、屏幕尺寸、语言等静态信息
- 尝试从缓存获取地理位置信息
- 返回完整的
SessionMetadataPayload对象
warmUp() - 预热地理位置获取
- 目的:提前获取地理位置权限和数据,避免在实际登录时等待
- 检查是否支持地理位置 API
- 防止重复预热(通过
warmUpInFlight标志) - 异步执行预热逻辑
warmUpInternal() - 预热内部实现
1. 检查缓存是否新鲜(10分钟内)
2. 查询地理位置权限状态
3. 如果权限被拒绝,直接返回
4. 获取浏览器地理位置并缓存
5. 失败时静默忽略(不影响主流程)
3. 地理位置相关方法
queryGeolocationPermission() - 查询地理位置权限
- 使用 Permissions API 检查权限状态
- 返回
granted | denied | prompt | null
getBrowserLocation(waitForUser) - 获取地理位置
waitForUser=true: 等待用户授权,无超时限制waitForUser=false: 设置 2.5 秒超时- 使用
navigator.geolocation.getCurrentPosition获取位置 - 配置选项:
enableHighAccuracy: false- 不要求高精度(节省电量)maximumAge: 5分钟- 接受5分钟内的缓存位置- 支持超时机制
缓存机制
- 读取缓存 (
readLocationCache): 从 localStorage 读取并验证缓存结构 - 保存缓存 (
saveLocationCache): 保存位置信息和时间戳 - 检查新鲜度 (
isLocationCacheFresh): 判断缓存是否在10分钟内
4. 辅助方法
toDeviceName() - 生成设备名称
// 示例输出:
"Windows · Chrome"
"macOS · Safari"
"Mobile · Chrome"
toDeviceTypeName() - 设备类型本地化
将 mobile/tablet/tv/desktop 转换为首字母大写的显示名称
设计亮点
- 渐进式增强:地理位置获取失败不影响核心功能
- 性能优化:
- 多层缓存机制(内存标志 + localStorage)
- 预热机制避免登录时阻塞
- 用户体验:
- 超时控制避免长时间等待
- 权限检查避免不必要的弹窗
- 健壮性:
- 全面的异常处理(try-catch 静默失败)
- 降级策略(无位置信息时仅返回静态信息)
- 隐私友好:
- 不强制要求地理位置
- 使用低精度模式节省资源
使用场景
典型用于登录/认证流程,用于设备指纹识别、安全审计、多设备管理等场景。
AI 正在分析代码…
评论加载中...