feat: 加强安全性 传输过程使用salt sha256

This commit is contained in:
手瓜一十雪
2025-04-19 19:50:52 +08:00
parent b1047309c9
commit fda050d3fe
6 changed files with 40 additions and 17 deletions

View File

@@ -55,6 +55,7 @@
"ahooks": "^3.8.4", "ahooks": "^3.8.4",
"axios": "^1.7.9", "axios": "^1.7.9",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"crypto-js": "^4.2.0",
"echarts": "^5.5.1", "echarts": "^5.5.1",
"event-source-polyfill": "^1.0.31", "event-source-polyfill": "^1.0.31",
"framer-motion": "^12.0.6", "framer-motion": "^12.0.6",
@@ -88,6 +89,7 @@
"@eslint/js": "^9.19.0", "@eslint/js": "^9.19.0",
"@react-types/shared": "^3.26.0", "@react-types/shared": "^3.26.0",
"@trivago/prettier-plugin-sort-imports": "^5.2.2", "@trivago/prettier-plugin-sort-imports": "^5.2.2",
"@types/crypto-js": "^4.2.2",
"@types/event-source-polyfill": "^1.0.5", "@types/event-source-polyfill": "^1.0.5",
"@types/fabric": "^5.3.9", "@types/fabric": "^5.3.9",
"@types/node": "^22.12.0", "@types/node": "^22.12.0",

View File

