useAuthRedirect.js
8.17 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
/*
* @Date: 2026-05-29 00:00:00
* @Description: 授权跳转辅助逻辑
*/
import { useRoute } from 'vue-router';
// 授权返回后的首轮路由检查只需要跳过一次,避免刚拿到 openid 又被周期选择打断
const SKIP_CYCLE_CHECK_FOR_AUTH_KEY = 'skip_cycle_check_for_auth';
// 只记录“这一轮刚发起过授权”的短期状态,供授权返回后修正浏览器后退行为
const AUTH_RETURN_GUARD_KEY = 'data-table-auth-return-guard';
// 只保留很短时间,避免授权链路中断后残留旧标记
const AUTH_RETURN_GUARD_TTL = 10 * 60 * 1000;
const toStringQueryValue = (value) => (typeof value === 'string' ? value : '');
const getQueryValueFromString = (search, key) => {
if (!search) return '';
const params = new URLSearchParams(search.startsWith('?') ? search : `?${search}`);
return params.get(key) || '';
};
const safeDecodeURIComponent = (value) => {
try {
return decodeURIComponent(value);
} catch (error) {
return value;
}
};
/**
* 消耗一次“跳过首次周期检查”的标记。
* 命中后会立即清理,确保它只影响授权返回的这一轮路由。
* @returns {boolean} 是否需要跳过一次周期检查
*/
export const consumeSkipCycleCheckForAuth = () => {
const shouldSkip = sessionStorage.getItem(SKIP_CYCLE_CHECK_FOR_AUTH_KEY) === 'true';
if (shouldSkip) {
sessionStorage.removeItem(SKIP_CYCLE_CHECK_FOR_AUTH_KEY);
}
return shouldSkip;
};
const clearAuthReturnGuard = () => {
sessionStorage.removeItem(AUTH_RETURN_GUARD_KEY);
};
const readAuthReturnGuard = () => {
try {
const state = JSON.parse(sessionStorage.getItem(AUTH_RETURN_GUARD_KEY) || '{}');
if (!state?.createdAt || Date.now() - state.createdAt > AUTH_RETURN_GUARD_TTL) {
clearAuthReturnGuard();
return null;
}
return state;
} catch (error) {
clearAuthReturnGuard();
return null;
}
};
/**
* 统一管理授权页与业务页之间共用的跳转能力。
* 这里不保存“授权成功”长期标记,避免后端授权失效后前端还拿旧状态误判。
* @param {import('vue-router').RouteLocationNormalizedLoaded} route 当前路由实例
* @returns {{
* getQueryValue: (key: string) => string,
* getCurrentTargetHash: () => string,
* getAuthTargetHash: () => string,
* buildOpenIdAuthUrl: (code: string, targetHash?: string) => string,
* redirectToOpenIdAuth: (code: string, targetHash?: string) => void,
* savePendingAuthReturnGuard: (code: string, targetHash?: string) => void,
* consumePendingAuthReturnGuard: (code: string, targetHash?: string) => { historyLength: number } | null,
* installAuthBackHistoryGuard: () => void,
* markSkipCycleCheckForAuth: () => void,
* consumeSkipCycleCheckForAuth: () => boolean
* }}
*/
export function useAuthRedirect(route = useRoute()) {
const getQueryValue = (key) => {
const routeValue = toStringQueryValue(route.query[key]);
if (routeValue) return routeValue;
// 兼容两类入口:
// 1. hash 路由标准参数:#/path?code=xxx
// 2. 老入口或外部重定向参数:index.html?code=xxx#/path
const searchValue = getQueryValueFromString(location.search, key);
if (searchValue) return searchValue;
const hash = location.hash || '';
const hashQueryIndex = hash.indexOf('?');
if (hashQueryIndex >= 0) {
const hashSearch = hash.slice(hashQueryIndex + 1);
return getQueryValueFromString(hashSearch, key);
}
return '';
};
// 当前表单页在 hash 模式下的完整路由,授权成功后要回到这里
const getCurrentTargetHash = () => `${location.search}${location.hash || `#${route.fullPath}`}`;
// 优先读 target,新链路;兼容 href,避免老链接直接失效
const getAuthTargetHash = () => {
const rawTarget = getQueryValue('target') || getQueryValue('href');
return rawTarget ? safeDecodeURIComponent(rawTarget) : '';
};
/**
* 统一拼接 openid 授权链接。
* 新链路会在业务页里直接 replace 到这个地址,避免先进入站内 auth 页留下额外历史记录。
* @param {string} code 表单 code
* @param {string} targetHash 授权成功后需要回到的 hash 路由
* @returns {string} 可直接跳转的授权地址
*/
const buildOpenIdAuthUrl = (code, targetHash = getCurrentTargetHash()) => {
const rawUrl = encodeURIComponent(`${location.origin}${location.pathname}${targetHash}`);
const shortUrl = `/srv/?f=custom_form&a=openid&res=${rawUrl}&form_code=${code}`;
// 开发环境继续复用旧的 openid 注入方式,避免影响本地调试体验
if (import.meta.env.DEV) {
return `${shortUrl}&openid=${import.meta.env.VITE_OPENID}`;
}
return shortUrl;
};
/**
* 直接跳到后端 openid 授权地址。
* 使用 location.replace 是为了把“当前未授权页”替换掉,避免浏览器后退时再落回授权中转页。
* @param {string} code 表单 code
* @param {string} targetHash 授权成功后需要回到的 hash 路由
*/
const redirectToOpenIdAuth = (code, targetHash = getCurrentTargetHash()) => {
window.location.replace(buildOpenIdAuthUrl(code, targetHash));
};
/**
* 记录一次待返回的授权流程。
* 这里只保存当前目标页和发起前的历史长度,返回后会立刻消费掉,不作为长期登录态判断。
* @param {string} code 表单 code
* @param {string} targetHash 授权成功后需要回到的 hash 路由
*/
const savePendingAuthReturnGuard = (code, targetHash = getCurrentTargetHash()) => {
sessionStorage.setItem(
AUTH_RETURN_GUARD_KEY,
JSON.stringify({
code,
targetHash,
historyLength: window.history.length,
createdAt: Date.now(),
}),
);
};
/**
* 消费一次授权返回标记。
* 只有当前页面和发起授权时记录的目标页一致,才认为这是同一轮授权返回。
* @param {string} code 表单 code
* @param {string} targetHash 当前页面 hash
* @returns {{ historyLength: number } | null} 命中的返回信息
*/
const consumePendingAuthReturnGuard = (code, targetHash = getCurrentTargetHash()) => {
const state = readAuthReturnGuard();
if (!state) return null;
const matched = state.code === code && state.targetHash === targetHash;
clearAuthReturnGuard();
if (!matched) {
return null;
}
return {
historyLength: Number(state.historyLength) || 0,
};
};
/**
* 在授权成功回到表单后,临时插入一个“同地址跳板”。
* 这样用户第一次点浏览器后退时,会先回到这个跳板,然后我们再一次性跳过授权地址回到真正的上一页。
*/
const installAuthBackHistoryGuard = () => {
const guardKey = `auth-back-guard:${Date.now()}`;
const currentState = window.history.state || {};
// 当前记录标成“跳板底座”
window.history.replaceState(
{
...currentState,
__authBackGuardBase: guardKey,
},
'',
location.href,
);
// 再压入一个相同地址的“顶层占位”,让第一次后退先落到上面的底座状态
window.history.pushState(
{
...currentState,
__authBackGuardTop: guardKey,
},
'',
location.href,
);
const handlePopState = (event) => {
if (event.state?.__authBackGuardBase !== guardKey) {
return;
}
window.removeEventListener('popstate', handlePopState);
// 当前位置已经从顶层占位退回到底座了,再退两层即可跨过授权地址回到真实上一页
window.history.go(-2);
};
window.addEventListener('popstate', handlePopState);
};
/**
* 标记这次授权流程已经进入外部跳转阶段。
* 这个标记只用于让路由守卫在授权返回时跳过一次周期检查。
*/
const markSkipCycleCheckForAuth = () => {
sessionStorage.setItem(SKIP_CYCLE_CHECK_FOR_AUTH_KEY, 'true');
};
return {
getQueryValue,
getCurrentTargetHash,
getAuthTargetHash,
buildOpenIdAuthUrl,
redirectToOpenIdAuth,
savePendingAuthReturnGuard,
consumePendingAuthReturnGuard,
installAuthBackHistoryGuard,
markSkipCycleCheckForAuth,
consumeSkipCycleCheckForAuth,
};
}