Skip to content

Commit 05d4669

Browse files
author
yqz
committed
Revert "添加双token无感刷新设计"
This reverts commit 9989b19.
1 parent 9989b19 commit 05d4669

5 files changed

Lines changed: 42 additions & 198 deletions

File tree

README.md

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -172,25 +172,6 @@ mvn -DskipTests package
172172

173173
详细放行规则见 `SecurityConfig`
174174

175-
### 双 Token 与无感刷新(设计说明)
176-
177-
**后端(无需单独端口)**
178-
179-
- 登录成功返回 **Access Token**(短效)与 **Refresh Token**(长效),均由 `openblog.jwt.*` 配置过期时间(见上文配置表)。
180-
- 换发接口:`POST /api/v1/auth/refresh`,请求体为 `{ "refreshToken": "<refresh_jwt>" }`,成功时返回新的 **accessToken****refreshToken**(与登录接口同一 `AuthResponse` 结构)。
181-
- 与登录、文章等接口共用 **`server.port` 同一端口**;不存在「刷新专用端口」。
182-
183-
**前端(Vue,`vue/src`**
184-
185-
- **`vue/src/auth/session.js`**`isConsoleSessionValid()`**access 已过期但 refresh 仍有效** 时也视为已登录,避免仅因短 token 过期被路由误判下线。
186-
- **`vue/src/api/http.js`**
187-
- **`refreshSessionTokens()`**:直连 `/api/v1/auth/refresh` 换发双 token,**不走** `request()`,避免递归;并发多次刷新合并为 **同一 Promise**(互斥)。
188-
- **`request(..., { withAuth: true })` 发起前**:若 refresh 仍可用,且「无 access」或 **access 剩余有效期不足约 90 秒」,则**主动刷新**,减少首包 401。
189-
- **收到 HTTP 401**:在带鉴权请求上 **自动刷新一次并重试原请求**(带 `_refreshRetried` 标记,防止死循环);刷新仍失败则 `clearAuth()`,在 `/console` 下跳转控制台登录。
190-
- **`vue/src/api/media.js`**:上传走原生 `fetch`,在 **401** 时调用 `refreshSessionTokens()`**重传一次**(与 JSON 接口策略一致)。
191-
192-
**小结**:后端只提供 REST 路径;无感刷新由前端在**同一 API 基址**上组合「主动刷新 + 401 重试 + 并发合并」完成。
193-
194175
---
195176

196177
## 测试

vue/src/api/http.js

Lines changed: 18 additions & 126 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,7 @@
44
* - 未设置:开发环境默认连仓库里的远程后端;生产环境默认同域(与上面空字符串一致)。
55
* 注意:不要用 `||`,否则空字符串会被当成假值而误用默认 IP。
66
*/
7-
import {
8-
clearAuth,
9-
getStoredAccessToken,
10-
getStoredRefreshToken,
11-
hasUsableRefreshToken,
12-
isJwtExpired,
13-
isLikelyJwt,
14-
parseJwtPayload
15-
} from '../auth/session'
7+
import { clearAuth, getStoredAccessToken } from '../auth/session'
168

179
function resolveApiBase() {
1810
const v = import.meta.env.VITE_API_BASE
@@ -24,107 +16,21 @@ function resolveApiBase() {
2416

2517
export const API_BASE = resolveApiBase()
2618

19+
const baseUrl = API_BASE
20+
2721
function buildUrl(path) {
2822
// 后端统一是 /api/v1 前缀
2923
if (path.startsWith('http://') || path.startsWith('https://')) return path
30-
return `${API_BASE}${path}`
31-
}
32-
33-
/** 并发刷新合并为同一 Promise */
34-
let refreshInflight = null
35-
36-
/**
37-
* 使用 refreshToken 换发双 token(不经过 request,避免环)。
38-
* 失败时会 clearAuth。
39-
*/
40-
export function refreshSessionTokens() {
41-
if (refreshInflight) return refreshInflight
42-
43-
refreshInflight = (async () => {
44-
const rt = getStoredRefreshToken()
45-
if (!rt || !isLikelyJwt(rt) || isJwtExpired(rt)) {
46-
clearAuth()
47-
const err = new Error('登录已失效')
48-
err.httpStatus = 401
49-
throw err
50-
}
51-
52-
const res = await fetch(buildUrl('/api/v1/auth/refresh'), {
53-
method: 'POST',
54-
headers: { 'Content-Type': 'application/json' },
55-
body: JSON.stringify({ refreshToken: rt })
56-
})
57-
58-
const text = await res.text()
59-
let json = null
60-
try {
61-
json = text ? JSON.parse(text) : null
62-
} catch {
63-
// ignore
64-
}
65-
66-
if (!res.ok || (json && typeof json.code === 'number' && json.code !== 0)) {
67-
clearAuth()
68-
const err = new Error(json?.message || '登录已失效')
69-
err.httpStatus = res.status || 401
70-
err.code = json?.code
71-
throw err
72-
}
73-
74-
const data = json?.data
75-
if (!data?.accessToken || !data?.refreshToken) {
76-
clearAuth()
77-
const err = new Error('刷新返回异常')
78-
err.httpStatus = 401
79-
throw err
80-
}
81-
82-
localStorage.setItem('accessToken', data.accessToken)
83-
localStorage.setItem('refreshToken', data.refreshToken)
84-
return data
85-
})()
86-
87-
return refreshInflight.finally(() => {
88-
refreshInflight = null
89-
})
90-
}
91-
92-
function accessExpiresWithinSeconds(seconds) {
93-
const t = getStoredAccessToken()
94-
if (!t || !isLikelyJwt(t)) return true
95-
const p = parseJwtPayload(t)
96-
if (typeof p.exp !== 'number') return false
97-
return p.exp * 1000 - Date.now() < seconds * 1000
98-
}
99-
100-
function redirectConsoleLoginIfNeeded() {
101-
if (typeof window !== 'undefined' && window.location.pathname.startsWith('/console')) {
102-
const redirect = window.location.pathname + window.location.search
103-
window.location.assign('/console/login?redirect=' + encodeURIComponent(redirect))
104-
}
24+
return `${baseUrl}${path}`
10525
}
10626

10727
export async function request(path, options = {}) {
108-
const isRetry = Boolean(options._refreshRetried)
109-
const extraHeaders = options.headers && typeof options.headers === 'object' ? options.headers : {}
11028
const headers = {
11129
'Content-Type': 'application/json',
112-
...extraHeaders
30+
...(options.headers || {})
11331
}
11432

11533
if (options.withAuth) {
116-
if (
117-
!isRetry &&
118-
hasUsableRefreshToken() &&
119-
(!getStoredAccessToken() || accessExpiresWithinSeconds(90))
120-
) {
121-
try {
122-
await refreshSessionTokens()
123-
} catch {
124-
// 无 access 时交给后续分支;将过期 access 时可能仍失败,再由 401 重试
125-
}
126-
}
127-
12834
const token = getStoredAccessToken()
12935
if (!token) {
13036
const err = new Error('未登录')
@@ -134,46 +40,31 @@ export async function request(path, options = {}) {
13440
headers.Authorization = `Bearer ${token}`
13541
}
13642

137-
const fetchOpts = { ...options }
138-
delete fetchOpts.withAuth
139-
delete fetchOpts._refreshRetried
140-
delete fetchOpts.headers
141-
14243
const res = await fetch(buildUrl(path), {
143-
...fetchOpts,
144-
headers
44+
headers,
45+
...options
14546
})
14647

14748
const text = await res.text()
14849
let json = null
149-
if (!_skipJson) {
150-
try {
151-
json = text ? JSON.parse(text) : null
152-
} catch {
153-
// ignore
154-
}
50+
try {
51+
json = text ? JSON.parse(text) : null
52+
} catch {
53+
// ignore
15554
}
15655

157-
if (res.status === 401 && options.withAuth && !isRetry) {
158-
try {
159-
await refreshSessionTokens()
160-
return request(path, { ...options, _refreshRetried: true })
161-
} catch {
162-
redirectConsoleLoginIfNeeded()
163-
const err = new Error('登录已失效')
164-
err.httpStatus = 401
165-
throw err
166-
}
167-
}
168-
169-
if (res.status === 401 && options.withAuth && isRetry) {
56+
if (res.status === 401 && options.withAuth) {
17057
clearAuth()
171-
redirectConsoleLoginIfNeeded()
58+
if (typeof window !== 'undefined' && window.location.pathname.startsWith('/console')) {
59+
const redirect = window.location.pathname + window.location.search
60+
window.location.assign('/console/login?redirect=' + encodeURIComponent(redirect))
61+
}
17262
const err = new Error('登录已失效')
17363
err.httpStatus = 401
17464
throw err
17565
}
17666

67+
// API 统一返回 ApiResponse,HTTP 层可能仍为 200
17768
if (json && typeof json.code === 'number' && json.code !== 0) {
17869
const msg = json.message || '请求失败'
17970
const err = new Error(msg)
@@ -192,3 +83,4 @@ export async function request(path, options = {}) {
19283

19384
return json?.data ?? json
19485
}
86+

vue/src/api/media.js

Lines changed: 18 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { getStoredAccessToken } from '../auth/session'
2-
import { API_BASE, refreshSessionTokens } from './http'
2+
import { API_BASE } from './http'
33

44
function getAuthHeader() {
55
const token = getStoredAccessToken()
@@ -8,17 +8,23 @@ function getAuthHeader() {
88
}
99

1010
export async function uploadMedia(file) {
11-
let { res, json } = await doUploadWithFile(file)
12-
13-
if (res.status === 401) {
14-
try {
15-
await refreshSessionTokens()
16-
;({ res, json } = await doUploadWithFile(file))
17-
} catch {
18-
const err = new Error('登录已失效')
19-
err.httpStatus = 401
20-
throw err
21-
}
11+
const fd = new FormData()
12+
fd.append('file', file)
13+
14+
const res = await fetch(`${API_BASE}/api/v1/media/upload`, {
15+
method: 'POST',
16+
headers: {
17+
...getAuthHeader()
18+
},
19+
body: fd
20+
})
21+
22+
const text = await res.text()
23+
let json = null
24+
try {
25+
json = text ? JSON.parse(text) : null
26+
} catch {
27+
json = null
2228
}
2329

2430
if (!res.ok) {
@@ -35,25 +41,3 @@ export async function uploadMedia(file) {
3541
return json?.data ?? json
3642
}
3743

38-
function doUploadWithFile(file) {
39-
const fd = new FormData()
40-
fd.append('file', file)
41-
42-
return fetch(`${API_BASE}/api/v1/media/upload`, {
43-
method: 'POST',
44-
headers: {
45-
...getAuthHeader()
46-
},
47-
body: fd
48-
}).then(async (res) => {
49-
const text = await res.text()
50-
let json = null
51-
try {
52-
json = text ? JSON.parse(text) : null
53-
} catch {
54-
json = null
55-
}
56-
return { res, json }
57-
})
58-
}
59-

vue/src/auth/session.js

Lines changed: 3 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -15,19 +15,6 @@ export function getStoredAccessToken() {
1515
return s.length > 0 ? s : null
1616
}
1717

18-
export function getStoredRefreshToken() {
19-
const t = localStorage.getItem('refreshToken')
20-
if (typeof t !== 'string') return null
21-
const s = t.trim()
22-
return s.length > 0 ? s : null
23-
}
24-
25-
/** refresh JWT 仍有效且未过期 */
26-
export function hasUsableRefreshToken() {
27-
const rt = getStoredRefreshToken()
28-
return Boolean(rt && isLikelyJwt(rt) && !isJwtExpired(rt))
29-
}
30-
3118
/** 是否为标准 JWT 外形(header.payload.sig) */
3219
export function isLikelyJwt(token) {
3320
return typeof token === 'string' && token.split('.').length === 3
@@ -55,11 +42,10 @@ export function clearAuth() {
5542
localStorage.removeItem('refreshToken')
5643
}
5744

58-
/** 控制台/带鉴权前台:access 有效,或仍可用 refresh 换发(无感刷新入口) */
5945
export function isConsoleSessionValid() {
60-
const access = getStoredAccessToken()
61-
if (access && isLikelyJwt(access) && !isJwtExpired(access)) return true
62-
return hasUsableRefreshToken()
46+
const token = getStoredAccessToken()
47+
if (!token || !isLikelyJwt(token) || isJwtExpired(token)) return false
48+
return true
6349
}
6450

6551
/** JWT payload 中的 role(ADMIN / AUTHOR / READER) */

vue/src/components/BlogHeader.vue

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ import { computed, onMounted, ref, watch } from 'vue'
8282
import { useRoute, useRouter } from 'vue-router'
8383
import { toggleTheme as applyToggle } from '../theme'
8484
import { fetchMe } from '../api/admin'
85-
import { clearAuth, isConsoleSessionValid } from '../auth/session'
85+
import { clearAuth, getStoredAccessToken, isJwtExpired, isLikelyJwt } from '../auth/session'
8686
8787
defineEmits(['toggle-widgets'])
8888
@@ -98,7 +98,8 @@ const displayName = computed(() => {
9898
})
9999
100100
function tokenLooksValid() {
101-
return isConsoleSessionValid()
101+
const t = getStoredAccessToken()
102+
return Boolean(t && isLikelyJwt(t) && !isJwtExpired(t))
102103
}
103104
104105
async function loadMe() {

0 commit comments

Comments
 (0)