@@ -3,7 +3,7 @@ import { EventSourcePolyfill } from 'event-source-polyfill'
import { LogLevel } from '@/const/enum' import { LogLevel } from '@/const/enum'
import { serverRequest } from '@/utils/request' import { serverRequest } from '@/utils/request'
import CryptoJS from "crypto-js";
export interface Log { export interface Log {
level: LogLevel level: LogLevel
message: string message: string
@@ -17,9 +17,10 @@ export default class WebUIManager {
} }
public static async loginWithToken(token: string) { public static async loginWithToken(token: string) {
const sha256 = CryptoJS.SHA256(token + '.napcat').toString();
const { data } = await serverRequest.post<ServerResponse<AuthResponse>>( const { data } = await serverRequest.post<ServerResponse<AuthResponse>>(
'/auth/login', '/auth/login',
{ token } { hash: sha256 }
) )
return data.data.Credential return data.data.Credential
} }

View File

@@ -20,25 +20,26 @@ export const CheckDefaultTokenHandler: RequestHandler = async (_, res) => {
export const LoginHandler: RequestHandler = async (req, res) => { export const LoginHandler: RequestHandler = async (req, res) => {
// 获取WebUI配置 // 获取WebUI配置
const WebUiConfigData = await WebUiConfig.GetWebUIConfig(); const WebUiConfigData = await WebUiConfig.GetWebUIConfig();
// 获取请求体中的token // 获取请求体中的hash
const { token } = req.body; const { hash } = req.body;
// 获取客户端IP // 获取客户端IP
const clientIP = req.ip || req.socket.remoteAddress || ''; const clientIP = req.ip || req.socket.remoteAddress || '';
// 如果token为空返回错误信息 // 如果token为空返回错误信息
if (isEmpty(token)) { if (isEmpty(hash)) {
return sendError(res, 'token is empty'); return sendError(res, 'token is empty');
} }
// 检查登录频率 // 检查登录频率
if (!WebUiDataRuntime.checkLoginRate(clientIP, WebUiConfigData.loginRate)) { if (!WebUiDataRuntime.checkLoginRate(clientIP, WebUiConfigData.loginRate)) {
return sendError(res, 'login rate limit'); return sendError(res, 'login rate limit');
} }
//验证config.token是否等于token //验证config.token hash是否等于token hash
if (WebUiConfigData.token !== token) { if (!AuthHelper.comparePasswordHash(WebUiConfigData.token, hash)) {
return sendError(res, 'token is invalid'); return sendError(res, 'token is invalid');
} }
// 签发凭证 // 签发凭证
const signCredential = Buffer.from(JSON.stringify(AuthHelper.signCredential(WebUiConfigData.token))).toString( const signCredential = Buffer.from(JSON.stringify(AuthHelper.signCredential(hash))).toString(
'base64' 'base64'
); );
// 返回成功信息 // 返回成功信息

View File

@@ -5,13 +5,13 @@ export class AuthHelper {
/** /**
* 签名凭证方法。 * 签名凭证方法。
* @param token 待签名的凭证字符串。 * @param hash 待签名的凭证字符串。
* @returns 签名后的凭证对象。 * @returns 签名后的凭证对象。
*/ */
public static signCredential(token: string): WebUiCredentialJson { public static signCredential(hash: string): WebUiCredentialJson {
const innerJson: WebUiCredentialInnerJson = { const innerJson: WebUiCredentialInnerJson = {
CreatedTime: Date.now(), CreatedTime: Date.now(),
TokenEncoded: token, HashEncoded: hash,
}; };
const jsonString = JSON.stringify(innerJson); const jsonString = JSON.stringify(innerJson);
const hmac = crypto.createHmac('sha256', AuthHelper.secretKey).update(jsonString, 'utf8').digest('hex'); const hmac = crypto.createHmac('sha256', AuthHelper.secretKey).update(jsonString, 'utf8').digest('hex');
@@ -57,8 +57,7 @@ export class AuthHelper {
const currentTime = Date.now() / 1000; const currentTime = Date.now() / 1000;
const createdTime = credentialJson.Data.CreatedTime; const createdTime = credentialJson.Data.CreatedTime;
const timeDifference = currentTime - createdTime; const timeDifference = currentTime - createdTime;
return timeDifference <= 3600 && credentialJson.Data.HashEncoded === AuthHelper.generatePasswordHash(token);
return timeDifference <= 3600 && credentialJson.Data.TokenEncoded === token;
} }
/** /**
@@ -85,4 +84,23 @@ export class AuthHelper {
return store.exists(`revoked:${hmac}`) > 0; return store.exists(`revoked:${hmac}`) > 0;
} }
/**
* 生成密码Hash
* @param password 密码
* @returns 生成的Hash值
*/
public static generatePasswordHash(password: string): string {
return crypto.createHash('sha256').update(password + '.napcat').digest().toString('hex')
}
/**
* 对比密码和Hash值
* @param password 密码
* @param hash Hash值
* @returns 布尔值表示密码是否匹配Hash值
*/
public static comparePasswordHash(password: string, hash: string): boolean {
return this.generatePasswordHash(password) === hash;
}
} }

View File

@@ -21,17 +21,18 @@ export async function auth(req: Request, res: Response, next: NextFunction) {
return sendError(res, 'Unauthorized'); return sendError(res, 'Unauthorized');
} }
// 获取token // 获取token
const token = authorization[1]; const hash = authorization[1];
if(!hash) return sendError(res, 'Unauthorized');
// 解析token // 解析token
let Credential: WebUiCredentialJson; let Credential: WebUiCredentialJson;
try { try {
Credential = JSON.parse(Buffer.from(token, 'base64').toString('utf-8')); Credential = JSON.parse(Buffer.from(hash, 'base64').toString('utf-8'));
} catch (e) { } catch (e) {
return sendError(res, 'Unauthorized'); return sendError(res, 'Unauthorized');
} }
// 获取配置 // 获取配置
const config = await WebUiConfig.GetWebUIConfig(); const config = await WebUiConfig.GetWebUIConfig();
// 验证凭证在1小时内有效且token与原始token相同 // 验证凭证在1小时内有效
const credentialJson = AuthHelper.validateCredentialWithinOneHour(config.token, Credential); const credentialJson = AuthHelper.validateCredentialWithinOneHour(config.token, Credential);
if (credentialJson) { if (credentialJson) {
// 通过验证 // 通过验证

View File

@@ -1,6 +1,6 @@
interface WebUiCredentialInnerJson { interface WebUiCredentialInnerJson {
CreatedTime: number; CreatedTime: number;
TokenEncoded: string; HashEncoded: string;
} }
interface WebUiCredentialJson { interface WebUiCredentialJson {