Vue3实战七、登录认证与退出登录

登录认证与退出登录

登录认证实现步骤

第一步、登录路由配置

关于 /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

第二步、实现登录页效果

  1. 声明登录表单的数据类型

    typescript 复制代码
    // 登录信息
    declare interface LoginData {
    	username: string;
    	password: string;
    }
  1. 实现静态页面,静态页面代码如下,直接使用即可

    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 管理认证状态

  1. 创建utils/storage.ts

    typescript 复制代码
    /**
     * 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();
    	},
    };
  2. src/types/pinia.d.ts 定义 认证状态 类型:

    typescript 复制代码
    // 用户认证信息
    declare interface AuthState<T = any> {
      rememberData?: LoginData; // 记住我(登录数据)
      accessToken?: string; //访问令牌
    }
  3. 创建 src/stores/auth.tsPinia 实现记住密码、登录处理:

记住密码:将登录信息保存下来。 提交登录信息处理,后端接口校验用户名密码正确后,会返回令牌 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); // 异常
        });
      });
    },
  }
});

第四步、提交登录表单

  1. 创建api/auth/index.ts文件

    typescript 复制代码
    import request from "@/utils/request";
    const baseUrl = "/auth";
    // 登录系统
    export function login(data: LoginData) {
      return request({
        url: `${baseUrl}/token`,
        method: 'POST',
        data
      });
    }
  2. 实现表单登录按钮逻辑

    typescript 复制代码
    import { 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 {
        }
      });
    }
  3. 效果

第六步、axios在请求头上添加认证token

  1. src/utils/request.tsaxios 请求拦截器中针对发送的验合法性:Authorization: Bearer ${accessToken}

    typescript 复制代码
    import { 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);
    })
  2. 观察登录的时候调用接口,接口中的请求头是否有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);
    }
  }
});

第三步、触发退出登录事件

  1. 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>

效果:

相关推荐
m0_7381207215 分钟前
应急响应——知攻善防Web-3靶机详细教程
服务器·前端·网络·安全·web安全·php
程序员爱钓鱼7 小时前
Node.js 编程实战:文件读写操作
前端·后端·node.js
PineappleCoder7 小时前
工程化必备!SVG 雪碧图的最佳实践:ID 引用 + 缓存友好,无需手动算坐标
前端·性能优化
JIngJaneIL8 小时前
基于springboot + vue古城景区管理系统(源码+数据库+文档)
java·开发语言·前端·数据库·vue.js·spring boot·后端
敲敲了个代码8 小时前
隐式类型转换:哈基米 == 猫 ? true :false
开发语言·前端·javascript·学习·面试·web
澄江静如练_8 小时前
列表渲染(v-for)
前端·javascript·vue.js
JustHappy9 小时前
「chrome extensions🛠️」我写了一个超级简单的浏览器插件Vue开发模板
前端·javascript·github
Loo国昌9 小时前
Vue 3 前端工程化:架构、核心原理与生产实践
前端·vue.js·架构
sg_knight9 小时前
拥抱未来:ECMAScript Modules (ESM) 深度解析
开发语言·前端·javascript·vue·ecmascript·web·esm