登录改用访问令牌 + 刷新令牌,不用 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:
1 parent
a3eb563b70
commit
5bb2940969
11 files changed
+332
-110
No files matched your search
+24
-12
@@ -1,5 +1,7 @@
|
||||
// 调用服务器接口。出错时抛出 ApiError,message 是服务器给的中文说明;
|
||||
// 登录过期(401)时回到登录页。
|
||||
// 请求带上访问令牌(快过期时先刷新);登录过期(刷新也失败)时回到登录页。
|
||||
import { refreshTokens, validAccessToken } from './tokens'
|
||||
|
||||
export class ApiError extends Error {
|
||||
constructor(
|
||||
message: string,
|
||||
@@ -13,19 +15,29 @@ export class ApiError extends Error {
|
||||
export const apiHooks = { onUnauthorized: () => {} }
|
||||
|
||||
export async function api<T>(method: string, path: string, json?: unknown, init?: RequestInit): Promise<T> {
|
||||
let response: Response
|
||||
try {
|
||||
response = await fetch(path, {
|
||||
method,
|
||||
headers: json === undefined ? undefined : { 'Content-Type': 'application/json' },
|
||||
body: json === undefined ? undefined : JSON.stringify(json),
|
||||
...init,
|
||||
})
|
||||
} catch {
|
||||
throw new ApiError('连不上服务器,请检查网络', 0)
|
||||
}
|
||||
const response = await send(method, path, json, init)
|
||||
const data = (await response.json().catch(() => ({}))) as T & { message?: string }
|
||||
if (response.status === 401 && path !== '/api/login') apiHooks.onUnauthorized()
|
||||
if (!response.ok) throw new ApiError(data.message ?? `请求失败(${response.status})`, response.status)
|
||||
return data
|
||||
}
|
||||
|
||||
/** 发请求并带上访问令牌;令牌被拒绝(401)时刷新一次再重发 */
|
||||
async function send(method: string, path: string, json: unknown, init: RequestInit | undefined, retried = false): Promise<Response> {
|
||||
const headers: Record<string, string> = {}
|
||||
if (json !== undefined) headers['Content-Type'] = 'application/json'
|
||||
if (path !== '/api/login') {
|
||||
const token = await validAccessToken()
|
||||
if (token) headers.Authorization = `Bearer ${token}`
|
||||
}
|
||||
let response: Response
|
||||
try {
|
||||
response = await fetch(path, { method, headers, body: json === undefined ? undefined : JSON.stringify(json), ...init })
|
||||
} catch {
|
||||
throw new ApiError('连不上服务器,请检查网络', 0)
|
||||
}
|
||||
if (response.status === 401 && path !== '/api/login' && !retried && (await refreshTokens())) {
|
||||
return send(method, path, json, init, true)
|
||||
}
|
||||
return response
|
||||
}
|
||||
Reference in new issue
Block a user