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'
const authStore = useAuthStore()
const isAuthenticated = computed(() => {
return localStorage.getItem('token')
})
const isAuthenticated = computed(() => authStore.isAuthenticated)
const handleLogout = () => {
authStore.logout()
......
import { createRouter, createWebHistory } from 'vue-router'
import { useAuthStore } from '@/stores/auth'
import HomeView from '../views/HomeView.vue'
import LoginView from '../views/LoginView.vue'
import ProfileView from '../views/ProfileView.vue'
......@@ -32,18 +33,31 @@ const router = createRouter({
})
// 路由守卫
router.beforeEach((to, from, next) => {
const isAuthenticated = localStorage.getItem('token') // 简单的认证检查
router.beforeEach(async (to, from, next) => {
const authStore = useAuthStore()
const isAuthed = authStore.isAuthenticated
if (to.meta.requiresAuth && !isAuthenticated) {
// 需要认证但未登录,重定向到登录页
if (to.meta.requiresAuth && !isAuthed) {
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('/')
} else {
next()
return
}
next()
})
export default router
import { ref, computed } from 'vue'
import { defineStore } from 'pinia'
import { useRouter } from 'vue-router'
import axios from 'axios'
export const useAuthStore = defineStore('auth', () => {
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 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) => {
try {
// TODO: 调用登录API
console.log('登录凭证:', credentials)
// 模拟登录成功
const mockToken = 'mock-jwt-token-' + Date.now()
const mockUser = {
id: 1,
name: '张三',
email: credentials.email,
role: 'student',
const { username, password } = credentials || {}
// 基于后端接口进行真实登录
const response = await axios.post(
'http://192.168.1.52:8001/api/token/',
{
username,
password,
},
{
headers: {
'Content-Type': 'application/json',
},
}
)
const { access, refresh } = response.data || {}
if (!access) {
return { success: false, error: '未获取到访问令牌' }
}
// 保存认证信息
token.value = mockToken
user.value = mockUser
localStorage.setItem('token', mockToken)
localStorage.setItem('user', JSON.stringify(mockUser))
accessToken.value = access
refreshToken.value = refresh || null
user.value = { username }
localStorage.setItem('accessToken', access)
if (refresh) localStorage.setItem('refreshToken', refresh)
localStorage.setItem('user', JSON.stringify(user.value))
// 设置全局请求头,方便后续接口调用
setAxiosAuthHeader(access)
// 跳转到主页
router.push('/')
return { success: true }
} catch (error) {
const message =
error?.response?.data?.detail || error?.response?.data?.message || error.message || '登录失败'
console.error('登录失败:', error)
return { success: false, error: error.message }
return { success: false, error: message }
}
}
const logout = () => {
// 清除认证信息
token.value = null
accessToken.value = null
refreshToken.value = null
user.value = null
localStorage.removeItem('token')
localStorage.removeItem('accessToken')
localStorage.removeItem('refreshToken')
localStorage.removeItem('user')
// 清除全局请求头
setAxiosAuthHeader(null)
// 跳转到登录页
router.push('/login')
}
const initializeAuth = () => {
const savedToken = localStorage.getItem('token')
const savedToken = localStorage.getItem('accessToken')
const savedRefreshToken = localStorage.getItem('refreshToken')
const savedUser = localStorage.getItem('user')
if (savedToken && savedUser) {
token.value = savedToken
accessToken.value = savedToken
refreshToken.value = savedRefreshToken
user.value = JSON.parse(savedUser)
// 初始化时设置全局请求头
setAxiosAuthHeader(savedToken)
}
// 初始化 Axios 拦截器
setupAxiosInterceptors()
}
return {
token,
accessToken,
user,
isAuthenticated,
isAccessTokenExpired,
isAccessTokenExpiringSoon,
refreshAccessToken,
setupAxiosInterceptors,
login,
logout,
initializeAuth,
refreshToken,
}
})
......@@ -3,14 +3,14 @@ import { ref } from 'vue'
import { useAuthStore } from '../stores/auth'
const authStore = useAuthStore()
const email = ref('')
const username = ref('')
const password = ref('')
const loading = ref(false)
const error = ref('')
const handleLogin = async () => {
if (!email.value || !password.value) {
error.value = '请填写邮箱和密码'
if (!username.value || !password.value) {
error.value = '请填写用户名和密码'
return
}
......@@ -19,7 +19,7 @@ const handleLogin = async () => {
try {
const result = await authStore.login({
email: email.value,
username: username.value,
password: password.value,
})
......@@ -48,11 +48,11 @@ const handleLogin = async () => {
</v-alert>
<v-form @submit.prevent="handleLogin">
<v-text-field
v-model="email"
label="邮箱"
name="email"
prepend-icon="mdi-email"
type="email"
v-model="username"
label="用户名"
name="username"
prepend-icon="mdi-account"
type="text"
required
/>
<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