mirror of
https://github.com/NapNeko/NapCatQQ.git
synced 2025-07-19 12:03:37 +00:00
feat: 加强安全性 传输过程使用salt sha256
This commit is contained in:
@@ -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",
|
||||||
|
@@ -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
|
||||||
}
|
}
|
||||||
|
@@ -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'
|
||||||
);
|
);
|
||||||
// 返回成功信息
|
// 返回成功信息
|
||||||
|
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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) {
|
||||||
// 通过验证
|
// 通过验证
|
||||||
|
2
src/webui/src/types/sign_token.d.ts
vendored
2
src/webui/src/types/sign_token.d.ts
vendored
@@ -1,6 +1,6 @@
|
|||||||
interface WebUiCredentialInnerJson {
|
interface WebUiCredentialInnerJson {
|
||||||
CreatedTime: number;
|
CreatedTime: number;
|
||||||
TokenEncoded: string;
|
HashEncoded: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface WebUiCredentialJson {
|
interface WebUiCredentialJson {
|
||||||
|
Reference in New Issue
Block a user