Toggle navigation
Toggle navigation
This project
Loading...
Sign in
Hooke
/
mlaj
Go to a project
Toggle navigation
Toggle navigation pinning
Projects
Groups
Snippets
Help
Project
Activity
Repository
Pipelines
Graphs
Issues
0
Merge Requests
0
Wiki
Snippets
Network
Create a new issue
Builds
Commits
Issue Boards
Authored by
hookehuyr
2026-03-30 13:49:06 +0800
Browse Files
Options
Browse Files
Download
Email Patches
Plain Diff
Commit
5ec68929c89c9e9bd604c372c322fd2ec1e96783
5ec68929
1 parent
7269b785
fix(auth): restore hash route after oauth redirect
Show whitespace changes
Inline
Side-by-side
Showing
5 changed files
with
207 additions
and
59 deletions
src/main.js
src/router/guards.js
src/utils/__tests__/oauthHashRestore.test.js
src/utils/oauthHashRestore.js
src/views/auth.vue
src/main.js
View file @
5ec6892
...
...
@@ -9,9 +9,10 @@ import { createApp } from 'vue'
import
'./style.css'
import
App
from
'./App.vue'
import
router
from
'./router'
import
axios
from
'@/utils/axios'
;
import
axios
from
'@/utils/axios'
import
{
restoreHashAfterOAuth
}
from
'@/utils/oauthHashRestore'
import
'vant/lib/index.css'
import
'@vant/touch-emulator'
;
import
'@vant/touch-emulator'
/* import the fontawesome core */
import
{
library
}
from
'@fortawesome/fontawesome-svg-core'
...
...
@@ -19,47 +20,80 @@ import { library } from '@fortawesome/fontawesome-svg-core'
import
{
FontAwesomeIcon
}
from
'@fortawesome/vue-fontawesome'
import
{
Icon
as
IconifyIcon
}
from
'@iconify/vue'
/* import specific icons */
import
{
faCirclePause
,
faCirclePlay
,
faPlay
,
faPause
,
faBackwardStep
,
faForwardStep
,
faVolumeUp
,
faRedo
,
faRepeat
,
faList
,
faChevronDown
,
faVolumeOff
,
faXmark
,
faFileAlt
,
faTimes
,
faEye
,
faFilePdf
,
faExternalLinkAlt
,
faSpinner
,
faExclamationCircle
,
faDownload
,
faVenus
,
faMars
,
faMagnifyingGlassPlus
,
faMagnifyingGlassMinus
}
from
'@fortawesome/free-solid-svg-icons'
import
{
faCirclePause
,
faCirclePlay
,
faPlay
,
faPause
,
faBackwardStep
,
faForwardStep
,
faVolumeUp
,
faRedo
,
faRepeat
,
faList
,
faChevronDown
,
faVolumeOff
,
faXmark
,
faFileAlt
,
faTimes
,
faEye
,
faFilePdf
,
faExternalLinkAlt
,
faSpinner
,
faExclamationCircle
,
faDownload
,
faVenus
,
faMars
,
faMagnifyingGlassPlus
,
faMagnifyingGlassMinus
,
}
from
'@fortawesome/free-solid-svg-icons'
/* add icons to the library */
library
.
add
(
faCirclePause
,
faCirclePlay
,
faPlay
,
faPause
,
faBackwardStep
,
faForwardStep
,
faVolumeUp
,
faRedo
,
faRepeat
,
faList
,
faChevronDown
,
faVolumeOff
,
faXmark
,
faFileAlt
,
faTimes
,
faEye
,
faFilePdf
,
faExternalLinkAlt
,
faSpinner
,
faExclamationCircle
,
faDownload
,
faVenus
,
faMars
,
faMagnifyingGlassPlus
,
faMagnifyingGlassMinus
)
library
.
add
(
faCirclePause
,
faCirclePlay
,
faPlay
,
faPause
,
faBackwardStep
,
faForwardStep
,
faVolumeUp
,
faRedo
,
faRepeat
,
faList
,
faChevronDown
,
faVolumeOff
,
faXmark
,
faFileAlt
,
faTimes
,
faEye
,
faFilePdf
,
faExternalLinkAlt
,
faSpinner
,
faExclamationCircle
,
faDownload
,
faVenus
,
faMars
,
faMagnifyingGlassPlus
,
faMagnifyingGlassMinus
)
if
(
!
Array
.
prototype
.
at
)
{
Array
.
prototype
.
at
=
function
(
n
)
{
n
=
Math
.
trunc
(
n
)
||
0
;
if
(
n
<
0
)
n
+=
this
.
length
;
if
(
n
<
0
||
n
>=
this
.
length
)
return
undefined
;
return
this
[
n
]
;
}
;
Array
.
prototype
.
at
=
function
(
n
)
{
n
=
Math
.
trunc
(
n
)
||
0
if
(
n
<
0
)
n
+=
this
.
length
if
(
n
<
0
||
n
>=
this
.
length
)
return
undefined
return
this
[
n
]
}
}
const
app
=
createApp
(
App
)
app
.
component
(
'Icon'
,
IconifyIcon
)
app
.
component
(
'
font-awesome-i
con'
,
FontAwesomeIcon
)
app
.
component
(
'
FontAwesomeI
con'
,
FontAwesomeIcon
)
// 屏蔽警告信息
app
.
config
.
warnHandler
=
()
=>
null
;
app
.
config
.
globalProperties
.
$http
=
axios
;
// 关键语句
/**
* @function restoreHashAfterOAuth
* @description 前端复原 OAuth 回跳的 hash 路由位置:当 URL 中存在 ret_hash 参数且当前无 hash 时,将其拼回地址栏。
* @returns {void}
*/
// function restoreHashAfterOAuth() {
// const url = new URL(window.location.href);
// const ret_hash = url.searchParams.get('ret_hash');
// if (ret_hash && !window.location.hash) {
// // 删除 ret_hash,保留其他查询参数
// url.searchParams.delete('ret_hash');
// const base = url.toString().split('#')[0];
// const new_url = base + ret_hash;
// // 使用 replaceState 避免再次刷新与历史记录污染
// window.history.replaceState(null, '', new_url);
// }
// }
// void restoreHashAfterOAuth
app
.
config
.
warnHandler
=
()
=>
null
app
.
config
.
globalProperties
.
$http
=
axios
// 关键语句
// 在安装路由前进行一次 hash 复原,确保初始路由正确
//
restoreHashAfterOAuth()
restoreHashAfterOAuth
()
app
.
use
(
router
)
// 开发环境添加欢迎页调试工具
...
...
src/router/guards.js
View file @
5ec6892
...
...
@@ -6,7 +6,8 @@
* @Description: 路由守卫逻辑
*/
import
{
getAuthInfoAPI
}
from
'@/api/auth'
import
{
wxInfo
}
from
"@/utils/tools"
import
{
buildOAuthAuthorizeUrl
}
from
'@/utils/oauthHashRestore'
import
{
wxInfo
}
from
'@/utils/tools'
// TAG: 需要登录才能访问的路由
export
const
authRequiredRoutes
=
[
...
...
@@ -46,12 +47,12 @@ export const checkWxAuth = async () => {
try
{
if
(
!
import
.
meta
.
env
.
DEV
&&
wxInfo
().
isWeiXin
)
{
// 仅做一次授权状态探测,避免无意义请求
await
getAuthInfoAPI
();
await
getAuthInfoAPI
()
}
}
catch
(
error
)
{
// 忽略授权探测错误,不影响后续流程
}
return
true
;
return
true
}
/**
...
...
@@ -67,29 +68,33 @@ export const startWxAuth = async () => {
// 开发环境不触发微信授权
if
(
import
.
meta
.
env
.
DEV
)
{
// 开发环境下不触发微信授权登录
return
;
return
}
const
info
=
wxInfo
();
const
info
=
wxInfo
()
// 非微信环境不进行授权跳转
if
(
!
info
.
isWeiXin
)
{
return
;
return
}
// 如果已授权则不跳转;否则进入授权页
try
{
const
{
code
,
data
}
=
await
getAuthInfoAPI
();
const
{
code
,
data
}
=
await
getAuthInfoAPI
()
if
(
code
&&
data
.
openid_has
)
{
return
;
return
}
}
catch
(
e
)
{
// 探测失败不影响授权流程,继续跳转
}
// 跳转到微信授权地址
const
raw_url
=
encodeURIComponent
(
location
.
href
);
const
short_url
=
`/srv/?f=behalo&a=openid&res=
${
raw_url
}
`
;
location
.
href
=
short_url
;
// 跳转到微信授权地址。
// OAuth 回跳链路对 hash 路由并不稳定,这里显式拆分 base_url 与 ret_hash,
// 由前端在 main.js 安装路由前进行一次 hash 复原。
const
short_url
=
buildOAuthAuthorizeUrl
(
window
.
location
.
href
)
if
(
!
short_url
)
{
return
}
location
.
href
=
short_url
}
// 首次访问标志
...
...
@@ -100,9 +105,7 @@ const WELCOME_VISITED_AT = 'welcome_visited_at'
* @description 检查用户是否已访问过欢迎页
* @returns {boolean}
*/
export
const
hasVisitedWelcome
=
()
=>
{
return
localStorage
.
getItem
(
HAS_VISITED_WELCOME
)
===
'true'
}
export
const
hasVisitedWelcome
=
()
=>
localStorage
.
getItem
(
HAS_VISITED_WELCOME
)
===
'true'
/**
* @description 标记用户已访问欢迎页
...
...
@@ -124,18 +127,17 @@ export const resetWelcomeFlag = () => {
// 检查用户是否已登录
/**
* @description 登录权限检查,未登录时重定向到登录页
* @param {*} to 目标路由对象
* @returns {true|Object} 允许通过或返回重定向对象
*/
export
const
checkAuth
=
(
to
)
=>
{
export
const
checkAuth
=
to
=>
{
const
currentUser
=
JSON
.
parse
(
localStorage
.
getItem
(
'currentUser'
))
// 检查当前路由是否需要认证
// 方式一:白名单匹配(兼容旧逻辑)
const
needAuthByList
=
authRequiredRoutes
.
some
((
route
)
=>
{
const
needAuthByList
=
authRequiredRoutes
.
some
(
route
=>
{
// 如果是正则匹配模式
if
(
route
.
regex
)
{
return
new
RegExp
(
`^
${
route
.
path
}
$`).test(to.path)
...
...
@@ -152,7 +154,7 @@ export const checkAuth = (to) => {
// 如果没有 matched 属性,则回退到仅依赖 path 的白名单匹配
const needAuthByMeta = to.matched
? to.matched.some(record => record.meta && record.meta.requiresAuth === true)
: false;
: false
const needAuth = needAuthByList || needAuthByMeta
...
...
src/utils/__tests__/oauthHashRestore.test.js
0 → 100644
View file @
5ec6892
import
{
describe
,
expect
,
it
}
from
'vitest'
import
{
buildOAuthAuthorizeUrl
,
getOAuthRestoredUrl
}
from
'../oauthHashRestore'
describe
(
'oauthHashRestore'
,
()
=>
{
it
(
'should build oauth authorize url with split base and hash'
,
()
=>
{
const
href
=
'https://wxm.behalo.cc/f/mlaj/#/studyDetail/3552321?from_course_id=2995248'
expect
(
buildOAuthAuthorizeUrl
(
href
)).
toBe
(
'/srv/?f=behalo&a=openid&res=https%3A%2F%2Fwxm.behalo.cc%2Ff%2Fmlaj%2F&ret_hash=%23%2FstudyDetail%2F3552321%3Ffrom_course_id%3D2995248'
)
})
it
(
'should append test_openid in dev mode'
,
()
=>
{
const
href
=
'https://wxm.behalo.cc/f/mlaj/#/studyDetail/3552321'
expect
(
buildOAuthAuthorizeUrl
(
href
,
{
is_dev
:
true
,
test_openid
:
'abc123'
})).
toBe
(
'/srv/?f=behalo&a=openid&res=https%3A%2F%2Fwxm.behalo.cc%2Ff%2Fmlaj%2F&ret_hash=%23%2FstudyDetail%2F3552321&test_openid=abc123'
)
})
it
(
'should restore ret_hash when current url has no hash'
,
()
=>
{
const
href
=
'https://wxm.behalo.cc/f/mlaj/?code=abc&state=1&ret_hash=%23%2Flogin%3Fredirect%3D%252FstudyDetail%252F3552321%253Ffrom_course_id%253D2995248'
expect
(
getOAuthRestoredUrl
(
href
)).
toBe
(
'https://wxm.behalo.cc/f/mlaj/?code=abc&state=1#/login?redirect=%2FstudyDetail%2F3552321%3Ffrom_course_id%3D2995248'
)
})
it
(
'should not override an existing hash'
,
()
=>
{
const
href
=
'https://wxm.behalo.cc/f/mlaj/?ret_hash=%23%2FstudyDetail%2F3552321#/checkin/index?id=3189684'
expect
(
getOAuthRestoredUrl
(
href
)).
toBeNull
()
})
it
(
'should return null when ret_hash is missing'
,
()
=>
{
const
href
=
'https://wxm.behalo.cc/f/mlaj/?code=abc&state=1'
expect
(
getOAuthRestoredUrl
(
href
)).
toBeNull
()
})
it
(
'should return null for invalid authorize target'
,
()
=>
{
expect
(
buildOAuthAuthorizeUrl
(
''
)).
toBeNull
()
})
})
src/utils/oauthHashRestore.js
0 → 100644
View file @
5ec6892
/**
* @description 从 OAuth 回跳 URL 中恢复 hash 路由。
* 某些回跳链路只会稳定保留 base URL,因此需要借助 ret_hash 在前端复原原始 hash。
* @param {string} href 当前完整地址
* @returns {string|null} 需要替换的新地址;无需处理时返回 null
*/
export
const
getOAuthRestoredUrl
=
href
=>
{
if
(
!
href
||
typeof
href
!==
'string'
)
return
null
let
url
=
null
try
{
url
=
new
URL
(
href
)
}
catch
(
error
)
{
return
null
}
const
ret_hash
=
url
.
searchParams
.
get
(
'ret_hash'
)
if
(
!
ret_hash
||
url
.
hash
)
return
null
url
.
searchParams
.
delete
(
'ret_hash'
)
const
base
=
url
.
toString
().
split
(
'#'
)[
0
]
return
`
${
base
}${
ret_hash
}
`
}
/**
* @description 构造微信 OAuth 授权地址,显式拆分 base_url 与 hash。
* @param {string} target_href 授权前的目标地址
* @param {{ is_dev?: boolean, test_openid?: string }} [options] 额外选项
* @returns {string|null} 可直接跳转的授权地址;输入非法时返回 null
*/
export
const
buildOAuthAuthorizeUrl
=
(
target_href
,
options
=
{})
=>
{
if
(
!
target_href
||
typeof
target_href
!==
'string'
)
return
null
const
[
base_part
,
...
hash_parts
]
=
target_href
.
split
(
'#'
)
if
(
!
base_part
)
return
null
const
ret_hash
=
hash_parts
.
length
?
`#
${
hash_parts
.
join
(
'#'
)}
`
:
''
const
base_url
=
encodeURIComponent
(
base_part
)
const
encoded_hash
=
encodeURIComponent
(
ret_hash
)
let
short_url
=
`/srv/?f=behalo&a=openid&res=
${
base_url
}
&ret_hash=
${
encoded_hash
}
`
if
(
options
.
is_dev
&&
options
.
test_openid
)
{
short_url
+=
`&test_openid=
${
encodeURIComponent
(
options
.
test_openid
)}
`
}
return
short_url
}
/**
* @description 在应用初始化前尝试恢复 OAuth 回跳丢失的 hash。
* @returns {boolean} 是否发生了地址恢复
*/
export
const
restoreHashAfterOAuth
=
()
=>
{
if
(
typeof
window
===
'undefined'
||
typeof
window
.
location
===
'undefined'
)
{
return
false
}
const
next_url
=
getOAuthRestoredUrl
(
window
.
location
.
href
)
if
(
!
next_url
)
return
false
window
.
history
.
replaceState
(
null
,
''
,
next_url
)
return
true
}
src/views/auth.vue
View file @
5ec6892
...
...
@@ -12,8 +12,9 @@
<script setup>
import { onMounted } from 'vue'
import { useRoute } from 'vue-router'
import { buildOAuthAuthorizeUrl } from '@/utils/oauthHashRestore'
const $route = useRoute()
;
const $route = useRoute()
onMounted(() => {
// php需要先跳转链接获取openid
...
...
@@ -22,11 +23,12 @@ onMounted(() => {
* 该方法不会对 ASCII 字母和数字进行编码,也不会对这些 ASCII 标点符号进行编码: - _ . ! ~ * ' ( ) 。
* 其他字符(比如 :;/?:@&=+$,# 这些用于分隔 URI 组件的标点符号),都是由一个或多个十六进制的转义序列替换的。
*/
let raw_url = encodeURIComponent(location.origin + location.pathname + $route.query.href); // 未授权的地址
// TAG: 开发环境测试数据
const short_url = `/srv/?f=behalo&a=openid&res=${raw_url}`;
location.href = import.meta.env.DEV
? `${short_url}&test_openid=${import.meta.env.VITE_OPENID}`
: `${short_url}`;
const target_href = `${location.origin}${location.pathname}${String($route.query.href || '')}`
const short_url = buildOAuthAuthorizeUrl(target_href, {
is_dev: import.meta.env.DEV,
test_openid: import.meta.env.VITE_OPENID,
})
if (!short_url) return
location.href = short_url
})
</script>
...
...
Please
register
or
login
to post a comment