Toggle navigation
Toggle navigation
This project
Loading...
Sign in
Hooke
/
data-table
Go to a project
Toggle navigation
Toggle navigation pinning
Projects
Groups
Snippets
Help
Project
Activity
Repository
Graphs
Network
Create a new issue
Commits
Issue Boards
Authored by
hookehuyr
2026-05-29 18:04:54 +0800
Browse Files
Options
Browse Files
Download
Email Patches
Plain Diff
Commit
3ec075e9f3d58462f413d67a7e131e3f20485c98
3ec075e9
1 parent
8d8b1deb
fix(auth): smooth openid redirect flow
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
5 changed files
with
258 additions
and
23 deletions
src/App.vue
src/composables/index.js
src/composables/useAuthRedirect.js
src/router.js
src/views/auth.vue
src/App.vue
View file @
3ec075e
This diff is collapsed. Click to expand it.
src/composables/index.js
View file @
3ec075e
...
...
@@ -7,6 +7,7 @@
* @Description:
*/
import
{
onMounted
,
onUnmounted
}
from
'vue'
export
{
useAuthRedirect
}
from
'./useAuthRedirect.js'
/**
* 添加和清除 DOM 事件监听器
...
...
src/composables/useAuthRedirect.js
0 → 100644
View file @
3ec075e
/*
* @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
,
};
}
src/router.js
View file @
3ec075e
...
...
@@ -13,6 +13,7 @@ import { Loading } from "vant";
import
Cookies
from
'js-cookie'
;
import
{
showUnfinishedFormDialog
,
resetDialogState
}
from
'@/utils/dialogControl.js'
;
import
{
getCycleListAPI
}
from
'@/api/cycle'
;
import
{
consumeSkipCycleCheckForAuth
}
from
'@/composables/useAuthRedirect.js'
;
// TAG: 路由配置表
/**
...
...
@@ -145,13 +146,8 @@ router.beforeEach((to, from, next) => {
return
;
}
// 检查是否从授权页面返回,如果是首次访问且可能需要授权,则跳过周期检查
const
isFromAuth
=
from
.
path
===
'/auth'
;
const
skipCycleCheck
=
sessionStorage
.
getItem
(
'skip_cycle_check_for_auth'
);
// 如果不是从授权页面返回,且设置了跳过标识,则先清除标识并跳过周期检查
if
(
!
isFromAuth
&&
skipCycleCheck
===
'true'
)
{
sessionStorage
.
removeItem
(
'skip_cycle_check_for_auth'
);
// 授权成功回到业务页后的第一轮路由里,跳过一次周期检查,避免刚拿到 openid 又被打断
if
(
consumeSkipCycleCheckForAuth
())
{
// 直接执行表单检查逻辑,跳过周期检查
if
(
to
.
query
.
page_type
===
'add'
||
to
.
query
.
page_type
===
undefined
)
{
const
existingCookie
=
Cookies
.
get
(
to
.
query
.
code
);
...
...
src/views/auth.vue
View file @
3ec075e
...
...
@@ -11,24 +11,23 @@
<script setup>
import { onMounted } from 'vue'
import { use
Route } from 'vue-router
'
import { use
AuthRedirect } from '@/composables
'
const $route = useRoute();
const {
getAuthTargetHash,
getQueryValue,
redirectToOpenIdAuth,
savePendingAuthReturnGuard,
markSkipCycleCheckForAuth,
} = useAuthRedirect();
onMounted(() => {
// php需要先跳转链接获取openid
/**
* encodeURIComponent() 函数可把字符串作为 URI 组件进行编码。
* 该方法不会对 ASCII 字母和数字进行编码,也不会对这些 ASCII 标点符号进行编码: - _ . ! ~ * ' ( ) 。
* 其他字符(比如 :;/?:@&=+$,# 这些用于分隔 URI 组件的标点符号),都是由一个或多个十六进制的转义序列替换的。
*/
let raw_url = encodeURIComponent(location.origin + location.pathname + $route.query.href); // 未授权的地址
// TAG: 开发环境测试数据
const short_url = `/srv/?f=custom_form&a=openid&res=${raw_url}&form_code=${$route.query.code}`;
// 使用 replace 方法替代 href,避免在浏览器历史中留下记录
// 这样用户点击后退按钮时不会回到授权页面
window.location.replace(import.meta.env.DEV
? `${short_url}&openid=${import.meta.env.VITE_OPENID}`
: `${short_url}`);
const code = getQueryValue('code');
const targetHash = getAuthTargetHash();
// 兼容老的 /auth 入口时,也补上同一套一次性回退保护和周期检查跳过标记
savePendingAuthReturnGuard(code, targetHash || '#/');
markSkipCycleCheckForAuth();
// 兼容老的 /auth 入口:统一交给 composable 计算并 replace 到后端授权地址
redirectToOpenIdAuth(code, targetHash || '#/');
})
</script>
...
...
Please
register
or
login
to post a comment