src/
├── auth/ # 认证模块
│ ├── stores/ # 状态管理
│ │ └── auth.ts # 认证状态
│ ├── composables/ # 组合式函数
│ │ └── useAuth.ts # 认证逻辑
│ ├── components/ # 认证组件
│ │ ├── Login.vue # 登录组件
│ │ └── AuthGuard.vue# 权限守卫组件
│ └── services/ # API服务
│ └── authService.ts # 认证API
├── router/ # 路由配置
│ └── index.ts # 路由守卫
└── utils/ # 工具函数
└── token.ts # Token管理
// auth/stores/auth.ts
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';
import { useRouter } from 'vue-router';
import { authService } from '@/auth/services/authService';
import { useToken } from '@/auth/composables/useToken';
export const useAuthStore = defineStore('auth', () => {
const router = useRouter();
const { getToken, setToken, removeToken } = useToken();
// 状态
const user = ref(null);
const token = ref(getToken());
const loading = ref(false);
// 获取器
const isAuthenticated = computed(() => !!token.value);
// 登录方法
const login = async (credentials) => {
loading.value = true;
try {
const response = await authService.login(credentials);
const { token: newToken, user: newUser } = response;
setToken(newToken);
token.value = newToken;
user.value = newUser;
await router.push('/dashboard');
} catch (error) {
console.error('Login error:', error);
throw error;
} finally {
loading.value = false;
}
};
// 登出方法
const logout = () => {
removeToken();
token.value = null;
user.value = null;
router.push('/login');
};
// 刷新用户信息
const refreshUser = async () => {
if (!token.value) return;
try {
const response = await authService.getUserInfo();
user.value = response;
} catch (error) {
console.error('Refresh user error:', error);
logout();
}
};
return {
user,
token,
loading,
isAuthenticated,
login,
logout,
refreshUser,
};
});
// auth/composables/useToken.ts
export const useToken = () => {
const TOKEN_KEY = 'auth_token';
// 获取Token
const getToken = () => {
return localStorage.getItem(TOKEN_KEY);
};
// 设置Token
const setToken = (token: string) => {
localStorage.setItem(TOKEN_KEY, token);
};
// 移除Token
const removeToken = () => {
localStorage.removeItem(TOKEN_KEY);
};
return {
getToken,
setToken,
removeToken,
};
};
// auth/services/authService.ts
import axios from '@/utils/axios';
export const authService = {
// 登录
login(credentials: { username: string; password: string }) {
return axios.post('/api/auth/login', credentials);
},
// 注册
register(userData: { username: string; password: string; email: string }) {
return axios.post('/api/auth/register', userData);
},
// 获取用户信息
getUserInfo() {
return axios.get('/api/auth/user');
},
// 刷新Token
refreshToken() {
return axios.post('/api/auth/refresh');
},
// 登出
logout() {
return axios.post('/api/auth/logout');
},
};
// router/index.ts
import { createRouter, createWebHistory } from 'vue-router';
import { useAuthStore } from '@/auth/stores/auth';
const routes = [
{
path: '/login',
name: 'Login',
component: () => import('@/auth/components/Login.vue'),
meta: { guest: true },
},
{
path: '/dashboard',
name: 'Dashboard',
component: () => import('@/views/Dashboard.vue'),
meta: { requiresAuth: true },
},
{
path: '/admin',
name: 'Admin',
component: () => import('@/views/Admin.vue'),
meta: { requiresAuth: true, roles: ['admin'] },
},
];
const router = createRouter({
history: createWebHistory(),
routes,
});
// 全局前置守卫
router.beforeEach(async (to, from, next) => {
const authStore = useAuthStore();
// 检查是否需要认证
if (to.meta.requiresAuth) {
// 如果未认证,重定向到登录页
if (!authStore.isAuthenticated) {
return next({ name: 'Login' });
}
// 检查角色权限
if (to.meta.roles && !to.meta.roles.includes(authStore.user?.role)) {
return next({ name: 'Forbidden' });
}
}
// 如果是登录页且已认证,重定向到首页
if (to.meta.guest && authStore.isAuthenticated) {
return next({ name: 'Dashboard' });
}
// 刷新用户信息
if (authStore.isAuthenticated && !authStore.user) {
await authStore.refreshUser();
}
next();
});
export default router;
<!-- auth/components/Login.vue -->
<template>
<div class="login-container">
<div class="login-card">
<h2 class="login-title">用户登录</h2>
<form @submit.prevent="handleLogin">
<div class="form-group">
<label for="username">用户名</label>
<input
type="text"
id="username"
v-model="form.username"
:class="{ 'is-invalid': errors.username }"
placeholder="请输入用户名"
/>
<div v-if="errors.username" class="invalid-feedback">
{{ errors.username }}
</div>
</div>
<div class="form-group">
<label for="password">密码</label>
<input
type="password"
id="password"
v-model="form.password"
:class="{ 'is-invalid': errors.password }"
placeholder="请输入密码"
/>
<div v-if="errors.password" class="invalid-feedback">
{{ errors.password }}
</div>
</div>
<button
type="submit"
class="login-button"
:disabled="loading"
>
{{ loading ? '登录中...' : '登录' }}
</button>
</form>
<div class="login-footer">
<router-link to="/register">注册账号</router-link>
<router-link to="/forgot-password">忘记密码?</router-link>
</div>
</div>
</div>
</template>
<script setup>
import { ref, reactive } from 'vue';
import { useAuthStore } from '@/auth/stores/auth';
const authStore = useAuthStore();
// 表单数据
const form = reactive({
username: '',
password: '',
});
// 错误信息
const errors = reactive({
username: '',
password: '',
});
// 加载状态
const loading = ref(false);
// 登录处理
const handleLogin = async () => {
// 重置错误
errors.username = '';
errors.password = '';
// 简单验证
if (!form.username) {
errors.username = '请输入用户名';
return;
}
if (!form.password) {
errors.password = '请输入密码';
return;
}
loading.value = true;
try {
await authStore.login(form);
} catch (error: any) {
if (error.response && error.response.status === 401) {
errors.password = '用户名或密码错误';
} else {
errors.common = '登录失败,请稍后重试';
}
} finally {
loading.value = false;
}
};
</script>
<!-- auth/components/AuthGuard.vue -->
<template>
<div v-if="hasAccess">
<slot />
</div>
<div v-else>
<div v-if="!isAuthenticated" class="auth-message">
<p>请先登录以访问此页面</p>
<button @click="redirectToLogin">登录</button>
</div>
<div v-else class="auth-message">
<p>您没有权限访问此页面</p>
<button @click="redirectToHome">返回首页</button>
</div>
</div>
</template>
<script setup>
import { computed } from 'vue';
import { useAuthStore } from '@/auth/stores/auth';
import { useRouter } from 'vue-router';
const props = defineProps({
roles: {
type: Array,
default: () => [],
},
});
const authStore = useAuthStore();
const router = useRouter();
// 是否已认证
const isAuthenticated = computed(() => authStore.isAuthenticated);
// 是否有权限访问
const hasAccess = computed(() => {
if (!isAuthenticated.value) return false;
if (!props.roles.length) return true;
return props.roles.includes(authStore.user?.role);
});
// 重定向到登录页
const redirectToLogin = () => {
router.push({ name: 'Login' });
};
// 重定向到首页
const redirectToHome = () => {
router.push({ name: 'Home' });
};
</script>
<!-- views/Admin.vue -->
<template>
<div>
<AuthGuard roles="['admin']">
<h1>管理员面板</h1>
<p>这里是只有管理员可以访问的内容</p>
</AuthGuard>
</div>
</template>
<script setup>
import AuthGuard from '@/auth/components/AuthGuard.vue';
</script>
<template>
<div>
<button v-if="isAdmin" @click="manageUsers">管理用户</button>
<AuthGuard roles="['editor']">
<button @click="editArticle">编辑文章</button>
</AuthGuard>
</div>
</template>
<script setup>
import { computed } from 'vue';
import { useAuthStore } from '@/auth/stores/auth';
import AuthGuard from '@/auth/components/AuthGuard.vue';
const authStore = useAuthStore();
const isAdmin = computed(() => authStore.user?.role === 'admin');
</script>
// auth/composables/useRefreshToken.ts
import { ref, onMounted, watch } from 'vue';
import { useAuthStore } from '@/auth/stores/auth';
import { authService } from '@/auth/services/authService';
export const useRefreshToken = () => {
const authStore = useAuthStore();
const refreshTimer = ref<NodeJS.Timeout | null>(null);
// 刷新Token
const refreshToken = async () => {
if (!authStore.isAuthenticated) return;
try {
const response = await authService.refreshToken();
const { token } = response;
authStore.token = token;
localStorage.setItem('auth_token', token);
// 设置下一次刷新时间
setRefreshTimer();
} catch (error) {
console.error('Token refresh failed:', error);
authStore.logout();
}
};
// 设置刷新定时器
const setRefreshTimer = () => {
if (refreshTimer.value) {
clearTimeout(refreshTimer.value);
}
// 假设Token有效期为1小时,提前10分钟刷新
const expiresIn = 60 * 60 * 1000; // 1小时
const refreshBefore = 10 * 60 * 1000; // 10分钟
refreshTimer.value = setTimeout(refreshToken, expiresIn - refreshBefore);
};
onMounted(() => {
if (authStore.isAuthenticated) {
setRefreshTimer();
}
});
watch(() => authStore.isAuthenticated, (newValue) => {
if (newValue) {
setRefreshTimer();
} else if (refreshTimer.value) {
clearTimeout(refreshTimer.value);
refreshTimer.value = null;
}
});
return {
refreshToken,
};
};
// utils/axios.ts
import axios from 'axios';
import { useAuthStore } from '@/auth/stores/auth';
import { useRefreshToken } from '@/auth/composables/useRefreshToken';
const service = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL,
timeout: 10000,
});
// 请求拦截器
service.interceptors.request.use(
(config) => {
const authStore = useAuthStore();
if (authStore.token) {
config.headers.Authorization = `Bearer ${authStore.token}`;
}
return config;
},
(error) => {
console.error('Request error:', error);
return Promise.reject(error);
}
);
// 响应拦截器
service.interceptors.response.use(
(response) => {
return response;
},
async (error) => {
const originalRequest = error.config;
const authStore = useAuthStore();
const { refreshToken } = useRefreshToken();
// 如果响应状态码是401(未授权)并且还没有重试
if (error.response.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
try {
// 刷新Token
await refreshToken();
// 使用新的Token重试请求
return service(originalRequest);
} catch (refreshError) {
// 刷新Token失败,跳转到登录页
authStore.logout();
return Promise.reject(refreshError);
}
}
return Promise.reject(error);
}
);
export default service;
通过以上方案,我们实现了一个完整的Vue登录授权功能封装,包括:
这种封装方式使登录授权功能在Vue应用中变得清晰、可维护,并且可以轻松扩展以满足不同项目的需求。
Vue, 登录授权,功能封装,技术方案,前端开发,身份验证,JWT,Token, 路由守卫,状态管理,Vue Router,Vuex, 权限控制,单点登录,前端安全
资源地址:
https://pan.quark.cn/s/dc62840f6d67
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。