Skip to content

Commit 7f31d46

Browse files
committed
refactor: implement cookie management and password hashing for improved security
1 parent 4d89c62 commit 7f31d46

File tree

6 files changed

+117
-15
lines changed

6 files changed

+117
-15
lines changed

src/webui/BE/passwordHash.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import crypto from 'node:crypto';
2+
3+
/**
4+
* 使用 SHA-256 对密码进行哈希
5+
* @param password 明文密码
6+
* @returns 哈希后的密码(十六进制字符串)
7+
*/
8+
export function hashPassword(password: string): string {
9+
return crypto.createHash('sha256').update(password).digest('hex');
10+
}

src/webui/BE/server.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { getConfigUtil, webuiTokenUtil } from '@/common/config'
66
import { Config, WebUIConfig } from '@/common/types'
77
import { Server } from 'http'
88
import { Socket } from 'net'
9+
import { hashPassword } from './passwordHash'
910
import { Context, Service } from 'cordis'
1011
import { selfInfo, LOG_DIR } from '@/common/globalVars'
1112
import { getAvailablePort } from '@/common/utils/port'
@@ -146,7 +147,10 @@ export class WebUIServer extends Service {
146147
})
147148
return
148149
}
149-
if (reqToken !== token) {
150+
151+
// 将存储的明文密码 hash 后与前端传来的 hash 对比
152+
const hashedToken = hashPassword(token)
153+
if (reqToken !== hashedToken) {
150154
// 记录失败尝试
151155
globalLoginAttempt.consecutiveFailures++
152156
globalLoginAttempt.lastAttempt = Date.now()

src/webui/FE/App.tsx

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import QQLogin from './components/QQLogin';
88
import { ToastContainer, showToast } from './components/Toast';
99
import AnimatedBackground from './components/AnimatedBackground';
1010
import { Config, ResConfig } from './types';
11-
import { apiFetch, getToken, setPasswordPromptHandler, setTokenStorage } from './utils/api';
11+
import { apiFetch, setPasswordPromptHandler } from './utils/api';
1212
import { Save, Loader2, Settings, Eye, EyeOff } from 'lucide-react';
1313
import { defaultConfig } from '../../common/defaultConfig'
1414
import { version } from '../../version'
@@ -21,7 +21,6 @@ function App() {
2121
const [isLoggedIn, setIsLoggedIn] = useState(false);
2222
const [checkingLogin, setCheckingLogin] = useState(true);
2323
const [accountInfo, setAccountInfo] = useState<{ nick: string; uin: string } | null>(null);
24-
const [token, setToken] = useState(getToken() || '');
2524
const [showPasswordDialog, setShowPasswordDialog] = useState(false);
2625
const [passwordError, setPasswordError] = useState('');
2726
const [passwordResolve, setPasswordResolve] = useState<((value: string) => void) | null>(null);
@@ -91,7 +90,6 @@ function App() {
9190
body: JSON.stringify({ config: finalConfig }),
9291
});
9392
if (response.success) {
94-
setTokenStorage(token);
9593
showToast('配置保存成功', 'success');
9694
} else {
9795
showToast('保存失败:' + response.message, 'error');
@@ -101,7 +99,7 @@ function App() {
10199
} finally {
102100
setLoading(false);
103101
}
104-
}, [config, token]); // 依赖 config 和 token
102+
}, [config]); // 依赖 config
105103

