安全加固:增加后台登录 IP 限流并记录验证结果

This commit is contained in:
yml2213
2026-08-30 16:47:16 +08:00
parent d24c69141a
commit 6ca54e468e
10 changed files with 527 additions and 6 deletions
@@ -0,0 +1,113 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import type { NextFunction, Request, Response } from 'express'
import { createRateLimitMiddleware, resetRateLimitBucketsForTest } from './rate-limit.js'
test('createRateLimitMiddleware allows requests within the window', () => {
resetRateLimitBucketsForTest()
const limiter = createRateLimitMiddleware({
scope: 'test:allow',
windowMs: 60_000,
max: 2,
})
const req = createMockRequest()
const res = createMockResponse()
let nextCount = 0
const next: NextFunction = () => {
nextCount += 1
}
limiter(req, res, next)
limiter(req, res, next)
assert.equal(nextCount, 2)
assert.equal(res.statusCode, 200)
})
test('createRateLimitMiddleware blocks requests over the limit', () => {
resetRateLimitBucketsForTest()
const limiter = createRateLimitMiddleware({
scope: 'test:block',
windowMs: 60_000,
max: 1,
})
const req = createMockRequest()
const res = createMockResponse()
let nextCount = 0
const next: NextFunction = () => {
nextCount += 1
}
limiter(req, res, next)
limiter(req, res, next)
assert.equal(nextCount, 1)
assert.equal(res.statusCode, 429)
assert.equal(res.headers['Retry-After'], '60')
assert.equal(res.body.errorCode, 'rate_limited')
})
test('createRateLimitMiddleware supports custom limit responses', () => {
resetRateLimitBucketsForTest()
const limiter = createRateLimitMiddleware({
scope: 'test:custom',
windowMs: 60_000,
max: 1,
onLimit: (_req, res) => {
res.status(200).json({ code: 429, message: 'limited' })
},
})
const req = createMockRequest()
const res = createMockResponse()
limiter(req, res, () => {})
limiter(req, res, () => {})
assert.equal(res.statusCode, 200)
assert.deepEqual(res.body, { code: 429, message: 'limited' })
})
function createMockRequest(): Request {
return {
ip: '127.0.0.1',
originalUrl: '/test',
headers: {},
socket: {
remoteAddress: '127.0.0.1',
},
} as Request
}
function createMockResponse(): Response & {
statusCode: number
headers: Record<string, string>
body: any
} {
const res = {
statusCode: 200,
headers: {} as Record<string, string>,
body: null as any,
setHeader(name: string, value: string) {
this.headers[name] = value
return this
},
status(statusCode: number) {
this.statusCode = statusCode
return this
},
json(body: any) {
this.body = body
return this
},
}
return res as Response & {
statusCode: number
headers: Record<string, string>
body: any
}
}
@@ -0,0 +1,59 @@
import { Router } from 'express'
import { createRateLimitMiddleware, getBodyFieldRateLimitKey } from '../../middleware/rate-limit.js'
import {
getAdminSessionSummary,
loginAdmin,
logoutAdmin,
} from '../../services/admin/admin-auth-service.js'
import {
resolveClientIp,
resolveClientLocation,
resolveUserAgent,
} from '../../services/admin/admin-login-log-service.js'
import { createJsonHandler, extractBearerToken } from './session.js'
const router = Router()
router.post(
'/auth/login',
createRateLimitMiddleware({
scope: 'admin:login',
windowMs: 60_000,
max: 10,
key: getBodyFieldRateLimitKey('username'),
}),
createJsonHandler(
(req) =>
loginAdmin(req.body?.username, req.body?.password, {
ip: resolveClientIp(req),
userAgent: resolveUserAgent(req),
location: resolveClientLocation(req),
}),
{
successMessage: '登录成功',
errorMessage: '后台登录失败',
scope: '[admin/auth/login]',
},
),
)
router.get(
'/auth/session',
createJsonHandler((req) => getAdminSessionSummary(extractBearerToken(req)), {
successMessage: 'ok',
errorMessage: '读取后台登录态失败',
scope: '[admin/auth/session]',
}),
)
router.post(
'/auth/logout',
createJsonHandler((req) => logoutAdmin(extractBearerToken(req)), {
successMessage: '已退出登录',
errorMessage: '后台退出失败',
scope: '[admin/auth/logout]',
}),
)
export default router