chore: 登录失败也记录日志

This commit is contained in:
xiaojunnuo
2026-07-12 22:28:51 +08:00
parent bab1df2c78
commit bfb3ee4c43
18 changed files with 78 additions and 57 deletions
@@ -18,4 +18,5 @@ export type AuditLogContext = {
scope?: string;
userId?: number;
username?: string;
success?: boolean;
};
@@ -0,0 +1,13 @@
import { createRequestParamDecorator } from "@midwayjs/core";
export const AuditLog = (opts: { enabled?: boolean }) => {
return createRequestParamDecorator(ctx => {
if (!ctx.auditLog) {
ctx.auditLog = {};
}
if (opts.enabled !== undefined) {
ctx.auditLog.enabled = opts.enabled || true;
}
return ctx.auditLog;
});
};
@@ -0,0 +1 @@
export * from "./decoractor.js";
@@ -7,3 +7,4 @@ export * from "./result.js";
export * from "./base-service.js";
export * from "./audit.js";
export * from "./mode.js";
export * from "./core/index.js";
@@ -1,2 +1,4 @@
ALTER TABLE `cd_audit_log` ADD COLUMN `scope` varchar(32) NOT NULL DEFAULT 'user';
CREATE INDEX `index_audit_log_scope` ON `cd_audit_log` (`scope`);
ALTER TABLE `cd_audit_log` ADD COLUMN `success` tinyint(1) NOT NULL DEFAULT 1;
@@ -1,2 +1,4 @@
ALTER TABLE "cd_audit_log" ADD COLUMN "scope" varchar(32) NOT NULL DEFAULT ('user');
CREATE INDEX "index_audit_log_scope" ON "cd_audit_log" ("scope");
ALTER TABLE "cd_audit_log" ADD COLUMN "success" boolean NOT NULL DEFAULT true;
@@ -1,2 +1,4 @@
ALTER TABLE "cd_audit_log" ADD COLUMN "scope" varchar(32) NOT NULL DEFAULT ('user');
CREATE INDEX "index_audit_log_scope" ON "cd_audit_log" ("scope");
ALTER TABLE "cd_audit_log" ADD COLUMN "success" boolean NOT NULL DEFAULT (1);
@@ -134,6 +134,5 @@ export class MainConfiguration {
logger.info(text);
});
logger.info("当前环境:", this.app.getEnv()); // prod
}
}
@@ -54,7 +54,7 @@ export class ForgotPasswordController extends BaseController {
} else {
throw new CommonException("暂不支持的找回类型,请联系管理员找回");
}
const {id,username} = await this.userService.forgotPassword(body);
const { id, username } = await this.userService.forgotPassword(body);
username && this.loginService.clearCacheOnSuccess(username);
this.auditLog({ userId: id, content: "用户请求找回密码" });
return this.ok();
@@ -7,8 +7,6 @@ import { CaptchaService } from "../../../modules/basic/service/captcha-service.j
import { PasskeyService } from "../../../modules/login/service/passkey-service.js";
import { AuditType } from "../../../modules/sys/enterprise/service/audit-constants.js";
/**
*/
@Provide()
@Controller("/api/")
export class LoginController extends BaseController {
@@ -46,10 +44,10 @@ export class LoginController extends BaseController {
try {
const token = await this.loginService.loginByPassword(body);
this.writeTokenCookie(token);
this.auditLog({ userId: token.userId, username: body.username, content: `用户「${body.username}」登录成功` });
this.auditLog({ userId: token.userId, username: token.username, content: `用户「${body.username}」登录成功` });
return this.ok(token);
} catch (err:any) {
this.auditLog({userId:err.userId, username: body.username, content: `用户「${body.username}」登录失败` });
} catch (err: any) {
this.auditLog({userId:err.userId, username: body.username, content: `用户「${body.username}」登录失败${err.message}` });
throw err;
}
}
@@ -79,10 +77,10 @@ export class LoginController extends BaseController {
});
this.writeTokenCookie(token);
this.auditLog({ userId: token.userId, username: body.mobile, content: `用户「${body.mobile}」短信登录成功` });
this.auditLog({ userId: token.userId, username: token.username, content: `用户「${body.mobile}」短信登录成功` });
return this.ok(token);
} catch (err) {
this.auditLog({ userId: err.userId, username: body.mobile, content: `用户「${body.mobile}」短信登录失败` });
} catch (err: any) {
this.auditLog({userId: err.userId, username: body.mobile, content: `用户「${body.mobile}」短信登录失败${err.message}` });
throw err;
}
}
@@ -99,10 +97,10 @@ export class LoginController extends BaseController {
});
this.writeTokenCookie(token);
this.auditLog({ userId: token.userId, username: body.loginId, content: `用户「${body.loginId}」两步验证登录成功` });
this.auditLog({ userId: token.userId, username: token.username, content: `用户「${body.loginId}」两步验证登录成功` });
return this.ok(token);
} catch (err) {
this.auditLog({ userId: err.userId, username: body.loginId, content: `用户「${body.loginId}」两步验证登录失败` });
} catch (err: any) {
this.auditLog({userId: err.userId, username: body.loginId, content: `用户「${body.loginId}」两步验证登录失败${err.message}` });
throw err;
}
}
@@ -132,10 +130,10 @@ export class LoginController extends BaseController {
);
this.writeTokenCookie(token);
this.auditLog({ userId: token.userId, content: "用户Passkey登录成功" });
this.auditLog({ userId: token.userId, username: token.username, content: "用户Passkey登录成功" });
return this.ok(token);
} catch (err) {
this.auditLog({ userId: err.userId, content: "用户Passkey登录失败" });
} catch (err: any) {
this.auditLog({userId: err.userId, username: body.credential, content: `用户Passkey登录失败${err.message}` });
throw err;
}
}
@@ -14,18 +14,18 @@ export class AuditLogController extends BaseController {
@Post("/page", { description: Constants.per.authOnly, summary: "分页查询当前用户操作日志" })
async page(@Body(ALL) body: any) {
const { projectId, userId } = await this.getProjectUserIdRead();
const query: any = { }
const query: any = {};
if (projectId) {
//如果是项目级别,排除userId参数,因为日志这里userId不是-1 ,而是实际的用户
query.projectId = projectId;
}else{
} else {
query.userId = userId;
}
body.query = {
...body.query || {},
...(body.query || {}),
...query,
scope: "user"
}
scope: "user",
};
const pageRet = await this.auditService.page(body);
return this.ok(pageRet);
@@ -33,7 +33,7 @@ export class DnsPersistRecordController extends CrudController<DnsPersistRecordS
const { projectId, userId } = await this.getProjectUserIdWrite();
bean.projectId = projectId;
bean.userId = userId;
const res = await this.getService().add(bean);
const res = await this.getService().add(bean);
this.auditLog({ content: `新增了DNS持久验证记录(ID:${res.id})` });
return this.ok(res);
}
@@ -94,14 +94,15 @@ describe("AuditLogMiddleware", () => {
assert.equal(records.length, 0);
});
it("skips failed response", async () => {
it("writes audit log with success=false on failed response", async () => {
const { middleware, records } = createMiddleware();
const ctx = createCtx();
ctx.body = Constants.res.error;
await middleware.resolve()(ctx, async () => {});
assert.equal(records.length, 0);
assert.equal(records.length, 1);
assert.equal(records[0].success, false);
});
it("skips anonymous request", async () => {
@@ -2,53 +2,47 @@ import { Inject, Provide } from "@midwayjs/core";
import { IMidwayKoaContext, IWebMiddleware, NextFunction } from "@midwayjs/koa";
import { Constants } from "@certd/lib-server";
import { AuditService } from "../modules/sys/enterprise/service/audit-service.js";
import { isPlus } from "@certd/plus-core";
@Provide()
export class AuditLogMiddleware implements IWebMiddleware {
@Inject()
auditService: AuditService;
private isPlusFn: (() => Promise<boolean>) | null = null;
private async getIsPlus(): Promise<boolean> {
if (!this.isPlusFn) {
try {
const mod = await import("@certd/plus-core");
this.isPlusFn = async () => mod.isPlus();
} catch {
this.isPlusFn = async () => false;
}
}
return this.isPlusFn();
}
resolve() {
return async (ctx: IMidwayKoaContext, next: NextFunction) => {
await next();
await this.writeAuditLog(ctx);
try {
await next();
await this.writeAuditLog(ctx);
} catch (err) {
await this.writeAuditLog(ctx, err);
throw err;
}
};
}
private async writeAuditLog(ctx: IMidwayKoaContext) {
private async writeAuditLog(ctx: IMidwayKoaContext, err?: any) {
const routeInfo = ctx.auditRouteInfo;
if (!routeInfo) {
return;
}
if (!ctx.auditLog?.enabled) {
return;
}
const isPlus = await this.getIsPlus();
if (!isPlus) {
return;
}
if (!this.isSuccessResponse(ctx)) {
return;
}
const auditLog = ctx.auditLog;
if (!auditLog.enabled) {
if (!auditLog?.enabled) {
return;
}
if (!isPlus()) {
return;
}
let isSuccess = this.isSuccessResponse(ctx);
if (err) {
isSuccess = false;
if (err?.userId != null && auditLog?.userId == null) {
auditLog.userId = err.userId;
}
}
const type = auditLog.type || (await this.resolveControllerType(routeInfo.controllerClz, ctx as any));
const action = auditLog.action || routeInfo.summary || "";
const append = auditLog.append;
@@ -61,7 +55,7 @@ export class AuditLogMiddleware implements IWebMiddleware {
const projectId = auditLog.projectId ?? ctx.projectId ?? 0;
const scope = auditLog.scope || (ctx.path.startsWith("/api/sys/") ? "system" : "user");
const ipAddress = ctx.ip || "";
const ipAddress = ctx.ip || "";
await this.auditService.log({
userId: auditLog.userId ?? ctx.user?.id ?? 0,
@@ -70,8 +64,10 @@ export class AuditLogMiddleware implements IWebMiddleware {
content,
username: auditLog.username || ctx.user?.username,
projectId,
projectName: auditLog.projectName,
ipAddress,
scope,
success: isSuccess,
});
}
@@ -252,6 +252,7 @@ export class LoginService {
token,
expire,
userId: user.id,
username: user.username,
};
}
@@ -974,7 +974,7 @@ export class PipelineService extends BaseService<PipelineEntity> {
await this.checkUserId(id, projectId, "projectId");
}
await this.delete(id);
ids.push(id);
ids.push(id);
}
return ids.length;
}
@@ -34,6 +34,9 @@ export class AuditLogEntity {
@Column({ name: "ip_address", comment: "IP地址" })
ipAddress: string;
@Column({ name: "success", comment: "是否成功", default: true })
success: boolean;
@Column({
name: "create_time",
comment: "创建时间",
@@ -25,13 +25,13 @@ export class AuditService extends BaseService<AuditLogEntity> {
const end = pageReq.query.createTime[1];
delete pageReq.query.createTime;
pageReq.buildQuery = (qb: any) => {
qb.andWhere("main.createTime BETWEEN :start AND :end", { start, end });
qb.andWhere("main.createTime BETWEEN :start AND :end", { start, end });
};
}
return await super.page(pageReq);
}
async log(params: { userId: number; type: string; action: string; content: string; username?: string; projectId?: number; ipAddress?: string; scope?: string }): Promise<void> {
async log(params: { userId: number; type: string; action: string; content: string; username?: string; projectId?: number; projectName?: string; ipAddress?: string; scope?: string; success?: boolean }): Promise<void> {
try {
let { username } = params;
if (!username && params.userId != null) {
@@ -47,9 +47,10 @@ export class AuditService extends BaseService<AuditLogEntity> {
entity.action = params.action;
entity.content = params.content;
entity.projectId = params.projectId || 0;
entity.projectName = "";
entity.projectName = params.projectName || "";
entity.ipAddress = params.ipAddress || "";
entity.scope = params.scope || "user";
entity.success = params.success ?? true;
await this.repository.save(entity);
} catch (e) {
logger.error("写入审计日志失败:", e);