浏览代码

feat: prevent public_key endpoint from being cached #1234

0xJacky 3 周之前
父节点
当前提交
ad6e4d2670

+ 17 - 0
api/crypto/crypto.go

@@ -2,14 +2,30 @@ package crypto
 
 import (
 	"net/http"
+	"time"
 
 	"github.com/0xJacky/Nginx-UI/internal/crypto"
 	"github.com/gin-gonic/gin"
+	"github.com/google/uuid"
 	"github.com/uozi-tech/cosy"
 )
 
 // GetPublicKey generates a new ED25519 key pair and registers it in the cache
 func GetPublicKey(c *gin.Context) {
+	var data struct {
+		Timestamp   int64  `json:"timestamp" binding:"required"`
+		Fingerprint string `json:"fingerprint" binding:"required"`
+	}
+
+	if !cosy.BindAndValid(c, &data) {
+		return
+	}
+
+	if time.Now().Unix()-data.Timestamp > 10 {
+		cosy.ErrHandler(c, crypto.ErrTimeout)
+		return
+	}
+
 	params, err := crypto.GetCryptoParams()
 	if err != nil {
 		cosy.ErrHandler(c, err)
@@ -18,5 +34,6 @@ func GetPublicKey(c *gin.Context) {
 
 	c.JSON(http.StatusOK, gin.H{
 		"public_key": params.PublicKey,
+		"request_id": uuid.NewString(),
 	})
 }

+ 1 - 1
api/crypto/router.go

@@ -5,6 +5,6 @@ import "github.com/gin-gonic/gin"
 func InitPublicRouter(r *gin.RouterGroup) {
 	g := r.Group("/crypto")
 	{
-		g.GET("public_key", GetPublicKey)
+		g.POST("public_key", GetPublicKey)
 	}
 }

+ 1 - 0
app/package.json

@@ -15,6 +15,7 @@
   "dependencies": {
     "@0xjacky/vue-github-button": "^3.1.1",
     "@ant-design/icons-vue": "^7.0.1",
+    "@fingerprintjs/fingerprintjs": "^4.6.2",
     "@formkit/auto-animate": "^0.8.2",
     "@simplewebauthn/browser": "^13.1.2",
     "@uozi-admin/curd": "^4.5.3",

+ 10 - 0
app/pnpm-lock.yaml

@@ -14,6 +14,9 @@ importers:
       '@ant-design/icons-vue':
         specifier: ^7.0.1
         version: 7.0.1(vue@3.5.17(typescript@5.8.3))
+      '@fingerprintjs/fingerprintjs':
+        specifier: ^4.6.2
+        version: 4.6.2
       '@formkit/auto-animate':
         specifier: ^0.8.2
         version: 0.8.2
@@ -812,6 +815,9 @@ packages:
     resolution: {integrity: sha512-1+WqvgNMhmlAambTvT3KPtCl/Ibr68VldY2XY40SL1CE0ZXiakFR/cbTspaF5HsnpDMvcYYoJHfl4980NBjGag==}
     engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
 
+  '@fingerprintjs/fingerprintjs@4.6.2':
+    resolution: {integrity: sha512-g8mXuqcFKbgH2CZKwPfVtsUJDHyvcgIABQI7Y0tzWEFXpGxJaXuAuzlifT2oTakjDBLTK4Gaa9/5PERDhqUjtw==}
+
   '@formkit/auto-animate@0.8.2':
     resolution: {integrity: sha512-SwPWfeRa5veb1hOIBMdzI+73te5puUBHmqqaF1Bu7FjvxlYSz/kJcZKSa9Cg60zL0uRNeJL2SbRxV6Jp6Q1nFQ==}
 
@@ -4654,6 +4660,10 @@ snapshots:
       '@eslint/core': 0.15.1
       levn: 0.4.1
 
+  '@fingerprintjs/fingerprintjs@4.6.2':
+    dependencies:
+      tslib: 2.8.1
+
   '@formkit/auto-animate@0.8.2': {}
 
   '@humanfs/core@0.19.1': {}

+ 43 - 0
app/src/lib/helper/fingerprint.ts

@@ -0,0 +1,43 @@
+import FingerprintJS from '@fingerprintjs/fingerprintjs'
+
+let fpPromise: Promise<string> | null = null
+
+/**
+ * Get browser fingerprint
+ * Use caching mechanism to avoid duplicate calculations
+ */
+export async function getBrowserFingerprint(): Promise<string> {
+  if (!fpPromise) {
+    fpPromise = generateFingerprint()
+  }
+  return fpPromise
+}
+
+/**
+ * Generate browser fingerprint
+ */
+async function generateFingerprint(): Promise<string> {
+  try {
+    // Initialize FingerprintJS
+    const fp = await FingerprintJS.load()
+
+    // Get fingerprint result
+    const result = await fp.get()
+
+    // Return fingerprint ID
+    return result.visitorId
+  }
+  catch (error) {
+    console.warn('Failed to generate browser fingerprint, fallback to User Agent:', error)
+    // If fingerprint generation fails, fallback to User Agent
+    return navigator.userAgent
+  }
+}
+
+/**
+ * Clear fingerprint cache
+ * Force regenerate fingerprint
+ */
+export function clearFingerprintCache(): void {
+  fpPromise = null
+}

+ 2 - 0
app/src/lib/helper/index.ts

@@ -73,3 +73,5 @@ export {
   fromNow,
   urlJoin,
 }
+
+export { clearFingerprintCache, getBrowserFingerprint } from './fingerprint'

+ 7 - 1
app/src/lib/http/interceptors.ts

@@ -1,8 +1,10 @@
 import type { CosyError } from './types'
 import { http, useAxios } from '@uozi-admin/request'
+import dayjs from 'dayjs'
 import JSEncrypt from 'jsencrypt'
 import { storeToRefs } from 'pinia'
 import use2FAModal from '@/components/TwoFA/use2FAModal'
+import { getBrowserFingerprint } from '@/lib/helper'
 import { useNProgress } from '@/lib/nprogress/nprogress'
 import { useSettingsStore, useUserStore } from '@/pinia'
 import router from '@/routes'
@@ -16,7 +18,11 @@ const dedupe = useMessageDedupe()
 // Helper function for encrypting JSON data
 // eslint-disable-next-line ts/no-explicit-any
 async function encryptJsonData(data: any): Promise<string> {
-  const cryptoParams = await http.get('/crypto/public_key')
+  const fingerprint = await getBrowserFingerprint()
+  const cryptoParams = await http.post('/crypto/public_key', {
+    timestamp: dayjs().unix(),
+    fingerprint,
+  })
   const { public_key } = await cryptoParams
 
   // Encrypt data with RSA public key