
登录认证与退出登录

登录认证实现步骤
第一步、登录路由配置
关于 /login 登录组件路由配置:在 src/router/index.ts 新定义一个 fullscreenRoutes 指定全屏显示路由(登录页),不作用到 layout 布局的渲染出口。
typescript
import { createRouter, createWebHistory } from 'vue-router'
import type { RouteRecordRaw } from 'vue-router'
// 扩展 RouteMeta 接口,因为 Vue-Router 的配置路由对象的 meta 属性有限,所以需要扩展
declare module 'vue-router' {
interface RouteMeta {
title?: string; // 菜单标题
icon?: string; // 图标
linkTo?: string; // 外链地址
cache?: boolean; //是否缓存:true缓存,false不缓存,会将 name 值用于 <keep-alive>的includes上
hidden?: boolean; // 是否在菜单中显示:true显示,false隐藏
isBreadcrumb?: boolean; // 是否显示到面包屑,默认或true会显示,false不显示。
}
}
/**
* 路由表配置数组(单独抽取,后面扩展后端动态加载)
*/
export const dynamicRoutes: RouteRecordRaw[] = [... 省略]
/**
* 全屏显示路由,不作用到 layout 布局渲染出口。
* (后端路由控制:后端配置菜单数据中不需要下面的菜单项)
*/
export const fullscreenRoutes: RouteRecordRaw[] = [
{
path: '/login',
name: 'Login',
component: () => import('@/views/auth/login.vue'),
meta: {
title: '登录',
hidden: true,
},
},
];
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [...dynamicRoutes, ...fullscreenRoutes]
})
export default router
第二步、实现登录页效果
-
声明登录表单的数据类型
typescript// 登录信息 declare interface LoginData { username: string; password: string; }

