登录改用访问令牌 + 刷新令牌,不用 cookie

- 访问令牌:JWT(HS256),15 分钟,Authorization: Bearer;WebSocket 放在网址参数里
- 刷新令牌:30 天,每次刷新换新的,数据库只存哈希;退出登录即作废
- 重置密码、删除学生:令牌版本加一并删除刷新令牌,所有设备立即退出
- 签名密钥首次使用时生成,存在数据库 settings 表
- 浏览器:令牌存 localStorage;快过期先刷新,401 时刷新一次再重试;多个标签页同时刷新不冲突

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
xuyueandClaude Opus 5.5 committed 2026-10-22 18:20:41 +08:00
1 parent a3eb563b70
commit 5bb2940969
11 files changed
+332 -110

No files matched your search

+6 -4
View File
@@ -1,6 +1,6 @@
import type { ClientMessage, ServerMessage } from '@iot/shared'
import { createApp } from './app'
import { ensureTeacher, sessionToken, sessionUser } from './auth'
import { ensureTeacher, tokenUser } from './auth'
import { compileService } from './compile-service'
import { config } from './config'
import { openDatabase } from './db'
@@ -21,9 +21,11 @@ interface SocketData {
const server = Bun.serve({
port: config.port,
fetch(req, server) {
if (new URL(req.url).pathname === '/ws') {
if (!sessionUser(db, sessionToken(req.headers.get('cookie')))) return new Response('请先登录', { status: 401 })
async fetch(req, server) {
const url = new URL(req.url)
if (url.pathname === '/ws') {
// 浏览器的 WebSocket 不能带请求头,令牌放在网址参数里
if (!(await tokenUser(db, url.searchParams.get('token')))) return new Response('请先登录', { status: 401 })
if (server.upgrade(req, { data: {} })) return
return new Response('WebSocket 升级失败', { status: 400 })
}