refactor(登录): 登录态从 Cookie 换成 access token + refresh token
Deploy / deploy (push) Canceled after 0s

- access token 是 15 分钟的 JWT,走 Authorization: Bearer;refresh token 7 天,每次换都轮换
- 退出登录、改密码、禁用账号仍然立刻生效:每个请求核对 Redis 会话和库里新加的 user.token_version
- 在服务器后台改密码 / 禁用,机房那边登着的也会一起下线(原来两边 Redis 分开,管不到)
- WebSocket 握手借子协议带 token;JWT 密钥存在本站 Redis,部署不用加配置
- 去掉 COOKIE_SECURE;上线后所有人要重新登录一次

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
xuyueandClaude Opus 5.5 committed 2026-10-20 19:22:52 +08:00
1 parent 4be569d978
commit 793848b06f
31 files changed
+4639 -333

No files matched your search

+28
View File
@@ -5,6 +5,33 @@ export const loginRequestSchema = z.object({
password: z.string().min(1).max(1024),
})
/** 换 access token、退出登录都拿 refresh token 当凭证 */
export const refreshTokenRequestSchema = z.object({
refreshToken: z.string().min(1).max(512),
})
/**
* WebSocket 握手带 access token 用的子协议名。浏览器的 `new WebSocket()` 设不了请求头,
* 前端传 `[WS_AUTH_PROTOCOL, token]` 两个子协议,服务端回第一个 —— 必须回一个前端
* 传过的,否则 Chrome 判握手失败。不放 URL 查询参数:URL 会原样进反代的访问日志。
*/
export const WS_AUTH_PROTOCOL = "oj2.bearer"
/**
* 登录、换 token 的出参。
*
* `expiresIn` 是秒数而不是到期时刻:机房电脑的时钟不一定准,前端拿自己的时钟加上它
* 算到期,才不会因为差了几分钟一直以为 token 已过期。
*
* `refreshToken` 为 null 只出现在换 token 的宽限期里(同一张 refresh token 刚被别的
* 标签页换掉),这时沿用本地已经存着的那张,见 auth/session.ts 的 refreshTokens。
*/
export interface AuthTokens {
accessToken: string
refreshToken: string | null
expiresIn: number
}
export const sessionUserSchema = z.object({
id: z.number().int(),
username: z.string(),
@@ -29,5 +56,6 @@ export const userProfileSchema = z.object({
})
export type LoginRequest = z.input<typeof loginRequestSchema>
export type RefreshTokenRequest = z.input<typeof refreshTokenRequestSchema>
export type SessionUser = z.infer<typeof sessionUserSchema>
export type UserProfile = z.infer<typeof userProfileSchema>