feat(api/web): 实现API请求签名验证功能并优化测试环境配置
refactor(db): 重构查询条件类到query目录下 test: 添加登录流程测试脚本和测试数据 chore: 添加crypto-js依赖用于签名验证 ci: 配置测试环境数据库和端口设置
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
import { test, expect } from '@playwright/test'
|
||||
|
||||
test.describe('登录签名测试', () => {
|
||||
test('登录功能应该正常工作', async ({ page }) => {
|
||||
page.on('console', msg => {
|
||||
console.log('BROWSER CONSOLE:', msg.type(), msg.text())
|
||||
})
|
||||
|
||||
page.on('pageerror', error => {
|
||||
console.error('PAGE ERROR:', error.message)
|
||||
})
|
||||
|
||||
page.on('requestfailed', request => {
|
||||
console.error('REQUEST FAILED:', request.url(), request.failure()?.errorText)
|
||||
})
|
||||
|
||||
await page.goto('/login')
|
||||
|
||||
await page.fill('input[placeholder="请输入用户名"]', 'admin')
|
||||
await page.fill('input[placeholder="请输入密码"]', 'admin123')
|
||||
|
||||
await page.click('button:has-text("登录")')
|
||||
|
||||
await page.waitForURL('**/dashboard', { timeout: 10000 })
|
||||
|
||||
console.log('Current URL after login:', page.url())
|
||||
|
||||
const token = await page.evaluate(() => localStorage.getItem('token'))
|
||||
console.log('Token in localStorage:', token ? 'exists' : 'not found')
|
||||
|
||||
expect(page.url()).toContain('/dashboard')
|
||||
expect(token).toBeTruthy()
|
||||
})
|
||||
})
|
||||
@@ -32,6 +32,7 @@
|
||||
"dependencies": {
|
||||
"@element-plus/icons-vue": "^2.3.2",
|
||||
"axios": "^1.6.2",
|
||||
"crypto-js": "^4.2.0",
|
||||
"dayjs": "^1.11.10",
|
||||
"element-plus": "^2.13.5",
|
||||
"pinia": "^3.0.4",
|
||||
@@ -41,6 +42,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.40.1",
|
||||
"@types/crypto-js": "^4.2.2",
|
||||
"@types/node": "^20.10.0",
|
||||
"@typescript-eslint/eslint-plugin": "^6.18.1",
|
||||
"@typescript-eslint/parser": "^6.18.1",
|
||||
|
||||
@@ -6,7 +6,7 @@ const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
const isHeadless = process.env.PLAYWRIGHT_HEADLESS === 'true' || process.env.CI === 'true';
|
||||
const baseURL = process.env.TEST_BASE_URL || process.env.VITE_BASE_URL || 'http://localhost:3001';
|
||||
const baseURL = process.env.TEST_BASE_URL || process.env.VITE_BASE_URL || 'http://localhost:3002';
|
||||
|
||||
export default defineConfig({
|
||||
testDir: './e2e',
|
||||
@@ -98,7 +98,7 @@ export default defineConfig({
|
||||
|
||||
webServer: {
|
||||
command: 'npm run dev',
|
||||
url: 'http://localhost:3001',
|
||||
url: 'http://localhost:3002',
|
||||
reuseExistingServer: !process.env.CI,
|
||||
timeout: 120000,
|
||||
stdout: 'pipe',
|
||||
|
||||
Generated
+16
@@ -14,6 +14,9 @@ importers:
|
||||
axios:
|
||||
specifier: ^1.6.2
|
||||
version: 1.13.6
|
||||
crypto-js:
|
||||
specifier: ^4.2.0
|
||||
version: 4.2.0
|
||||
dayjs:
|
||||
specifier: ^1.11.10
|
||||
version: 1.11.20
|
||||
@@ -36,6 +39,9 @@ importers:
|
||||
'@playwright/test':
|
||||
specifier: ^1.40.1
|
||||
version: 1.58.2
|
||||
'@types/crypto-js':
|
||||
specifier: ^4.2.2
|
||||
version: 4.2.2
|
||||
'@types/node':
|
||||
specifier: ^20.10.0
|
||||
version: 20.19.37
|
||||
@@ -552,6 +558,9 @@ packages:
|
||||
'@types/chai@5.2.3':
|
||||
resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==}
|
||||
|
||||
'@types/crypto-js@4.2.2':
|
||||
resolution: {integrity: sha512-sDOLlVbHhXpAUAL0YHDUUwDZf3iN4Bwi4W6a0W0b+QcAezUbRtH4FVb+9J4h+XFPW7l/gQ9F8qC7P+Ec4k8QVQ==}
|
||||
|
||||
'@types/deep-eql@4.0.2':
|
||||
resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==}
|
||||
|
||||
@@ -891,6 +900,9 @@ packages:
|
||||
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
|
||||
engines: {node: '>= 8'}
|
||||
|
||||
crypto-js@4.2.0:
|
||||
resolution: {integrity: sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==}
|
||||
|
||||
css-tree@3.2.1:
|
||||
resolution: {integrity: sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==}
|
||||
engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0}
|
||||
@@ -2231,6 +2243,8 @@ snapshots:
|
||||
'@types/deep-eql': 4.0.2
|
||||
assertion-error: 2.0.1
|
||||
|
||||
'@types/crypto-js@4.2.2': {}
|
||||
|
||||
'@types/deep-eql@4.0.2': {}
|
||||
|
||||
'@types/estree@1.0.8': {}
|
||||
@@ -2660,6 +2674,8 @@ snapshots:
|
||||
shebang-command: 2.0.0
|
||||
which: 2.0.2
|
||||
|
||||
crypto-js@4.2.0: {}
|
||||
|
||||
css-tree@3.2.1:
|
||||
dependencies:
|
||||
mdn-data: 2.27.1
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import axios from 'axios'
|
||||
import axios, { AxiosRequestConfig } from 'axios'
|
||||
import { generateSignatureHeaders } from './signature'
|
||||
|
||||
const request = axios.create({
|
||||
baseURL: '/api',
|
||||
@@ -6,11 +7,23 @@ const request = axios.create({
|
||||
})
|
||||
|
||||
request.interceptors.request.use(
|
||||
(config) => {
|
||||
(config: AxiosRequestConfig) => {
|
||||
const token = localStorage.getItem('token')
|
||||
if (token) {
|
||||
config.headers = config.headers || {}
|
||||
config.headers.Authorization = `Bearer ${token}`
|
||||
}
|
||||
|
||||
const method = config.method?.toUpperCase() || 'GET'
|
||||
const url = config.url || ''
|
||||
const body = config.data
|
||||
|
||||
const fullPath = `/api${url.startsWith('/') ? url : '/' + url}`
|
||||
const signatureHeaders = generateSignatureHeaders(method, fullPath, body)
|
||||
|
||||
config.headers = config.headers || {}
|
||||
Object.assign(config.headers, signatureHeaders)
|
||||
|
||||
return config
|
||||
},
|
||||
(error) => Promise.reject(error)
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
import CryptoJS from 'crypto-js'
|
||||
|
||||
const SIGNATURE_SECRET = 'NovalonManageSystemSecretKey2026'
|
||||
|
||||
export interface SignatureHeaders {
|
||||
'X-Signature': string
|
||||
'X-Timestamp': string
|
||||
'X-Nonce': string
|
||||
}
|
||||
|
||||
export function generateSignature(
|
||||
method: string,
|
||||
path: string,
|
||||
query: string = '',
|
||||
body: string = '',
|
||||
timestamp: number,
|
||||
nonce: string
|
||||
): string {
|
||||
const stringToSign = buildStringToSign(method, path, query, body, timestamp, nonce)
|
||||
|
||||
const signature = CryptoJS.HmacSHA256(stringToSign, SIGNATURE_SECRET)
|
||||
const signatureBase64 = CryptoJS.enc.Base64.stringify(signature)
|
||||
|
||||
return signatureBase64
|
||||
}
|
||||
|
||||
export function generateSignatureHeaders(
|
||||
method: string,
|
||||
url: string,
|
||||
body?: any
|
||||
): SignatureHeaders {
|
||||
const timestamp = Date.now()
|
||||
const nonce = generateNonce()
|
||||
|
||||
const { path, query } = parseUrl(url)
|
||||
const bodyString = ''
|
||||
|
||||
const signature = generateSignature(
|
||||
method.toUpperCase(),
|
||||
path,
|
||||
query || '',
|
||||
bodyString,
|
||||
timestamp,
|
||||
nonce
|
||||
)
|
||||
|
||||
return {
|
||||
'X-Signature': signature,
|
||||
'X-Timestamp': timestamp.toString(),
|
||||
'X-Nonce': nonce
|
||||
}
|
||||
}
|
||||
|
||||
function buildStringToSign(
|
||||
method: string,
|
||||
path: string,
|
||||
query: string,
|
||||
body: string,
|
||||
timestamp: number,
|
||||
nonce: string
|
||||
): string {
|
||||
return [
|
||||
method,
|
||||
path,
|
||||
query || '',
|
||||
body || '',
|
||||
timestamp.toString(),
|
||||
nonce
|
||||
].join('\n')
|
||||
}
|
||||
|
||||
function generateNonce(): string {
|
||||
const timestamp = Date.now().toString(36)
|
||||
const randomPart = Math.random().toString(36).substring(2, 15)
|
||||
return `${timestamp}-${randomPart}`
|
||||
}
|
||||
|
||||
function parseUrl(url: string): { path: string; query: string } {
|
||||
if (url.startsWith('http://') || url.startsWith('https://')) {
|
||||
const urlObj = new URL(url)
|
||||
return {
|
||||
path: urlObj.pathname,
|
||||
query: urlObj.search.substring(1)
|
||||
}
|
||||
}
|
||||
|
||||
const queryIndex = url.indexOf('?')
|
||||
if (queryIndex === -1) {
|
||||
return { path: url, query: '' }
|
||||
}
|
||||
|
||||
return {
|
||||
path: url.substring(0, queryIndex),
|
||||
query: url.substring(queryIndex + 1)
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@ export default defineConfig({
|
||||
strictPort: true,
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:8084',
|
||||
target: 'http://localhost:8080',
|
||||
changeOrigin: true,
|
||||
secure: false
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user