-
实现静态页面,静态页面代码如下,直接使用即可
typescript<script setup lang="ts"> import { ref, reactive, toRefs } from 'vue'; import { isUsername, isPassword } from '@/utils/validate'; import { useAuthStore } from '@/stores/auth'; import { useRouter, useRoute } from 'vue-router'; const authStore = useAuthStore(); const router = useRouter(); const route = useRoute(); const formRef = ref(); const state = reactive({ loading: false, isRemember: true, // 是否记住密码 loginData: { username: '', password: '', } as LoginData }); const { loading, isRemember, loginData } = {...toRefs(state)}; /** * 校验用户名是否合法 * @param rule * @param value * @param callback */ function checkUsername(rule: any, value: string, callback: Function) { if (!value) return callback(new Error('请输入有效帐号/手机号!')); if (!isUsername(value)) return callback(new Error('输入的格式不正确,请重新输入!')); callback(); // 通过 } /** * 校验密码是否合法 */ function checkPassword(rule: any, value: string, callback: Function) { if(!value) return callback(new Error('请输入有效密码!')); if(!isPassword(value)) return callback(new Error('密码输入错误,请重新输入!')); callback();// 通过 } // 提交表单 function submitForm(){ } </script> <template> <div class="login-container"> <div class="login-wrap"> <img class="lagin-logo" src="@/assets/logo-01.png"/> <div class="login-title"> 帐号登录 </div> <el-form class="login-form" ref="formRef" :model="loginData" size="large"> <el-form-item prop="username" :rules="{required: true, validator: checkUsername, trigger: 'blur'}"> <el-input v-model="loginData.username" placeholder="请输入帐号/手机号" maxlength="30" prefix-icon="ele-User" clearable/> </el-form-item> <el-form-item prop="password" :rules="{required: true, validator: checkPassword, trigger: 'blur'}"> <el-input v-model="loginData.password" type="password" placeholder="请输入密码" maxlength="30" prefix-icon="ele-Unlock" show-password clearable/> </el-form-item> <el-form-item> <div class="login-other"> <el-checkbox v-model="isRemember"> <span>记住密码</span> </el-checkbox> </div> <el-button class="login-submit" @click="submitForm()" :loading="loading" type="primary"> 登 录 </el-button> </el-form-item> </el-form> </div> </div> </template> <style lang="scss" scoped> .login-container { height: 100%; width: 100%; background-image: url('@/assets/loginBj.png'); background-size: cover; overflow: hidden; display: flex; align-items: center; // justify-content: center; .login-wrap { width: 410px; height: 460px; background-color: #fff; padding: 30px; margin: auto; margin-right: 130px; box-shadow: #74747462 0 2px 15px; border-radius: 10px; .lagin-logo { max-width: 130px; margin: auto; display: flex; } .login-title { font-size: 20px; height: 70px; line-height: 70px; font-weight: 500; text-align: left; color: #0d1234; letter-spacing: 2px; } .login-form { .el-form-item { margin-bottom: 27px; } .login-other { span { font-size: 13px; font-weight: 500; color: #999; } } .login-submit { width: 100%; height: 40px; letter-spacing: 3px; font-weight: 500; } } } } </style>
效果: 
第三步、Pinia 管理认证状态
-
创建
utils/storage.tstypescript/** * window.localStorage 浏览器永久缓存 * @method set 设置永久缓存 * @method get 获取永久缓存 * @method remove 移除永久缓存 * @method clear 移除全部永久缓存 */ export const Local = { // 设置永久缓存 set(key: string, val: any) { window.localStorage.setItem(key, JSON.stringify(val || '')); }, // 获取永久缓存 get(key: string) { let json: any = window.localStorage.getItem(key); return JSON.parse(json); }, // 移除永久缓存 remove(key: string) { window.localStorage.removeItem(key); }, // 移除全部永久缓存 clear() { window.localStorage.clear(); }, }; /** * window.sessionStorage 浏览器临时缓存 * @method set 设置临时缓存 * @method get 获取临时缓存 * @method remove 移除临时缓存 * @method clear 移除全部临时缓存 */ export const Session = { // 设置临时缓存 set(key: string, val: any) { // val为undefined保存时,当get时parse转成json对象会失败,加上 || ''解决这个 window.sessionStorage.setItem(key, JSON.stringify(val || '')); }, // 获取临时缓存 get(key: string) { let json: any = window.sessionStorage.getItem(key); // parse 无法对undefined 和 ''进行解析的 return JSON.parse(json); }, // 移除临时缓存 remove(key: string) { window.sessionStorage.removeItem(key); }, // 移除全部临时缓存 clear() { window.sessionStorage.clear(); }, }; -
在
src/types/pinia.d.ts定义 认证状态 类型:typescript// 用户认证信息 declare interface AuthState<T = any> { rememberData?: LoginData; // 记住我(登录数据) accessToken?: string; //访问令牌 } -
创建
src/stores/auth.ts,Pinia实现记住密码、登录处理:
记住密码:将登录信息保存下来。 提交登录信息处理,后端接口校验用户名密码正确后,会返回令牌
access_token,将令牌保存;
typescript
import { defineStore } from 'pinia';
import type { RouteRecordRaw } from 'vue-router';
import { Session, Local } from '@/utils/storage';
import { login } from '@/api/auth';
// 保存到 Local或 Session的key名
export const Key = {
rememberKey: 'isRemember', // 记住密码的key
accessTokenKey: 'accessToken', // 访问令牌本地保存的key
}
/**
* 用户所拥有的路由权限
*/
export const useAuthStore = defineStore('auth', {
state: (): AuthState<RouteRecordRaw> => {
return {
rememberData: Local.get(Key.rememberKey), // 记住密码
accessToken: Session.get(Key.accessTokenKey), // 访问令牌字符串
}
},
actions: {
// 记住密码
setRememberPwd(data?: LoginData) {
this.rememberData = data;
if (data) {
Local.set(Key.rememberKey, { username: data.username, password: data.password });
} else {
Local.remove(Key.rememberKey);
}
},
// 登录操作
userLogin(loginData: LoginData) {
return new Promise((resolve, reject) => {
login(loginData).then((res: any) => {
const { data } = res;
// 状态赋值
const { access_token } = data;
this.accessToken = access_token;
// 保存到session中
Session.set(Key.accessTokenKey, access_token);
// 正常响应钩子
resolve(res);
}).catch((error: Error) => {
reject(error); // 异常
});
});
},
}
});
第四步、提交登录表单
-
创建
api/auth/index.ts文件typescriptimport request from "@/utils/request"; const baseUrl = "/auth"; // 登录系统 export function login(data: LoginData) { return request({ url: `${baseUrl}/token`, method: 'POST', data }); } -
实现表单登录按钮逻辑
typescriptimport { useAuthStore } from "@/stores/auth"; const authStore = useAuthStore(); const state = reactive({ loading: false, isRemember: true, // 是否记住密码 loginData: { username: authStore.rememberData?.username, password: authStore.rememberData?.password, } as LoginData, }); // 提交登录 function submitForm() { // 如果在登录中,不允许重复登录 if (state.loading) return; formRef.value?.validate(async (valid: boolean) => { if (!valid) return false; try { // 登录中 state.loading = true; // 记住密码 authStore.setRememberPwd(state.isRemember ? state.loginData : undefined); // 发送登录请求 await authStore.userLogin(state.loginData); // 跳转来源地址 router.replace({ path: <string>route.query?.redirect || "/" }); } catch (error) { } finally { } }); } -
效果

第六步、axios在请求头上添加认证token
-
在
src/utils/request.ts的axios请求拦截器中针对发送的验合法性:Authorization: Bearer ${accessToken}typescriptimport { useAuthStore } from '@/stores/auth'; // 请求拦截器 request.interceptors.request.use(config => { // 在此处可向请求头加上认证token const authStore = useAuthStore(); // 获取token const accessToken = authStore.accessToken; if (accessToken) { // oauth2 请求头 Authorization: Bearer xxxxx config.headers.Authorization = `Bearer ${accessToken}`; } return config; }, error => { // 出现异常, catch可捕获到 return Promise.reject(error); }) -
观察登录的时候调用接口,接口中的请求头是否有
token
退出登录实现步骤
第一步、调用接口api方法 logout
typescript
import request from "@/utils/request";
const baseUrl = "/auth";
// 退出系统
export function logout() {
return request({
url: `${baseUrl}/logout`,
method: 'POST',
});
}
第二步、pinia 中实现退出功能
typescript
import { defineStore } from 'pinia';
import type { RouteRecordRaw } from 'vue-router';
import { Session, Local } from '@/utils/storage';
import { login, logout } from '@/api/auth';
// 保存到 Local或 Session的key名
export const Key = {
rememberKey: 'isRemember', // 记住密码的key
accessTokenKey: 'accessToken', // 访问令牌本地保存的key
userInfoKey: 'userInfo', // 用户信息本地保存的key
}
/**
* 用户所拥有的路由权限
*/
export const useAuthStore = defineStore('auth', {
state: (): AuthState<RouteRecordRaw> => {
return {
rememberData: Local.get(Key.rememberKey), // 记住密码
accessToken: Session.get(Key.accessTokenKey), // 访问令牌字符串
userInfo: Session.get(Key.userInfoKey),
}
},
actions: {
// 记住密码
setRememberPwd(data?: LoginData) {
this.rememberData = data;
if (data) {
Local.set(Key.rememberKey, { username: data.username, password: data.password });
} else {
Local.remove(Key.rememberKey);
}
},
userLogout() {
return new Promise((resolve, reject) => {
logout().then((res: any) => {
// 重置状态
this.resetUserState();
// 重新加载当前页,需认证页面会去登录页
window.location.reload();
resolve(res);
}).catch((error: Error) => {
reject(error);
})
});
},
// 重置用户状态
resetUserState() {
this.accessToken = undefined;
this.userInfo = undefined;
// 移除保存的数据
Session.remove(Key.accessTokenKey);
Session.remove(Key.userInfoKey);
}
}
});
第三步、触发退出登录事件
-
在
src/layout/layoutHeader/userDropdown.vue显示用户信息和点击退出系统authStore.userLogout()typescript<template> <el-dropdown> <template #dropdown> <el-dropdown-menu> <el-dropdown-item @click="authStore.userLogout()" divided >退出登录</el-dropdown-item > </el-dropdown-menu> </template> </el-dropdown> </div> </template> <script lang="ts" setup> import { useFullscreen, useDark } from "@vueuse/core"; import { useLayoutConfigStore } from "../../stores/layoutConfig"; import { useRouter } from "vue-router"; import { useAuthStore } from "../../stores/auth"; const router = useRouter(); const authStore = useAuthStore(); const layoutConfig = useLayoutConfigStore(); </script>
效果: 