试用前排查:WebSocket 心跳与断线重连、登录失败按账号限制、旧浏览器提示
CI/CD / test (push) Canceled after 0s
CI/CD / deploy (push) Canceled after 0s

- 服务器 30 秒发一次 ping:Nginx Proxy Manager 默认 60 秒没数据就断开,窗体开着不操作程序会被结束
- 连接意外断开时提示、清掉失效的窗体、自动重连,连上后提示重新运行
- 登录失败:同一账号 10 次只锁这个账号,同一 IP 300 次,登录成功不再清零 IP 计数
- index.html:浏览器太旧时说明原因和解决办法,加载慢时提示稍等

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
xuyueandClaude Opus 5.5 committed 2026-10-26 09:05:05 +08:00
1 parent 982b1297aa
commit 27fdc3615d
8 files changed
+137 -23

No files matched your search

+27 -5
View File
@@ -90,11 +90,33 @@ test('账号或密码不对时登录失败,老师登录后能看到自己的
expect(me.body.user).toMatchObject({ username: config.teacherUsername, role: 'teacher' })
})
test('同一个 IP 输错 30 次后暂停登录,别的 IP 不受影响', async () => {
const attacker = new Browser('10.0.0.66')
for (let i = 0; i < 30; i++) await attacker.login('admin', `guess${i}`)
expect((await attacker.login('admin', 'whatever')).status).toBe(429)
expect((await new Browser('10.0.0.67').login('admin', 'whatever')).status).toBe(401)
test('同一个账号输错 10 次后暂停这个账号的登录,同一 IP 的其他同学不受影响', async () => {
const t = await teacher()
await classWithStudents(t, [
{ username: 'guessed', name: '甲', password: '123456' },
{ username: 'other', name: '乙', password: '654321' },
])
const classmate = new Browser('10.0.0.66')
for (let i = 0; i < 9; i++) await classmate.login('guessed', `00000${i}`)
// 用自己的账号登录成功,不会清掉别人账号的失败次数
expect((await classmate.login('other', '654321')).status).toBe(200)
await classmate.login('guessed', '000009')
const blocked = await new Browser('10.0.0.67').login('guessed', '123456')
expect(blocked.status).toBe(429)
expect(blocked.body.message).toContain('这个账号')
expect((await new Browser('10.0.0.66').login('other', '654321')).status).toBe(200)
})
test('同一个 IP 输错 300 次后暂停登录,登录成功也不清零,别的 IP 不受影响', async () => {
const t = await teacher()
await classWithStudents(t, [{ username: 'mine', name: '甲', password: '111111' }])
const attacker = new Browser('10.0.0.68')
for (let i = 0; i < 300; i++) {
await attacker.login(`nobody${i % 50}`, 'guess')
if (i === 150) expect((await attacker.login('mine', '111111')).status).toBe(200)
}
expect((await attacker.login('mine', '111111')).status).toBe(429)
expect((await new Browser('10.0.0.69').login('mine', '111111')).status).toBe(200)
})
describe('班级和名单', () => {