106104
// 登录成功回调
107105
const handleLoginSuccess = useCallback(() => {

src/webui/FE/utils/api.ts

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,23 @@
11
import { ApiResponse } from '../types';
2-
import { showToast } from '../components/Toast'
2+
import { showToast } from '../components/Toast';
3+
import { hashPassword } from './passwordHash';
4+
import { getCookie, setCookie } from './cookie';
35

46
const TOKEN_KEY = 'webui_token';
7+
const TOKEN_EXPIRY_DAYS = 30; // Cookie 过期天数
8+
59
let passwordPromptHandler: ((tip: string) => Promise<string>) | null = null;
610

711
export function setPasswordPromptHandler(handler: (tip: string) => Promise<string>) {
812
passwordPromptHandler = handler;
913
}
1014

1115
export function getToken(): string | null {
12-
return localStorage.getItem(TOKEN_KEY);
16+
return getCookie(TOKEN_KEY);
1317
}
1418

1519
export function setTokenStorage(token: string) {
16-
localStorage.setItem(TOKEN_KEY, token);
20+
setCookie(TOKEN_KEY, token, TOKEN_EXPIRY_DAYS);
1721
}
1822

1923
export async function apiFetch<T = any>(
@@ -49,7 +53,7 @@ export async function apiFetch<T = any>(
4953
throw new Error('密码不能为空');
5054
}
5155

52-
// 调用设置密码接口
56+
// 调用设置密码接口(传送明文)
5357
const setTokenResponse = await fetch('/api/set-token', {
5458
method: 'POST',
5559
headers: { 'Content-Type': 'application/json' },
@@ -60,10 +64,12 @@ export async function apiFetch<T = any>(
6064
throw new Error('设置密码失败');
6165
}
6266

63-
setTokenStorage(newPassword.trim());
67+
// 存储 hash 后的密码
68+
const hashedPassword = await hashPassword(newPassword.trim());
69+
setTokenStorage(hashedPassword);
6470

65-
// 重新请求原接口
66-
response = await makeRequest(newPassword.trim());
71+
// 重新请求原接口(使用 hash)
72+
response = await makeRequest(hashedPassword);
6773
}
6874

6975
// 403: 密码错误或账户锁定,需要重新输入
@@ -96,10 +102,12 @@ export async function apiFetch<T = any>(
96102
throw new Error('密码不能为空');
97103
}
98104

99-
setTokenStorage(newPassword.trim());
105+
// 存储 hash 后的密码
106+
const hashedPassword = await hashPassword(newPassword.trim());
107+
setTokenStorage(hashedPassword);
100108

101-
// 重新请求
102-
response = await makeRequest(newPassword.trim());
109+
// 重新请求(使用 hash)
110+
response = await makeRequest(hashedPassword);
103111

104112
if (response.status === 200) {
105113
console.log('Authentication successful!');

src/webui/FE/utils/cookie.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/**
2+
* Cookie 工具函数
3+
*/
4+
5+
/**
6+
* 设置 Cookie
7+
* @param name Cookie 名称
8+
* @param value Cookie 值
9+
* @param days 过期天数(默认 30 天)
10+
*/
11+
export function setCookie(name: string, value: string, days: number = 30): void {
12+
const date = new Date();
13+
date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000);
14+
const expires = `expires=${date.toUTCString()}`;
15+
16+
// 设置 Cookie 属性
17+
// SameSite=Strict: 防止 CSRF 攻击
18+
// Secure: 仅在 HTTPS 下传输(开发环境可能是 HTTP,所以条件判断)
19+
// HttpOnly: 无法通过 JS 设置(但我们需要 JS 访问,所以不设置)
20+
const isSecure = window.location.protocol === 'https:';
21+
const secureFlag = isSecure ? '; Secure' : '';
22+
23+
document.cookie = `${name}=${value}; ${expires}; path=/; SameSite=Strict${secureFlag}`;
24+
}
25+
26+
/**
27+
* 获取 Cookie
28+
* @param name Cookie 名称
29+
* @returns Cookie 值,如果不存在返回 null
30+
*/
31+
export function getCookie(name: string): string | null {
32+
const nameEQ = name + '=';
33+
const ca = document.cookie.split(';');
34+
35+
for (let i = 0; i < ca.length; i++) {
36+
let c = ca[i];
37+
while (c.charAt(0) === ' ') c = c.substring(1, c.length);
38+
if (c.indexOf(nameEQ) === 0) return c.substring(nameEQ.length, c.length);
39+
}
40+
41+
return null;
42+
}
43+
44+
/**
45+
* 删除 Cookie
46+
* @param name Cookie 名称
47+
*/
48+
export function deleteCookie(name: string): void {
49+
document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`;
50+
}

src/webui/FE/utils/passwordHash.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/**
2+
* 密码哈希工具
3+
* 使用 SHA-256 对密码进行哈希处理
4+
*/
5+
6+
/**
7+
* 将字符串转换为 SHA-256 哈希
8+
* @param message 要哈希的字符串
9+
* @returns 十六进制格式的哈希值
10+
*/
11+
export async function sha256(message: string): Promise<string> {
12+
// 将字符串编码为 UTF-8
13+
const msgBuffer = new TextEncoder().encode(message);
14+
15+
// 计算哈希
16+
const hashBuffer = await crypto.subtle.digest('SHA-256', msgBuffer);
17+
18+
// 转换为十六进制字符串
19+
const hashArray = Array.from(new Uint8Array(hashBuffer));
20+
const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
21+
22+
return hashHex;
23+
}
24+
25+
/**
26+
* 对密码进行哈希处理
27+
* @param password 明文密码
28+
* @returns 哈希后的密码
29+
*/
30+
export async function hashPassword(password: string): Promise<string> {
31+
return await sha256(password);
32+
}

0 commit comments

Comments
 (0)