Commit cf4bbfe0 authored by Administrator's avatar Administrator
Browse files

completed step 2 including JWT token processes

parent e7411267
...@@ -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()
......
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
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 mockToken = 'mock-jwt-token-' + Date.now() const response = await axios.post(
const mockUser = { 'http://192.168.1.52:8001/api/token/',
id: 1, {
name: '张三', username,
email: credentials.email, password,
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 {
token, accessToken,
user, user,
isAuthenticated, isAuthenticated,
isAccessTokenExpired,
isAccessTokenExpiringSoon,
refreshAccessToken,
setupAxiosInterceptors,
login, login,
logout, logout,
initializeAuth, initializeAuth,
refreshToken,
} }
}) })
...@@ -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
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment