Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in / Register
Toggle navigation
Menu
Open sidebar
Clark Lin
student-app-frontend
Commits
cf4bbfe0
Commit
cf4bbfe0
authored
Aug 19, 2025
by
Administrator
Browse files
completed step 2 including JWT token processes
parent
e7411267
Changes
4
Hide whitespace changes
Inline
Side-by-side
src/App.vue
View file @
cf4bbfe0
...
@@ -4,9 +4,7 @@ import { useAuthStore } from './stores/auth'
...
@@ -4,9 +4,7 @@ import { useAuthStore } from './stores/auth'
const
authStore
=
useAuthStore
()
const
authStore
=
useAuthStore
()
const
isAuthenticated
=
computed
(()
=>
{
const
isAuthenticated
=
computed
(()
=>
authStore
.
isAuthenticated
)
return
localStorage
.
getItem
(
'
token
'
)
})
const
handleLogout
=
()
=>
{
const
handleLogout
=
()
=>
{
authStore
.
logout
()
authStore
.
logout
()
...
...
src/router/index.js
View file @
cf4bbfe0
import
{
createRouter
,
createWebHistory
}
from
'
vue-router
'
import
{
createRouter
,
createWebHistory
}
from
'
vue-router
'
import
{
useAuthStore
}
from
'
@/stores/auth
'
import
HomeView
from
'
../views/HomeView.vue
'
import
HomeView
from
'
../views/HomeView.vue
'
import
LoginView
from
'
../views/LoginView.vue
'
import
LoginView
from
'
../views/LoginView.vue
'
import
ProfileView
from
'
../views/ProfileView.vue
'
import
ProfileView
from
'
../views/ProfileView.vue
'
...
@@ -32,18 +33,31 @@ const router = createRouter({
...
@@ -32,18 +33,31 @@ const router = createRouter({
})
})
// 路由守卫
// 路由守卫
router
.
beforeEach
((
to
,
from
,
next
)
=>
{
router
.
beforeEach
(
async
(
to
,
from
,
next
)
=>
{
const
isAuthenticated
=
localStorage
.
getItem
(
'
token
'
)
// 简单的认证检查
const
authStore
=
useAuthStore
()
const
isAuthed
=
authStore
.
isAuthenticated
if
(
to
.
meta
.
requiresAuth
&&
!
isAuthenticated
)
{
if
(
to
.
meta
.
requiresAuth
&&
!
isAuthed
)
{
// 需要认证但未登录,重定向到登录页
next
(
'
/login
'
)
next
(
'
/login
'
)
}
else
if
(
to
.
path
===
'
/login
'
&&
isAuthenticated
)
{
return
// 已登录用户访问登录页,重定向到主页
}
// 对受保护页面,若即将过期,尝试静默刷新一次(不强制等待,可选:等待)
if
(
to
.
meta
.
requiresAuth
&&
authStore
.
isAccessTokenExpiringSoon
&&
authStore
.
isAccessTokenExpiringSoon
())
{
try
{
await
authStore
.
refreshAccessToken
()
}
catch
{
next
(
'
/login
'
)
return
}
}
if
(
to
.
path
===
'
/login
'
&&
isAuthed
)
{
next
(
'
/
'
)
next
(
'
/
'
)
}
else
{
return
next
()
}
}
next
()
})
})
export
default
router
export
default
router
src/stores/auth.js
View file @
cf4bbfe0
import
{
ref
,
computed
}
from
'
vue
'
import
{
ref
,
computed
}
from
'
vue
'
import
{
defineStore
}
from
'
pinia
'
import
{
defineStore
}
from
'
pinia
'
import
{
useRouter
}
from
'
vue-router
'
import
{
useRouter
}
from
'
vue-router
'
import
axios
from
'
axios
'
export
const
useAuthStore
=
defineStore
(
'
auth
'
,
()
=>
{
export
const
useAuthStore
=
defineStore
(
'
auth
'
,
()
=>
{
const
router
=
useRouter
()
const
router
=
useRouter
()
// 状态
// 状态
const
token
=
ref
(
localStorage
.
getItem
(
'
token
'
)
||
null
)
const
accessToken
=
ref
(
localStorage
.
getItem
(
'
accessToken
'
)
||
null
)
const
refreshToken
=
ref
(
localStorage
.
getItem
(
'
refreshToken
'
)
||
null
)
const
user
=
ref
(
null
)
const
user
=
ref
(
null
)
const
interceptorsInitialized
=
ref
(
false
)
const
isRefreshing
=
ref
(
false
)
let
refreshPromise
=
null
// 计算属性
// 计算属性
const
isAuthenticated
=
computed
(()
=>
!!
token
.
value
)
const
isAuthenticated
=
computed
(()
=>
!!
accessToken
.
value
)
// JWT 工具
const
decodeJwt
=
(
jwt
)
=>
{
try
{
const
payload
=
jwt
.
split
(
'
.
'
)[
1
]
const
decoded
=
atob
(
payload
.
replace
(
/-/g
,
'
+
'
).
replace
(
/_/g
,
'
/
'
))
// 处理可能缺失的 padding
const
json
=
decodeURIComponent
(
decoded
.
split
(
''
)
.
map
((
c
)
=>
'
%
'
+
(
'
00
'
+
c
.
charCodeAt
(
0
).
toString
(
16
)).
slice
(
-
2
))
.
join
(
''
)
)
return
JSON
.
parse
(
json
)
}
catch
{
return
null
}
}
const
getAccessTokenExp
=
()
=>
{
if
(
!
accessToken
.
value
)
return
0
const
payload
=
decodeJwt
(
accessToken
.
value
)
return
payload
?.
exp
?
Number
(
payload
.
exp
)
:
0
}
const
isAccessTokenExpired
=
()
=>
{
const
exp
=
getAccessTokenExp
()
if
(
!
exp
)
return
false
const
nowInSeconds
=
Math
.
floor
(
Date
.
now
()
/
1000
)
return
nowInSeconds
>=
exp
}
const
isAccessTokenExpiringSoon
=
(
thresholdSeconds
=
60
)
=>
{
const
exp
=
getAccessTokenExp
()
if
(
!
exp
)
return
false
const
nowInSeconds
=
Math
.
floor
(
Date
.
now
()
/
1000
)
return
exp
-
nowInSeconds
<=
thresholdSeconds
}
const
setAxiosAuthHeader
=
(
token
)
=>
{
if
(
token
)
{
axios
.
defaults
.
headers
.
common
.
Authorization
=
`Bearer
${
token
}
`
}
else
{
delete
axios
.
defaults
.
headers
.
common
.
Authorization
}
}
const
refreshAccessToken
=
async
()
=>
{
if
(
!
refreshToken
.
value
)
throw
new
Error
(
'
NO_REFRESH_TOKEN
'
)
if
(
isRefreshing
.
value
&&
refreshPromise
)
{
return
refreshPromise
}
isRefreshing
.
value
=
true
refreshPromise
=
axios
.
post
(
'
http://192.168.1.52:8001/api/token/refresh/
'
,
{
refresh
:
refreshToken
.
value
},
{
headers
:
{
'
Content-Type
'
:
'
application/json
'
}
}
)
.
then
((
resp
)
=>
{
const
newAccess
=
resp
.
data
?.
access
if
(
!
newAccess
)
throw
new
Error
(
'
NO_ACCESS_FROM_REFRESH
'
)
accessToken
.
value
=
newAccess
localStorage
.
setItem
(
'
accessToken
'
,
newAccess
)
setAxiosAuthHeader
(
newAccess
)
return
newAccess
})
.
finally
(()
=>
{
isRefreshing
.
value
=
false
refreshPromise
=
null
})
return
refreshPromise
}
const
setupAxiosInterceptors
=
()
=>
{
if
(
interceptorsInitialized
.
value
)
return
// 请求拦截:总是携带最新的访问令牌
axios
.
interceptors
.
request
.
use
((
config
)
=>
{
if
(
accessToken
.
value
)
{
config
.
headers
=
config
.
headers
||
{}
config
.
headers
.
Authorization
=
`Bearer
${
accessToken
.
value
}
`
}
return
config
})
// 响应拦截:遇到 401 尝试刷新一次并重试原请求
axios
.
interceptors
.
response
.
use
(
(
response
)
=>
response
,
async
(
error
)
=>
{
const
originalRequest
=
error
.
config
||
{}
const
status
=
error
?.
response
?.
status
const
isUnauthorized
=
status
===
401
if
(
!
isUnauthorized
)
return
Promise
.
reject
(
error
)
if
(
originalRequest
.
_retry
)
{
// 已重试过,仍然 401,直接登出
logout
()
return
Promise
.
reject
(
error
)
}
originalRequest
.
_retry
=
true
try
{
await
refreshAccessToken
()
originalRequest
.
headers
=
originalRequest
.
headers
||
{}
originalRequest
.
headers
.
Authorization
=
`Bearer
${
accessToken
.
value
}
`
return
axios
(
originalRequest
)
}
catch
(
e
)
{
logout
()
return
Promise
.
reject
(
e
)
}
}
)
interceptorsInitialized
.
value
=
true
}
// 方法
// 方法
const
login
=
async
(
credentials
)
=>
{
const
login
=
async
(
credentials
)
=>
{
try
{
try
{
// TODO: 调用登录API
const
{
username
,
password
}
=
credentials
||
{}
console
.
log
(
'
登录凭证:
'
,
credentials
)
// 基于后端接口进行真实登录
// 模拟登录成功
const
response
=
await
axios
.
post
(
const
mockToken
=
'
mock-jwt-token-
'
+
Date
.
now
()
'
http://192.168.1.52:8001/api/token/
'
,
const
mockUser
=
{
{
id
:
1
,
username
,
name
:
'
张三
'
,
password
,
email
:
credentials
.
email
,
},
role
:
'
student
'
,
{
headers
:
{
'
Content-Type
'
:
'
application/json
'
,
},
}
)
const
{
access
,
refresh
}
=
response
.
data
||
{}
if
(
!
access
)
{
return
{
success
:
false
,
error
:
'
未获取到访问令牌
'
}
}
}
// 保存认证信息
// 保存认证信息
token
.
value
=
mockToken
accessToken
.
value
=
access
user
.
value
=
mockUser
refreshToken
.
value
=
refresh
||
null
localStorage
.
setItem
(
'
token
'
,
mockToken
)
user
.
value
=
{
username
}
localStorage
.
setItem
(
'
user
'
,
JSON
.
stringify
(
mockUser
))
localStorage
.
setItem
(
'
accessToken
'
,
access
)
if
(
refresh
)
localStorage
.
setItem
(
'
refreshToken
'
,
refresh
)
localStorage
.
setItem
(
'
user
'
,
JSON
.
stringify
(
user
.
value
))
// 设置全局请求头,方便后续接口调用
setAxiosAuthHeader
(
access
)
// 跳转到主页
// 跳转到主页
router
.
push
(
'
/
'
)
router
.
push
(
'
/
'
)
return
{
success
:
true
}
return
{
success
:
true
}
}
catch
(
error
)
{
}
catch
(
error
)
{
const
message
=
error
?.
response
?.
data
?.
detail
||
error
?.
response
?.
data
?.
message
||
error
.
message
||
'
登录失败
'
console
.
error
(
'
登录失败:
'
,
error
)
console
.
error
(
'
登录失败:
'
,
error
)
return
{
success
:
false
,
error
:
error
.
message
}
return
{
success
:
false
,
error
:
message
}
}
}
}
}
const
logout
=
()
=>
{
const
logout
=
()
=>
{
// 清除认证信息
// 清除认证信息
token
.
value
=
null
accessToken
.
value
=
null
refreshToken
.
value
=
null
user
.
value
=
null
user
.
value
=
null
localStorage
.
removeItem
(
'
token
'
)
localStorage
.
removeItem
(
'
accessToken
'
)
localStorage
.
removeItem
(
'
refreshToken
'
)
localStorage
.
removeItem
(
'
user
'
)
localStorage
.
removeItem
(
'
user
'
)
// 清除全局请求头
setAxiosAuthHeader
(
null
)
// 跳转到登录页
// 跳转到登录页
router
.
push
(
'
/login
'
)
router
.
push
(
'
/login
'
)
}
}
const
initializeAuth
=
()
=>
{
const
initializeAuth
=
()
=>
{
const
savedToken
=
localStorage
.
getItem
(
'
token
'
)
const
savedToken
=
localStorage
.
getItem
(
'
accessToken
'
)
const
savedRefreshToken
=
localStorage
.
getItem
(
'
refreshToken
'
)
const
savedUser
=
localStorage
.
getItem
(
'
user
'
)
const
savedUser
=
localStorage
.
getItem
(
'
user
'
)
if
(
savedToken
&&
savedUser
)
{
if
(
savedToken
&&
savedUser
)
{
token
.
value
=
savedToken
accessToken
.
value
=
savedToken
refreshToken
.
value
=
savedRefreshToken
user
.
value
=
JSON
.
parse
(
savedUser
)
user
.
value
=
JSON
.
parse
(
savedUser
)
// 初始化时设置全局请求头
setAxiosAuthHeader
(
savedToken
)
}
}
// 初始化 Axios 拦截器
setupAxiosInterceptors
()
}
}
return
{
return
{
t
oken
,
accessT
oken
,
user
,
user
,
isAuthenticated
,
isAuthenticated
,
isAccessTokenExpired
,
isAccessTokenExpiringSoon
,
refreshAccessToken
,
setupAxiosInterceptors
,
login
,
login
,
logout
,
logout
,
initializeAuth
,
initializeAuth
,
refreshToken
,
}
}
})
})
src/views/LoginView.vue
View file @
cf4bbfe0
...
@@ -3,14 +3,14 @@ import { ref } from 'vue'
...
@@ -3,14 +3,14 @@ import { ref } from 'vue'
import
{
useAuthStore
}
from
'
../stores/auth
'
import
{
useAuthStore
}
from
'
../stores/auth
'
const
authStore
=
useAuthStore
()
const
authStore
=
useAuthStore
()
const
email
=
ref
(
''
)
const
username
=
ref
(
''
)
const
password
=
ref
(
''
)
const
password
=
ref
(
''
)
const
loading
=
ref
(
false
)
const
loading
=
ref
(
false
)
const
error
=
ref
(
''
)
const
error
=
ref
(
''
)
const
handleLogin
=
async
()
=>
{
const
handleLogin
=
async
()
=>
{
if
(
!
email
.
value
||
!
password
.
value
)
{
if
(
!
username
.
value
||
!
password
.
value
)
{
error
.
value
=
'
请填写
邮箱
和密码
'
error
.
value
=
'
请填写
用户名
和密码
'
return
return
}
}
...
@@ -19,7 +19,7 @@ const handleLogin = async () => {
...
@@ -19,7 +19,7 @@ const handleLogin = async () => {
try
{
try
{
const
result
=
await
authStore
.
login
({
const
result
=
await
authStore
.
login
({
email
:
email
.
value
,
username
:
username
.
value
,
password
:
password
.
value
,
password
:
password
.
value
,
})
})
...
@@ -48,11 +48,11 @@ const handleLogin = async () => {
...
@@ -48,11 +48,11 @@ const handleLogin = async () => {
</v-alert>
</v-alert>
<v-form
@
submit.prevent=
"handleLogin"
>
<v-form
@
submit.prevent=
"handleLogin"
>
<v-text-field
<v-text-field
v-model=
"
email
"
v-model=
"
username
"
label=
"
邮箱
"
label=
"
用户名
"
name=
"
email
"
name=
"
username
"
prepend-icon=
"mdi-
email
"
prepend-icon=
"mdi-
account
"
type=
"
email
"
type=
"
text
"
required
required
/>
/>
<v-text-field
<v-text-field
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment