一、项目概述
大事件项目是一个典型的后台管理系统,包含用户登录、权限管理、内容发布等功能模块。其中登录访问拦截是系统的核心安全机制之一,确保只有认证用户才能访问特定资源。
二、技术栈
-
前端:Vue.js + Element UI
-
后端:Node.js + Express/Koa
-
状态管理:Vuex/Pinia
-
路由:Vue Router
-
存储:localStorage/sessionStorage
三、登录拦
1. 路由拦截实现
路由配置
javascript
//src>router>index.js
import { createRouter, createWebHistory } from 'vue-router'
import { useUserStore } from '@/stores'
// createRouter用于创建路由实例,
// 配置history模式
// 1. history模式 createWebHistory 地址栏不带警号
// 2.带井号
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{ path: '/login', component: () => import('@/views/login/loginPage.vue') },
{
path: '/',
component: () => import('@/views/layout/LayoutContainer.vue'),
redirect: '/article/manage',
children: [
{
path: '/article/manage',
component: () => import('@/views/artical/ArticleManage.vue')
},
{
path: '/article/channel',
component: () => import('@/views/artical/ArticleChannel.vue')
},
{
path: '/user/profile',
component: () => import('@/views/user/UserProfile.vue')
},
{
path: '/user/avatar',
component: () => import('@/views/user/UserAvatar.vue')
},
{
path: '/user/password',
component: () => import('@/views/user/UserPassword.vue')
}
]
}
]
路由守卫实现
javascript
//src>router>index.js
router.beforeEach((to) => {
// 会在每次路由跳转之前执行,用来拦截和验证用户是否有权限访问某个页面。
const useStore = useUserStore()
// 从 Pinia store 中获取用户状态信息
if (!useStore.token && to.path !== '/login') return '/login'
// useStore.token 获取用户的登录令牌
return true
})
2. 登录状态管理
Vuex状态管理
javascript
//src>stores>models>user.js
import { userGetInfoService } from '@/api/user'
import { defineStore } from 'pinia'
import { ref } from 'vue'
// 用户模块 token setToken removeToken
export const useUserStore = defineStore(
'big-user',
() => {
// 组合式API写法
const token = ref('')
// token: 存储用户认证令牌的响应式引用
const setToken = (newToken) => {
// setToken(newToken): 设置新的令牌
token.value = newToken
}
const removeToken = () => {
// removeToken(): 清除令牌
token.value = ''
}
const user = ref({})
// user: 存储用户信息的响应式引用,初始值为空对象
const getUser = async () => {
const res = await userGetInfoService()
// getUser(): 异步函数,调用 API 获取用户信息并更新 user 状态
user.value = res.data.data
}
return {
token,
setToken,
removeToken,
user,
getUser
}
},
{
persist: true
// persist: true 表示这个状态存储会被持久化保存,即使页面刷新也能保持状态
}
)
3. 登录组件实现
javascript
//src>views>login>loginPage.vue
<script setup>
import { userRegisterService, userLoginService } from '@/api/user.js'
// import router from '@/router'
import { useUserStore } from '@/stores'
import { User, Lock } from '@element-plus/icons-vue'
import { ref, watch } from 'vue'
import { useRouter } from 'vue-router'
const isRegister = ref(false)
const form = ref()
// 整个的用于提交的数据对象
const formModel = ref({
username: '',
password: '',
repassword: ''
})
// 整个表单的校验规则
const rules = {
username: [
{ required: true, message: 'Please input Activity name', trigger: 'blur' },
{ min: 5, max: 10, message: '用户名', trigger: 'blur' }
],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' },
{
pattern: /^\S{6,15}$/,
message: '密码必须是6-15位的非空字符',
trigger: 'blur'
}
],
repassword: [
{ required: true, message: '请输入密码', trigger: 'blur' },
{
// 自定义校验
validator: (rule, value, callback) => {
if (value !== formModel.value.password) {
callback(new Error('两次输入密码不一致'))
} else {
callback() //就算成立了也需要callback
}
},
trigger: 'blur'
}
]
}
const register = async () => {
await form.value.validate()
await userRegisterService(formModel.value)
ElMessage.success('Congrats, this is a success message.')
// isRegister.value = false
}
// 调用方法将 token 存入 pinia 并 自动持久化本地
const userStore = useUserStore()
const router = useRouter()
const login = async () => {
await form.value.validate()
const res = await userLoginService(formModel.value)
userStore.setToken(res.data.token)
ElMessage.success('登录成功')
router.push('/')
console.log('开始登录', res)
}
//切换的时候,重置表单内容
watch(isRegister, () => {
formModel.value = {
username: '',
password: '',
repassword: ''
}
})
</script>
<template>
<el-row class="login-page">
<el-col :span="12" class="bg"></el-col>
<el-col :span="6" :offset="3" class="form">
<!-- 注册相关表单 -->
<el-form
:model="formModel"
:rules="rules"
ref="form"
size="large"
autocomplete="off"
v-if="isRegister"
>
<el-form-item>
<h1>注册</h1>
</el-form-item>
<el-form-item prop="username">
<el-input
v-model="formModel.username"
:prefix-icon="User"
placeholder="请输入用户名"
></el-input>
</el-form-item>
<el-form-item prop="password">
<el-input
v-model="formModel.password"
:prefix-icon="Lock"
type="password"
placeholder="请输入密码"
></el-input>
</el-form-item>
<el-form-item prop="repassword">
<el-input
v-model="formModel.repassword"
:prefix-icon="Lock"
type="password"
placeholder="请再次输入密码"
></el-input>
</el-form-item>
<el-form-item>
<el-button
@click="register"
class="button"
type="primary"
auto-insert-space
>
注册
</el-button>
</el-form-item>
<el-form-item class="flex">
<el-link type="info" :underline="false" @click="isRegister = false">
← 返回
</el-link>
</el-form-item>
</el-form>
<!-- 登录相关表单 -->
<el-form
:model="formModel"
:rules="rules"
ref="form"
size="large"
autocomplete="off"
v-else
>
<el-form-item>
<h1>登录</h1>
</el-form-item>
<el-form-item prop="username">
<el-input
v-model="formModel.username"
name="username"
:prefix-icon="User"
placeholder="请输入用户名"
></el-input>
</el-form-item>
<el-form-item prop="password">
<el-input
v-model="formModel.password"
name="password"
:prefix-icon="Lock"
type="password"
placeholder="请输入密码"
></el-input>
</el-form-item>
<el-form-item class="flex">
<div class="flex">
<el-checkbox>记住我</el-checkbox>
<el-link type="primary" :underline="false">忘记密码?</el-link>
</div>
</el-form-item>
<el-form-item>
<el-button
@click="login"
class="button"
type="primary"
auto-insert-space
>登录</el-button
>
</el-form-item>
<el-form-item class="flex">
<el-link type="info" :underline="false" @click="isRegister = true">
注册 →
</el-link>
</el-form-item>
</el-form>
</el-col>
</el-row>
</template>
<style lang="scss" scoped>
.login-page {
height: 100vh;
background-color: #fff;
.bg {
background:
url('@/assets/logo2.png') no-repeat 60% center / 240px auto,
url('@/assets/login_bg.jpg') no-repeat center / cover;
border-radius: 0 20px 20px 0;
}
.form {
display: flex;
flex-direction: column;
justify-content: center;
user-select: none;
.title {
margin: 0 auto;
}
.button {
width: 100%;
}
.flex {
width: 100%;
display: flex;
justify-content: space-between;
}
}
}
</style>
实现页面:

4. 请求拦截器实现
javascript
//src>utils>request
import axios from 'axios'
import { useUserStore } from '@/stores'
import { ElMessage } from 'element-plus'
import router from '@/router'
const baseURL = 'http://big-event-vue-api-t.itheima.net'
const instance = axios.create({
// TODO 1. 基础地址,超时时间
baseURL,
timeout: 10000
})
//请求拦截器
instance.interceptors.request.use(
(config) => {
// TODO 2. 携带token
const useStore = useUserStore()
if (useStore.token) {
config.headers.Authorization = useStore.token
}
return config
},
(err) => Promise.reject(err)
)
//响应拦截器
instance.interceptors.response.use(
(res) => {
// TODO 3. 处理业务失败
// TODO 4. 摘取核心响应数据
if (res.data.code === 0) {
return res
}
//处理业务失败,给错误提示,抛出错误
ElMessage.error(res.data.message || '服务异常')
return Promise.reject(res.data)
},
(err) => {
ElMessage.error(err.response.data.message || '服务异常')
// TODO 5. 处理401错误
if (err.response?.status === 401) {
router.push('/login')
}
//错误的默认qingkuang
ElMessage.error(err.response.data.message)
return Promise.reject(err)
}
)
export default instance
export { baseURL }
五、安全最佳实践
-
密码安全
-
前端应对密码进行基本校验(长度、复杂度)
-
使用HTTPS传输敏感数据
-
-
日志与监控
-
记录登录失败事件
-
监控异常登录行为
-
六、常见问题与解决方案
-
页面刷新后Vuex状态丢失
- 解决方案:结合localStorage实现状态持久化
-
多标签页登录状态同步
- 解决方案:使用storage事件监听localStorage变化
-
权限动态加载
- 解决方案:实现后端返回权限路由表,前端动态添加路由
七、总结
登录访问拦截是Web应用安全的第一道防线,本文详细介绍了从路由拦截、状态管理到请求拦截的完整实现方案。实际项目中应根据具体需求和安全要求进行调整,同时结合后端的安全措施共同构建完善的认证授权体系。