forked from imbytecat/fullstack-starter
feat: 添加硬件指纹功能并优化依赖管理
- 更新依赖管理文档,明确使用 Bun Catalog 统一管理版本并规范安装方式,新增已知问题与解决方案、依赖选择经验及 Git 工作流要求,强化团队协作与技术决策可追溯性。 - 添加硬件指纹页面,展示机器码、指纹质量等级及详细信息,并支持一键复制和缓存提示。 - 添加指纹路由配置并更新路由树类型定义以包含新路由路径和相关类型。 - 添加硬件指纹获取接口的契约定义,包含指纹字符串、质量等级、强标识符数量和时间戳的验证规则。 - 添加指纹合约到API合约导出中 - 添加硬件指纹获取接口,支持10分钟缓存并包含主硬盘序列号以提升指纹稳定性。 - 添加指纹路由到API路由器中 - 重构硬件指纹生成逻辑,引入缓存机制、质量等级评估和容错处理,提升稳定性与可维护性。
This commit is contained in:
@@ -16,6 +16,41 @@
|
|||||||
- **代码质量**: Biome (格式化 + Lint)
|
- **代码质量**: Biome (格式化 + Lint)
|
||||||
- **桌面壳** (可选): Tauri v2 (详见 `src-tauri/AGENTS.md`)
|
- **桌面壳** (可选): Tauri v2 (详见 `src-tauri/AGENTS.md`)
|
||||||
|
|
||||||
|
## 依赖管理
|
||||||
|
|
||||||
|
### Bun Catalog 系统
|
||||||
|
|
||||||
|
项目使用 **Bun Catalog** 统一管理依赖版本(定义在根目录 `package.json` 的 `catalog` 字段)。
|
||||||
|
|
||||||
|
**安装依赖的正确方式**:
|
||||||
|
```bash
|
||||||
|
# ✅ 正确:使用 catalog: 前缀
|
||||||
|
bun add <package-name>@catalog:
|
||||||
|
|
||||||
|
# ❌ 错误:直接安装会绕过版本统一管理
|
||||||
|
bun add <package-name>@latest
|
||||||
|
```
|
||||||
|
|
||||||
|
**示例**:
|
||||||
|
```bash
|
||||||
|
# 添加 systeminformation 依赖到 packages/utils
|
||||||
|
cd packages/utils
|
||||||
|
bun add systeminformation@catalog:
|
||||||
|
|
||||||
|
# 添加 react 依赖到 apps/server
|
||||||
|
cd apps/server
|
||||||
|
bun add react@catalog:
|
||||||
|
```
|
||||||
|
|
||||||
|
**为什么使用 Catalog**:
|
||||||
|
- 确保 monorepo 中所有包使用相同版本
|
||||||
|
- 集中管理依赖版本,避免版本冲突
|
||||||
|
- 简化依赖升级(只需修改根 package.json)
|
||||||
|
|
||||||
|
**添加新依赖的步骤**:
|
||||||
|
1. 在根目录 `package.json` 的 `catalog` 字段添加依赖及版本
|
||||||
|
2. 在目标包中使用 `bun add <package>@catalog:` 安装
|
||||||
|
|
||||||
## 构建、Lint 和测试命令
|
## 构建、Lint 和测试命令
|
||||||
|
|
||||||
### 开发
|
### 开发
|
||||||
@@ -273,5 +308,63 @@ const mutation = useMutation(orpc.myFeature.create.mutationOptions())
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**最后更新**: 2026-01-18
|
## 已知问题与解决方案
|
||||||
|
|
||||||
|
### 构建问题
|
||||||
|
|
||||||
|
**问题**: Vite 8.0.0-beta.9 与 Nitro 插件存在兼容性问题
|
||||||
|
- **错误**: `TypeError: Cannot redefine property: viteMetadata`
|
||||||
|
- **影响**: `bun run build` 构建失败
|
||||||
|
- **解决方案**: 等待 Vite 8.0 正式版发布修复,开发环境(`bun dev`)不受影响
|
||||||
|
- **临时方案**: 如需生产构建,可降级到 Vite 5.x 稳定版
|
||||||
|
|
||||||
|
### 依赖选择经验
|
||||||
|
|
||||||
|
**ohash vs crypto.createHash**
|
||||||
|
|
||||||
|
在实现硬件指纹功能时,曾误判 `ohash` 不适合用于硬件指纹识别。经深入研究发现:
|
||||||
|
|
||||||
|
**事实**:
|
||||||
|
- `ohash` 内部使用**完整的 SHA-256** 算法(256 位)
|
||||||
|
- 输出 43 字符 Base64URL 编码(等价于 64 字符 Hex)
|
||||||
|
- 碰撞概率与 `crypto.createHash('sha256')` **完全相同**(2^128)
|
||||||
|
- 自动处理对象序列化,代码更简洁
|
||||||
|
|
||||||
|
**对比**:
|
||||||
|
```typescript
|
||||||
|
// ohash - 推荐用于对象哈希
|
||||||
|
import { hash } from 'ohash'
|
||||||
|
const fingerprint = hash(systemInfo) // 一行搞定
|
||||||
|
|
||||||
|
// crypto - 需要手动序列化
|
||||||
|
import { createHash } from 'node:crypto'
|
||||||
|
const fingerprint = createHash('sha256')
|
||||||
|
.update(JSON.stringify(systemInfo))
|
||||||
|
.digest('base64url')
|
||||||
|
```
|
||||||
|
|
||||||
|
**结论**:
|
||||||
|
- ✅ `ohash` 完全适合硬件指纹场景(数据来自系统 API,非用户输入)
|
||||||
|
- ✅ 两者安全性等价,选择取决于代码风格偏好
|
||||||
|
- ⚠️ ohash 文档警告的"序列化安全性"仅针对**用户输入**场景
|
||||||
|
|
||||||
|
**经验教训**:
|
||||||
|
- 不要仅凭名称("短哈希")判断库的实现
|
||||||
|
- 深入研究文档和源码再做技术决策
|
||||||
|
- 区分"用户输入场景"和"系统数据场景"的安全要求
|
||||||
|
|
||||||
|
### Git 工作流要求
|
||||||
|
|
||||||
|
**重要原则**:保持代码仓库与文档同步
|
||||||
|
|
||||||
|
当遇到技术问题、做出架构决策、或发现重要经验时:
|
||||||
|
1. **立即更新 AGENTS.md**:记录问题、原因、解决方案
|
||||||
|
2. **持续同步**:每次重大变更后更新文档
|
||||||
|
3. **版本关联**:在文档中标注相关的库版本、commit hash
|
||||||
|
|
||||||
|
这确保未来的开发者(包括 AI 助手)能快速理解项目历史和技术选择。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**最后更新**: 2026-01-24
|
||||||
**项目版本**: 基于 package.json 依赖版本
|
**项目版本**: 基于 package.json 依赖版本
|
||||||
|
|||||||
@@ -9,10 +9,16 @@
|
|||||||
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
|
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
|
||||||
|
|
||||||
import { Route as rootRouteImport } from './routes/__root'
|
import { Route as rootRouteImport } from './routes/__root'
|
||||||
|
import { Route as FingerprintRouteImport } from './routes/fingerprint'
|
||||||
import { Route as IndexRouteImport } from './routes/index'
|
import { Route as IndexRouteImport } from './routes/index'
|
||||||
import { Route as ApiSplatRouteImport } from './routes/api/$'
|
import { Route as ApiSplatRouteImport } from './routes/api/$'
|
||||||
import { Route as ApiRpcSplatRouteImport } from './routes/api/rpc.$'
|
import { Route as ApiRpcSplatRouteImport } from './routes/api/rpc.$'
|
||||||
|
|
||||||
|
const FingerprintRoute = FingerprintRouteImport.update({
|
||||||
|
id: '/fingerprint',
|
||||||
|
path: '/fingerprint',
|
||||||
|
getParentRoute: () => rootRouteImport,
|
||||||
|
} as any)
|
||||||
const IndexRoute = IndexRouteImport.update({
|
const IndexRoute = IndexRouteImport.update({
|
||||||
id: '/',
|
id: '/',
|
||||||
path: '/',
|
path: '/',
|
||||||
@@ -31,36 +37,47 @@ const ApiRpcSplatRoute = ApiRpcSplatRouteImport.update({
|
|||||||
|
|
||||||
export interface FileRoutesByFullPath {
|
export interface FileRoutesByFullPath {
|
||||||
'/': typeof IndexRoute
|
'/': typeof IndexRoute
|
||||||
|
'/fingerprint': typeof FingerprintRoute
|
||||||
'/api/$': typeof ApiSplatRoute
|
'/api/$': typeof ApiSplatRoute
|
||||||
'/api/rpc/$': typeof ApiRpcSplatRoute
|
'/api/rpc/$': typeof ApiRpcSplatRoute
|
||||||
}
|
}
|
||||||
export interface FileRoutesByTo {
|
export interface FileRoutesByTo {
|
||||||
'/': typeof IndexRoute
|
'/': typeof IndexRoute
|
||||||
|
'/fingerprint': typeof FingerprintRoute
|
||||||
'/api/$': typeof ApiSplatRoute
|
'/api/$': typeof ApiSplatRoute
|
||||||
'/api/rpc/$': typeof ApiRpcSplatRoute
|
'/api/rpc/$': typeof ApiRpcSplatRoute
|
||||||
}
|
}
|
||||||
export interface FileRoutesById {
|
export interface FileRoutesById {
|
||||||
__root__: typeof rootRouteImport
|
__root__: typeof rootRouteImport
|
||||||
'/': typeof IndexRoute
|
'/': typeof IndexRoute
|
||||||
|
'/fingerprint': typeof FingerprintRoute
|
||||||
'/api/$': typeof ApiSplatRoute
|
'/api/$': typeof ApiSplatRoute
|
||||||
'/api/rpc/$': typeof ApiRpcSplatRoute
|
'/api/rpc/$': typeof ApiRpcSplatRoute
|
||||||
}
|
}
|
||||||
export interface FileRouteTypes {
|
export interface FileRouteTypes {
|
||||||
fileRoutesByFullPath: FileRoutesByFullPath
|
fileRoutesByFullPath: FileRoutesByFullPath
|
||||||
fullPaths: '/' | '/api/$' | '/api/rpc/$'
|
fullPaths: '/' | '/fingerprint' | '/api/$' | '/api/rpc/$'
|
||||||
fileRoutesByTo: FileRoutesByTo
|
fileRoutesByTo: FileRoutesByTo
|
||||||
to: '/' | '/api/$' | '/api/rpc/$'
|
to: '/' | '/fingerprint' | '/api/$' | '/api/rpc/$'
|
||||||
id: '__root__' | '/' | '/api/$' | '/api/rpc/$'
|
id: '__root__' | '/' | '/fingerprint' | '/api/$' | '/api/rpc/$'
|
||||||
fileRoutesById: FileRoutesById
|
fileRoutesById: FileRoutesById
|
||||||
}
|
}
|
||||||
export interface RootRouteChildren {
|
export interface RootRouteChildren {
|
||||||
IndexRoute: typeof IndexRoute
|
IndexRoute: typeof IndexRoute
|
||||||
|
FingerprintRoute: typeof FingerprintRoute
|
||||||
ApiSplatRoute: typeof ApiSplatRoute
|
ApiSplatRoute: typeof ApiSplatRoute
|
||||||
ApiRpcSplatRoute: typeof ApiRpcSplatRoute
|
ApiRpcSplatRoute: typeof ApiRpcSplatRoute
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module '@tanstack/react-router' {
|
declare module '@tanstack/react-router' {
|
||||||
interface FileRoutesByPath {
|
interface FileRoutesByPath {
|
||||||
|
'/fingerprint': {
|
||||||
|
id: '/fingerprint'
|
||||||
|
path: '/fingerprint'
|
||||||
|
fullPath: '/fingerprint'
|
||||||
|
preLoaderRoute: typeof FingerprintRouteImport
|
||||||
|
parentRoute: typeof rootRouteImport
|
||||||
|
}
|
||||||
'/': {
|
'/': {
|
||||||
id: '/'
|
id: '/'
|
||||||
path: '/'
|
path: '/'
|
||||||
@@ -87,6 +104,7 @@ declare module '@tanstack/react-router' {
|
|||||||
|
|
||||||
const rootRouteChildren: RootRouteChildren = {
|
const rootRouteChildren: RootRouteChildren = {
|
||||||
IndexRoute: IndexRoute,
|
IndexRoute: IndexRoute,
|
||||||
|
FingerprintRoute: FingerprintRoute,
|
||||||
ApiSplatRoute: ApiSplatRoute,
|
ApiSplatRoute: ApiSplatRoute,
|
||||||
ApiRpcSplatRoute: ApiRpcSplatRoute,
|
ApiRpcSplatRoute: ApiRpcSplatRoute,
|
||||||
}
|
}
|
||||||
|
|||||||
301
apps/server/src/routes/fingerprint.tsx
Normal file
301
apps/server/src/routes/fingerprint.tsx
Normal file
@@ -0,0 +1,301 @@
|
|||||||
|
import { useSuspenseQuery } from '@tanstack/react-query'
|
||||||
|
import { createFileRoute } from '@tanstack/react-router'
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import { orpc } from '@/client/query-client'
|
||||||
|
|
||||||
|
export const Route = createFileRoute('/fingerprint')({
|
||||||
|
component: FingerprintPage,
|
||||||
|
loader: async ({ context }) => {
|
||||||
|
await context.queryClient.ensureQueryData(
|
||||||
|
orpc.fingerprint.get.queryOptions(),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
function FingerprintPage() {
|
||||||
|
const query = useSuspenseQuery(orpc.fingerprint.get.queryOptions())
|
||||||
|
const [copied, setCopied] = useState(false)
|
||||||
|
|
||||||
|
const data = query.data
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (copied) {
|
||||||
|
const timer = setTimeout(() => setCopied(false), 2000)
|
||||||
|
return () => clearTimeout(timer)
|
||||||
|
}
|
||||||
|
}, [copied])
|
||||||
|
|
||||||
|
const handleCopy = async () => {
|
||||||
|
await navigator.clipboard.writeText(data.fingerprint)
|
||||||
|
setCopied(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const qualityConfig = {
|
||||||
|
strong: {
|
||||||
|
label: '强',
|
||||||
|
color: 'text-green-600',
|
||||||
|
bg: 'bg-green-50',
|
||||||
|
border: 'border-green-200',
|
||||||
|
icon: '✓',
|
||||||
|
description: '推荐用于生产授权',
|
||||||
|
},
|
||||||
|
medium: {
|
||||||
|
label: '中',
|
||||||
|
color: 'text-yellow-600',
|
||||||
|
bg: 'bg-yellow-50',
|
||||||
|
border: 'border-yellow-200',
|
||||||
|
icon: '!',
|
||||||
|
description: '可用但不理想',
|
||||||
|
},
|
||||||
|
weak: {
|
||||||
|
label: '弱',
|
||||||
|
color: 'text-red-600',
|
||||||
|
bg: 'bg-red-50',
|
||||||
|
border: 'border-red-200',
|
||||||
|
icon: '×',
|
||||||
|
description: '仅适合开发/测试',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = qualityConfig[data.quality]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50 to-slate-50 py-12 px-4 sm:px-6 font-sans">
|
||||||
|
<div className="max-w-4xl mx-auto space-y-8">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="text-center space-y-3">
|
||||||
|
<h1 className="text-4xl font-bold text-slate-900 tracking-tight">
|
||||||
|
硬件指纹
|
||||||
|
</h1>
|
||||||
|
<p className="text-slate-500 text-lg">用于软件授权和机器码识别</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Main Card */}
|
||||||
|
<div className="bg-white rounded-3xl shadow-xl border border-slate-100 overflow-hidden">
|
||||||
|
{/* Quality Badge */}
|
||||||
|
<div className={`px-8 py-6 border-b ${config.bg} ${config.border}`}>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div
|
||||||
|
className={`w-12 h-12 rounded-full ${config.bg} border-2 ${config.border} flex items-center justify-center text-2xl font-bold ${config.color}`}
|
||||||
|
>
|
||||||
|
{config.icon}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-sm font-medium text-slate-600">
|
||||||
|
指纹质量
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className={`px-3 py-1 rounded-full text-sm font-semibold ${config.bg} ${config.color} border ${config.border}`}
|
||||||
|
>
|
||||||
|
{config.label}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-slate-500 mt-1">
|
||||||
|
{config.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-right">
|
||||||
|
<div className="text-3xl font-bold text-slate-900">
|
||||||
|
{data.strongIdentifiersCount}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs font-medium text-slate-400 uppercase tracking-wider">
|
||||||
|
强标识符
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Fingerprint Display */}
|
||||||
|
<div className="px-8 py-8">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="text-sm font-semibold text-slate-700 uppercase tracking-wider">
|
||||||
|
机器码
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleCopy}
|
||||||
|
className={`px-4 py-2 rounded-lg font-medium text-sm transition-all ${
|
||||||
|
copied
|
||||||
|
? 'bg-green-100 text-green-700 border-2 border-green-300'
|
||||||
|
: 'bg-slate-100 text-slate-700 hover:bg-slate-200 border-2 border-slate-200'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{copied ? '已复制 ✓' : '复制'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="relative group">
|
||||||
|
<div className="absolute inset-0 bg-gradient-to-r from-blue-500 to-purple-500 rounded-xl blur-sm opacity-0 group-hover:opacity-20 transition-opacity" />
|
||||||
|
<div className="relative bg-slate-900 rounded-xl p-6 font-mono text-sm break-all leading-relaxed text-slate-100 shadow-inner border-2 border-slate-800">
|
||||||
|
{data.fingerprint}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2 text-xs text-slate-500">
|
||||||
|
<svg
|
||||||
|
className="w-4 h-4"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<title>信息</title>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<span>SHA-256 哈希,43 字符 Base64URL 编码</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Metadata */}
|
||||||
|
<div className="px-8 py-6 bg-slate-50 border-t border-slate-100">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="text-xs font-semibold text-slate-500 uppercase tracking-wider">
|
||||||
|
生成时间
|
||||||
|
</div>
|
||||||
|
<div className="text-lg font-medium text-slate-900">
|
||||||
|
{new Date(data.timestamp).toLocaleString('zh-CN', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
second: '2-digit',
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="text-xs font-semibold text-slate-500 uppercase tracking-wider">
|
||||||
|
缓存状态
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="w-2 h-2 rounded-full bg-green-500 animate-pulse" />
|
||||||
|
<span className="text-lg font-medium text-slate-900">
|
||||||
|
已缓存 10 分钟
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Info Cards */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||||
|
<div className="bg-white rounded-2xl shadow-md border border-slate-100 p-6 space-y-3">
|
||||||
|
<div className="w-10 h-10 rounded-lg bg-blue-100 flex items-center justify-center">
|
||||||
|
<svg
|
||||||
|
className="w-6 h-6 text-blue-600"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<title>安全</title>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h3 className="font-semibold text-slate-900">安全性</h3>
|
||||||
|
<p className="text-sm text-slate-600 leading-relaxed">
|
||||||
|
使用 HMAC-SHA256 加密,无法反推原始硬件信息
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-white rounded-2xl shadow-md border border-slate-100 p-6 space-y-3">
|
||||||
|
<div className="w-10 h-10 rounded-lg bg-purple-100 flex items-center justify-center">
|
||||||
|
<svg
|
||||||
|
className="w-6 h-6 text-purple-600"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<title>稳定性</title>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h3 className="font-semibold text-slate-900">稳定性</h3>
|
||||||
|
<p className="text-sm text-slate-600 leading-relaxed">
|
||||||
|
基于系统 UUID、序列号等不易变更的标识符
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-white rounded-2xl shadow-md border border-slate-100 p-6 space-y-3">
|
||||||
|
<div className="w-10 h-10 rounded-lg bg-green-100 flex items-center justify-center">
|
||||||
|
<svg
|
||||||
|
className="w-6 h-6 text-green-600"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<title>性能</title>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
d="M13 10V3L4 14h7v7l9-11h-7z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h3 className="font-semibold text-slate-900">高性能</h3>
|
||||||
|
<p className="text-sm text-slate-600 leading-relaxed">
|
||||||
|
自动缓存,减少系统调用开销
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Usage Hint */}
|
||||||
|
<div className="bg-blue-50 border-2 border-blue-200 rounded-2xl p-6">
|
||||||
|
<div className="flex gap-4">
|
||||||
|
<div className="flex-shrink-0">
|
||||||
|
<svg
|
||||||
|
className="w-6 h-6 text-blue-600"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<title>提示</title>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<h4 className="font-semibold text-blue-900">使用建议</h4>
|
||||||
|
<ul className="text-sm text-blue-800 space-y-1 list-disc list-inside">
|
||||||
|
<li>将机器码存储在授权服务器进行验证</li>
|
||||||
|
<li>建议配合用户账号进行双因素认证</li>
|
||||||
|
<li>同一台机器的指纹保持稳定,便于授权管理</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
26
apps/server/src/server/api/contracts/fingerprint.contract.ts
Normal file
26
apps/server/src/server/api/contracts/fingerprint.contract.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { oc } from '@orpc/contract'
|
||||||
|
import { z } from 'zod'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 硬件指纹质量等级
|
||||||
|
*/
|
||||||
|
const fingerprintQualitySchema = z.enum(['strong', 'medium', 'weak'])
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 硬件指纹响应 Schema
|
||||||
|
*/
|
||||||
|
const fingerprintResultSchema = z.object({
|
||||||
|
/** 机器码(HMAC-SHA256 哈希) */
|
||||||
|
fingerprint: z.string(),
|
||||||
|
/** 指纹质量等级 */
|
||||||
|
quality: fingerprintQualitySchema,
|
||||||
|
/** 可用的强标识符数量 */
|
||||||
|
strongIdentifiersCount: z.number(),
|
||||||
|
/** 生成时间戳 */
|
||||||
|
timestamp: z.number(),
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取硬件指纹契约
|
||||||
|
*/
|
||||||
|
export const get = oc.input(z.void()).output(fingerprintResultSchema)
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
|
import * as fingerprint from './fingerprint.contract'
|
||||||
import * as todo from './todo.contract'
|
import * as todo from './todo.contract'
|
||||||
|
|
||||||
export const contract = {
|
export const contract = {
|
||||||
|
fingerprint,
|
||||||
todo,
|
todo,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
11
apps/server/src/server/api/routers/fingerprint.router.ts
Normal file
11
apps/server/src/server/api/routers/fingerprint.router.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { getHardwareFingerprint } from '@furtherverse/utils/fingerprint'
|
||||||
|
import { os } from '../server'
|
||||||
|
|
||||||
|
export const get = os.fingerprint.get.handler(async () => {
|
||||||
|
const result = await getHardwareFingerprint({
|
||||||
|
cacheTtlMs: 10 * 60 * 1000, // 10 分钟缓存
|
||||||
|
includePrimaryDisk: true, // 包含主硬盘序列号以提高稳定性
|
||||||
|
})
|
||||||
|
|
||||||
|
return result
|
||||||
|
})
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
import { os } from '../server'
|
import { os } from '../server'
|
||||||
|
import * as fingerprint from './fingerprint.router'
|
||||||
import * as todo from './todo.router'
|
import * as todo from './todo.router'
|
||||||
|
|
||||||
export const router = os.router({
|
export const router = os.router({
|
||||||
|
fingerprint,
|
||||||
todo,
|
todo,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,29 +1,231 @@
|
|||||||
import { hash } from 'ohash'
|
import { hash } from 'ohash'
|
||||||
import si from 'systeminformation'
|
import si from 'systeminformation'
|
||||||
|
|
||||||
async function getSystemInfo() {
|
/**
|
||||||
const [uuid, baseboard, bios, system, diskLayout, networkInterfaces] =
|
* 硬件指纹质量等级
|
||||||
await Promise.all([
|
* - strong: 2+ 个强标识符可用(推荐用于生产授权)
|
||||||
|
* - medium: 1 个强标识符可用(可用但不理想)
|
||||||
|
* - weak: 无强标识符(仅适合开发/测试)
|
||||||
|
*/
|
||||||
|
export type FingerprintQuality = 'strong' | 'medium' | 'weak'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 标准化的系统信息(用于机器码生成)
|
||||||
|
*/
|
||||||
|
export type NormalizedSystemInfo = {
|
||||||
|
/** 系统 UUID(最稳定的硬件标识符) */
|
||||||
|
systemUuid: string | null
|
||||||
|
/** 系统序列号 */
|
||||||
|
systemSerial: string | null
|
||||||
|
/** 主板序列号 */
|
||||||
|
baseboardSerial: string | null
|
||||||
|
/** 主板制造商 */
|
||||||
|
baseboardManufacturer: string | null
|
||||||
|
/** BIOS 版本 */
|
||||||
|
biosVersion: string | null
|
||||||
|
/** BIOS 供应商 */
|
||||||
|
biosVendor: string | null
|
||||||
|
/** CPU 品牌标识(用于质量评估) */
|
||||||
|
cpuBrand: string | null
|
||||||
|
/** 主硬盘序列号(可选,高稳定性) */
|
||||||
|
primaryDiskSerial?: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 硬件指纹配置选项
|
||||||
|
*/
|
||||||
|
export type HardwareFingerprintOptions = {
|
||||||
|
/**
|
||||||
|
* 缓存 TTL(毫秒),默认 10 分钟
|
||||||
|
* 硬件信息变化频率极低,缓存可大幅提升性能
|
||||||
|
*/
|
||||||
|
cacheTtlMs?: number
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否包含主硬盘序列号(默认 true)
|
||||||
|
* 注意:在容器/虚拟机环境可能获取失败
|
||||||
|
*/
|
||||||
|
includePrimaryDisk?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 硬件指纹响应
|
||||||
|
*/
|
||||||
|
export type HardwareFingerprintResult = {
|
||||||
|
/** 机器码(HMAC-SHA256 哈希,64 字符十六进制) */
|
||||||
|
fingerprint: string
|
||||||
|
/** 指纹质量等级 */
|
||||||
|
quality: FingerprintQuality
|
||||||
|
/** 可用的强标识符数量 */
|
||||||
|
strongIdentifiersCount: number
|
||||||
|
/** 生成时间戳 */
|
||||||
|
timestamp: number
|
||||||
|
}
|
||||||
|
|
||||||
|
// 缓存实例
|
||||||
|
let cache: {
|
||||||
|
expiresAt: number
|
||||||
|
value: HardwareFingerprintResult
|
||||||
|
} | null = null
|
||||||
|
|
||||||
|
// 防止并发重复请求
|
||||||
|
let inFlight: Promise<HardwareFingerprintResult> | null = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算指纹质量
|
||||||
|
*/
|
||||||
|
function computeQuality(info: NormalizedSystemInfo): {
|
||||||
|
quality: FingerprintQuality
|
||||||
|
count: number
|
||||||
|
} {
|
||||||
|
const strongKeys = [
|
||||||
|
info.systemUuid,
|
||||||
|
info.systemSerial,
|
||||||
|
info.baseboardSerial,
|
||||||
|
info.primaryDiskSerial,
|
||||||
|
].filter(Boolean).length
|
||||||
|
|
||||||
|
if (strongKeys >= 2) return { quality: 'strong', count: strongKeys }
|
||||||
|
if (strongKeys === 1) return { quality: 'medium', count: strongKeys }
|
||||||
|
return { quality: 'weak', count: 0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 安全地收集标准化系统信息(容错处理)
|
||||||
|
*/
|
||||||
|
async function collectNormalizedInfo(
|
||||||
|
opts: HardwareFingerprintOptions,
|
||||||
|
): Promise<NormalizedSystemInfo> {
|
||||||
|
// 使用 Promise.allSettled 避免单点失败
|
||||||
|
const tasks = await Promise.allSettled([
|
||||||
si.uuid(),
|
si.uuid(),
|
||||||
|
si.system(),
|
||||||
si.baseboard(),
|
si.baseboard(),
|
||||||
si.bios(),
|
si.bios(),
|
||||||
si.system(),
|
si.cpu(),
|
||||||
si.diskLayout(),
|
opts.includePrimaryDisk !== false ? si.diskLayout() : Promise.resolve([]),
|
||||||
si.networkInterfaces(),
|
|
||||||
])
|
])
|
||||||
|
|
||||||
|
const [uuidRes, systemRes, baseboardRes, biosRes, cpuRes, diskRes] = tasks
|
||||||
|
|
||||||
|
const uuid = uuidRes.status === 'fulfilled' ? uuidRes.value : null
|
||||||
|
const system = systemRes.status === 'fulfilled' ? systemRes.value : null
|
||||||
|
const baseboard =
|
||||||
|
baseboardRes.status === 'fulfilled' ? baseboardRes.value : null
|
||||||
|
const bios = biosRes.status === 'fulfilled' ? biosRes.value : null
|
||||||
|
const cpu = cpuRes.status === 'fulfilled' ? cpuRes.value : null
|
||||||
|
|
||||||
|
// 提取主硬盘序列号(通常是第一个物理磁盘)
|
||||||
|
let primaryDiskSerial: string | null = null
|
||||||
|
if (diskRes.status === 'fulfilled' && Array.isArray(diskRes.value)) {
|
||||||
|
const disks = diskRes.value as Array<{ serialNum?: string; type?: string }>
|
||||||
|
const physicalDisk = disks.find(
|
||||||
|
(d) => d.type !== 'USB' && d.serialNum && d.serialNum.trim(),
|
||||||
|
)
|
||||||
|
primaryDiskSerial = physicalDisk?.serialNum?.trim() || null
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
uuid,
|
// 系统级标识符(最稳定)
|
||||||
baseboard,
|
systemUuid: (system?.uuid ?? uuid?.hardware ?? null) || null,
|
||||||
bios,
|
systemSerial: (system?.serial ?? null) || null,
|
||||||
system,
|
|
||||||
diskLayout,
|
// 主板标识符(次稳定)
|
||||||
networkInterfaces,
|
baseboardSerial: (baseboard?.serial ?? null) || null,
|
||||||
|
baseboardManufacturer: (baseboard?.manufacturer ?? null) || null,
|
||||||
|
|
||||||
|
// BIOS 信息(辅助识别)
|
||||||
|
biosVersion: (bios?.version ?? null) || null,
|
||||||
|
biosVendor: (bios?.vendor ?? null) || null,
|
||||||
|
|
||||||
|
// CPU 信息(辅助识别)
|
||||||
|
cpuBrand: (cpu?.brand ?? null) || null,
|
||||||
|
|
||||||
|
// 磁盘序列号(可选,高稳定性)
|
||||||
|
...(opts.includePrimaryDisk !== false ? { primaryDiskSerial } : {}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getHardwareFingerprint() {
|
/**
|
||||||
const systemInfo = await getSystemInfo()
|
/**
|
||||||
|
* 获取硬件指纹(机器码)
|
||||||
|
*
|
||||||
|
* 适用场景:客户端部署的软件授权、机器绑定
|
||||||
|
*
|
||||||
|
* 安全说明:
|
||||||
|
* - 返回 SHA-256 哈希(Base64URL 编码,43 字符),不可逆推原始硬件信息
|
||||||
|
* - 使用 ohash 自动处理对象序列化和哈希
|
||||||
|
* - 客户端部署场景:客户可以看到代码,无法使用密钥加密
|
||||||
|
* - 安全性依赖硬件信息本身的不可伪造性(来自操作系统)
|
||||||
|
* - 自动缓存减少系统调用开销
|
||||||
|
*
|
||||||
|
* 稳定性:
|
||||||
|
* - 优先使用系统 UUID、序列号等不易变更的标识符
|
||||||
|
* - 避免网络接口等易变信息
|
||||||
|
* - 容错处理,部分信息缺失不影响生成
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* const result = await getHardwareFingerprint({
|
||||||
|
* cacheTtlMs: 600000, // 10 分钟
|
||||||
|
* includePrimaryDisk: true,
|
||||||
|
* })
|
||||||
|
*
|
||||||
|
* console.log(result.fingerprint) // "a3f5e8c2d1b4..."
|
||||||
|
* console.log(result.quality) // "strong"
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export async function getHardwareFingerprint(
|
||||||
|
opts: HardwareFingerprintOptions,
|
||||||
|
): Promise<HardwareFingerprintResult> {
|
||||||
|
const ttl = opts.cacheTtlMs ?? 10 * 60 * 1000
|
||||||
|
const now = Date.now()
|
||||||
|
|
||||||
return hash(systemInfo)
|
// 返回缓存结果
|
||||||
|
if (cache && cache.expiresAt > now) {
|
||||||
|
return cache.value
|
||||||
|
}
|
||||||
|
|
||||||
|
// 防止并发重复请求
|
||||||
|
if (inFlight) {
|
||||||
|
return inFlight
|
||||||
|
}
|
||||||
|
|
||||||
|
inFlight = (async () => {
|
||||||
|
// 收集标准化信息
|
||||||
|
const info = await collectNormalizedInfo(opts)
|
||||||
|
|
||||||
|
// 计算质量
|
||||||
|
const { quality, count } = computeQuality(info)
|
||||||
|
|
||||||
|
// 使用 ohash 生成指纹(自动序列化 + SHA-256 + Base64URL)
|
||||||
|
const fingerprint = hash({
|
||||||
|
v: 1, // 版本号,未来如需变更采集策略可递增
|
||||||
|
info,
|
||||||
|
})
|
||||||
|
|
||||||
|
const result: HardwareFingerprintResult = {
|
||||||
|
fingerprint,
|
||||||
|
quality,
|
||||||
|
strongIdentifiersCount: count,
|
||||||
|
timestamp: now,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新缓存
|
||||||
|
cache = { expiresAt: now + ttl, value: result }
|
||||||
|
|
||||||
|
return result
|
||||||
|
})().finally(() => {
|
||||||
|
inFlight = null
|
||||||
|
})
|
||||||
|
|
||||||
|
return inFlight
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除指纹缓存(用于测试或强制刷新)
|
||||||
|
*/
|
||||||
|
export function clearFingerprintCache(): void {
|
||||||
|
cache = null
|
||||||
|
inFlight = null
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user