feat: 登录状态机

This commit is contained in:
bietiaop
2025-01-31 18:48:46 +08:00
parent 2b5b825d9a
commit 3496494616
12 changed files with 470 additions and 65 deletions

View File

@@ -13,12 +13,15 @@ export const LoginHandler: RequestHandler = async (req, res) => {
const WebUiConfigData = await WebUiConfig.GetWebUIConfig();
// 获取请求体中的token
const { token } = req.body;
// 获取客户端IP
const clientIP = req.ip || req.socket.remoteAddress || '';
// 如果token为空返回错误信息
if (isEmpty(token)) {
return sendError(res, 'token is empty');
}
// 检查登录频率
if (!WebUiDataRuntime.checkLoginRate(WebUiConfigData.loginRate)) {
if (!WebUiDataRuntime.checkLoginRate(clientIP, WebUiConfigData.loginRate)) {
return sendError(res, 'login rate limit');
}
//验证config.token是否等于token
@@ -26,7 +29,7 @@ export const LoginHandler: RequestHandler = async (req, res) => {
return sendError(res, 'token is invalid');
}
// 签发凭证
const signCredential = Buffer.from(JSON.stringify(await AuthHelper.signCredential(WebUiConfigData.token))).toString(
const signCredential = Buffer.from(JSON.stringify(AuthHelper.signCredential(WebUiConfigData.token))).toString(
'base64'
);
// 返回成功信息
@@ -36,9 +39,16 @@ export const LoginHandler: RequestHandler = async (req, res) => {
};
// 退出登录
export const LogoutHandler: RequestHandler = (_, res) => {
// TODO: 这玩意无状态销毁个灯 得想想办法
return sendSuccess(res, null);
export const LogoutHandler: RequestHandler = async (req, res) => {
const authorization = req.headers.authorization;
try {
const CredentialBase64: string = authorization?.split(' ')[1] as string;
const Credential = JSON.parse(Buffer.from(CredentialBase64, 'base64').toString());
AuthHelper.revokeCredential(Credential);
return sendSuccess(res, 'Logged out successfully');
} catch (e) {
return sendError(res, 'Logout failed');
}
};
// 检查登录状态
@@ -53,25 +63,41 @@ export const checkHandler: RequestHandler = async (req, res) => {
const CredentialBase64: string = authorization?.split(' ')[1] as string;
// 解析凭证
const Credential = JSON.parse(Buffer.from(CredentialBase64, 'base64').toString());
// 检查凭证是否已被注销
if (AuthHelper.isCredentialRevoked(Credential)) {
return sendError(res, 'Token has been revoked');
}
// 验证凭证是否在一小时内有效
await AuthHelper.validateCredentialWithinOneHour(WebUiConfigData.token, Credential);
const valid = AuthHelper.validateCredentialWithinOneHour(WebUiConfigData.token, Credential);
// 返回成功信息
return sendSuccess(res, null);
if (valid) return sendSuccess(res, null);
// 返回错误信息
return sendError(res, 'Authorization Failed');
} catch (e) {
// 返回错误信息
return sendError(res, 'Authorization Faild');
return sendError(res, 'Authorization Failed');
}
};
// 修改密码token
export const UpdateTokenHandler: RequestHandler = async (req, res) => {
const { oldToken, newToken } = req.body;
const authorization = req.headers.authorization;
if (isEmpty(oldToken) || isEmpty(newToken)) {
return sendError(res, 'oldToken or newToken is empty');
}
try {
// 注销当前的Token
if (authorization) {
const CredentialBase64: string = authorization.split(' ')[1];
const Credential = JSON.parse(Buffer.from(CredentialBase64, 'base64').toString());
AuthHelper.revokeCredential(Credential);
}
await WebUiConfig.UpdateToken(oldToken, newToken);
return sendSuccess(res, 'Token updated successfully');
} catch (e: any) {

View File

@@ -1,5 +1,7 @@
import type { LoginRuntimeType } from '../types/data';
import packageJson from '../../../../package.json';
import store from '@/common/store';
const LoginRuntime: LoginRuntimeType = {
LoginCurrentTime: Date.now(),
LoginCurrentRate: 0,
@@ -26,15 +28,22 @@ const LoginRuntime: LoginRuntimeType = {
};
export const WebUiDataRuntime = {
checkLoginRate(RateLimit: number): boolean {
LoginRuntime.LoginCurrentRate++;
//console.log(RateLimit, LoginRuntime.LoginCurrentRate, Date.now() - LoginRuntime.LoginCurrentTime);
if (Date.now() - LoginRuntime.LoginCurrentTime > 1000 * 60) {
LoginRuntime.LoginCurrentRate = 0; //超出时间重置限速
LoginRuntime.LoginCurrentTime = Date.now();
checkLoginRate(ip: string, RateLimit: number): boolean {
const key = `login_rate:${ip}`;
const count = store.get<number>(key) || 0;
if (count === 0) {
// 第一次访问设置计数器为1并设置60秒过期
store.set(key, 1, 60);
return true;
}
return LoginRuntime.LoginCurrentRate <= RateLimit;
if (count >= RateLimit) {
return false;
}
store.incr(key);
return true;
},
getQQLoginStatus(): LoginRuntimeType['QQLoginStatus'] {
@@ -108,5 +117,5 @@ export const WebUiDataRuntime = {
getQQVersion() {
return LoginRuntime.QQVersion;
}
},
};

View File

@@ -1,5 +1,5 @@
import crypto from 'crypto';
import store from '@/common/store';
export class AuthHelper {
private static readonly secretKey = Math.random().toString(36).slice(2);
@@ -8,7 +8,7 @@ export class AuthHelper {
* @param token 待签名的凭证字符串。
* @returns 签名后的凭证对象。
*/
public static async signCredential(token: string): Promise<WebUiCredentialJson> {
public static signCredential(token: string): WebUiCredentialJson {
const innerJson: WebUiCredentialInnerJson = {
CreatedTime: Date.now(),
TokenEncoded: token,
@@ -23,7 +23,7 @@ export class AuthHelper {
* @param credentialJson 凭证的JSON对象。
* @returns 布尔值,表示凭证是否有效。
*/
public static async checkCredential(credentialJson: WebUiCredentialJson): Promise<boolean> {
public static checkCredential(credentialJson: WebUiCredentialJson): boolean {
try {
const jsonString = JSON.stringify(credentialJson.Data);
const calculatedHmac = crypto
@@ -42,19 +42,47 @@ export class AuthHelper {
* @param credentialJson 已签名的凭证JSON对象。
* @returns 布尔值表示凭证是否有效且token匹配。
*/
public static async validateCredentialWithinOneHour(
token: string,
credentialJson: WebUiCredentialJson
): Promise<boolean> {
const isValid = await AuthHelper.checkCredential(credentialJson);
public static validateCredentialWithinOneHour(token: string, credentialJson: WebUiCredentialJson): boolean {
// 首先检查凭证是否被篡改
const isValid = AuthHelper.checkCredential(credentialJson);
if (!isValid) {
return false;
}
// 检查凭证是否在黑名单中
if (AuthHelper.isCredentialRevoked(credentialJson)) {
return false;
}
const currentTime = Date.now() / 1000;
const createdTime = credentialJson.Data.CreatedTime;
const timeDifference = currentTime - createdTime;
return timeDifference <= 3600 && credentialJson.Data.TokenEncoded === token;
}
/**
* 注销指定的Token凭证
* @param credentialJson 凭证JSON对象
* @returns void
*/
public static revokeCredential(credentialJson: WebUiCredentialJson): void {
const jsonString = JSON.stringify(credentialJson.Data);
const hmac = crypto.createHmac('sha256', AuthHelper.secretKey).update(jsonString, 'utf8').digest('hex');
// 将已注销的凭证添加到黑名单中有效期1小时
store.set(`revoked:${hmac}`, true, 3600);
}
/**
* 检查凭证是否已被注销
* @param credentialJson 凭证JSON对象
* @returns 布尔值,表示凭证是否已被注销
*/
public static isCredentialRevoked(credentialJson: WebUiCredentialJson): boolean {
const jsonString = JSON.stringify(credentialJson.Data);
const hmac = crypto.createHmac('sha256', AuthHelper.secretKey).update(jsonString, 'utf8').digest('hex');
return store.exists(`revoked:${hmac}`) > 0;
}
}

View File

@@ -32,7 +32,7 @@ export async function auth(req: Request, res: Response, next: NextFunction) {
// 获取配置
const config = await WebUiConfig.GetWebUIConfig();
// 验证凭证在1小时内有效且token与原始token相同
const credentialJson = await AuthHelper.validateCredentialWithinOneHour(config.token, Credential);
const credentialJson = AuthHelper.validateCredentialWithinOneHour(config.token, Credential);
if (credentialJson) {
// 通过验证
return next();