(系列十二)Vue3+.Net8实现用户登录(超详细登录文档)

说明

    该文章是属于OverallAuth2.0系列文章,每周更新一篇该系列文章(从0到1完成系统开发)。

    该系统文章,我会尽量说的非常详细,做到不管新手、老手都能看懂。

    说明:OverallAuth2.0 是一个简单、易懂、功能强大的权限+可视化流程管理系统。

友情提醒:本篇文章是属于系列文章,看该文章前,建议先看之前文章,可以更好理解项目结构。

qq群:801913255,进群有什么不懂的尽管问,群主都会耐心解答。

有兴趣的朋友,请关注我吧(*^▽^*)。

(系列十二)Vue3+.Net8实现用户登录(超详细登录文档)

关注我,学不会你来打我

前言

  随着前后端框架(轮子)的逐渐搭建完成,我们的OverallAuth2.0项目也正式迈入功能开发阶段。

  今天我们的目标是做一个带有认证的用户登录功能。

  看该文章前,说明一点。最好结合我之前的系列文章观看,因为会使用到之前系列文章中的代码。当然有一定基础的码友可自动忽略。

流程图

(系列十二)Vue3+.Net8实现用户登录(超详细登录文档)

从流程图上可以看出,本次登录非常简单,它没有过多的业务逻辑,就是一个简单的用户登录验证,成功之后,就能进入系统。至于说登录之后的业务逻辑处理,本篇文章不会涉及(交由之后的系列文章)。

实现功能

  1、用户登录

  2、登录失效处理

  3、异常信息提示

编写后端接口

  这里需要编写用户登录接口,并且返回用户数据。

       建立一个用户登录后的返回模型,由于在之前已经创建(LoginOutPut),我们只需在该模型中添加一个UserId即可,代码如下

/// <summary> /// 登录输出模型 /// </summary> public class LoginOutPut {     /// <summary>     /// 用户ID     /// </summary>     public int UserId { get; set; }      /// <summary>     /// 用户名     /// </summary>     public string? UserName { get; set; }      /// <summary>     /// 密码     /// </summary>     public string? Password { get; set; }      /// <summary>     /// Token     /// </summary>     public string? Token { get; set; }      /// <summary>     /// Token过期时间     /// </summary>     public string? ExpiresDate { get; set; }  }

ISysUserRepository仓储接口中,添加一个根据用户名和密码查找数据的接口

/// <summary> /// 根据用户名称和密码获取用户信息 /// </summary> /// <param name="userName">用户名称</param> /// <param name="password">用户密码</param> /// <returns></returns> public SysUser? GetUserMsg(string userName, string password);

SysUserRepository仓储中,实现接口

        /// <summary>         /// 根据用户名称和密码获取用户信息         /// </summary>         /// <param name="userName">用户名称</param>         /// <param name="password">用户密码</param>         /// <returns></returns>         public SysUser? GetUserMsg(string userName, string password)         {             string sql = " select * from Sys_User where UserName =@UserName and Password=@Password";             using var connection = DataBaseConnectConfig.GetSqlConnection();             return connection.QueryFirstOrDefault<SysUser>(sql, new { UserName = userName, Password = password });         }

同理在SysUserService、ISysUserService层中,添加相同接口

ISysUserService

/// <summary> /// 根据用户名称和密码获取用户信息 /// </summary> /// <param name="userName">用户名称</param> /// <param name="password">用户密码</param> /// <returns></returns> ReceiveStatus<LoginOutPut> GetUserMsg(string userName, string password);

SysUserService

 /// <summary>  /// 根据用户名称和密码获取用户信息  /// </summary>  /// <param name="userName">用户名称</param>  /// <param name="password">用户密码</param>  /// <returns></returns>  public ReceiveStatus<LoginOutPut> GetUserMsg(string userName, string password)  {      ReceiveStatus<LoginOutPut> receiveStatus = new ReceiveStatus<LoginOutPut>();      List<LoginOutPut> loginResultsList = new List<LoginOutPut>();      if (string.IsNullOrEmpty(userName))          return ExceptionHelper<LoginOutPut>.CustomExceptionData("用户名不能为空!");      if (string.IsNullOrEmpty(password))          return ExceptionHelper<LoginOutPut>.CustomExceptionData("密码不能为空!");      var result = _sysUserRepository.GetUserMsg(userName, password);      if (result == null)          return ExceptionHelper<LoginOutPut>.CustomExceptionData(string.Format("用户【{0}】不存在,或账号密码输入错误", userName));      if (result.IsOpen == false)          return ExceptionHelper<LoginOutPut>.CustomExceptionData(string.Format("用户【{0}】已停用,请开启后再登录", userName));           LoginOutPut loginResults = new LoginOutPut()      {          UserId = result.UserId,          UserName = result.UserName,          Token = string.Empty,          ExpiresDate = string.Empty      };      loginResultsList.Add(loginResults);      receiveStatus.data = loginResultsList;      receiveStatus.msg = "登录成功";      return receiveStatus;  }

上述接口中,我们要验证用户名、密码是否为空,是否正确,用户账号是否启用等。如果验证不通过。我们使用ExceptionHelper异常帮助类,把异常信息,反馈给前端。

如果验证通过,我们需要返回用户名、用户id、token、过期时间给前端。

在SysUserController控制器中,添加如下接口

 /// <summary>  /// 登录  /// </summary>  /// <returns></returns>  [HttpPost]  [AllowAnonymous] // 不验证权限  public ReceiveStatus<LoginOutPut> Login(LoginInput loginModel)  {      var result = _userService.GetUserMsg(loginModel.UserName ?? string.Empty, loginModel.Password ?? string.Empty);      if (result.success)      {          var loginResult = result.data.First();          var tokenResult = JwtPlugIn.BuildToken(loginModel);          loginResult.Token = tokenResult.Token;          loginResult.ExpiresDate = tokenResult.ExpiresDate;          result.data = new List<LoginOutPut>() { loginResult };      }      return result;  }

因为是用户登录接口,所以不需要jwt验证

在用户登录成功后,我们根据用户名、用户密码、jwt配置信息生成token和过期时间。

ps:jwt配置信息请查看之前系列文章

前端结构调整

移动一下文件(不是跟着系列文章的,请忽略,主要是前端结构调整)

把components文件夹下的图片,移动到同src文件夹同级的resources的picture目录下(记住调整图片引用路径)。

把components文件夹下的HelloWorld.vue文件内容,拷贝到views下的framework文件夹中(没有就新建),然后删除components文件夹。

搭建登录界面

在scr文件夹下创建model文件夹,并在下面创建user文件夹,然后再user文件夹下创建一个LoginInput.ts的文件,用于存放字段(model文件夹以后作为存放模型的文件夹)

export interface LoginInput {     //用户名称     UserName: string;     //用户密码     Password: string; }

接着往下。

在api文件夹下创建user文件夹,并添加index.ts文件内容如下(api文件夹以后作为存放调用后端接口的文件夹)

import { LoginInput } from '@/model/user/LoginInput'; import Http from '../http';  export const login = function(loginForm: LoginInput) {     return Http.post('/api/SysUser/Login', loginForm) }

该代码主要是调用后端写的登录接口

接着往下。

在views文件夹目录下,创建login文件夹,并添加index.vue文件。内容如下

<template>   <div class="backgroundStyle">     <div class="loginStyle">       <div style="color: rgb(76 104 139)">         <div class="systemTitle">           OverallAuth2.0 权限管理系统         </div>         <div class="systemSubTitle">           简单、易懂、功能强大,欢迎访问使用。         </div>       </div>       <div style="height: calc(100% - 260px)">         <div class="fieldStyle">           <div style="width: 100%; text-align: left; margin-left: 10%">             <el-tag>密码登录</el-tag>           </div>         </div>         <div class="fieldStyle">           <div style="width: 100%">             <el-input               v-model="loginForm.UserName"               style="width: 80%; height: 40px"               placeholder="请输入用户名"               :prefix-icon="User"             />           </div>         </div>         <div class="fieldStyle">           <div style="width: 100%">             <el-input               v-model="loginForm.Password"               style="width: 80%; height: 40px"               placeholder="请输入密码"               type="password"               show-password               :prefix-icon="Hide"             />           </div>         </div>         <div class="fieldStyle">           <div style="width: 100%">             <el-input               v-model="code"               style="width: 80%; height: 40px"               placeholder="请输入验证码"               :prefix-icon="Position"             />           </div>         </div>         <div class="fieldStyle">           <div style="width: 100%">             <el-button               @click="loginClick"               type="primary"               style="width: 80%; height: 50px"               >登录</el-button             >           </div>         </div>       </div>       <div style="height: 60px; text-align: left; margin-left: 10px">         <el-checkbox v-model="isStarted" label="码云是否Star" size="large" />         <div style="color: red; font-size: 12px">           *为了帮助更多的人知道及了解本项目,请帮忙Star。拜谢各位🙏🙏🙏         </div>       </div>       <div class="loginBottomStyle">         <el-divider content-position="left"           ><el-icon color="red"><star-filled /></el-icon>特色功能</el-divider         >         <div class="featuresFunction">           <el-tag>可视化权限设计</el-tag>           <el-tag type="success">数据行权限</el-tag>           <el-tag type="warning">数据列权限</el-tag>           <el-tag type="danger">完整流程审批</el-tag>         </div>       </div>     </div>   </div> </template>  <script lang="ts"> import { defineComponent, onMounted, reactive, ref } from "vue"; import { TestAutofac } from "../../api/module/user"; import { User, Hide, Position, StarFilled } from "@element-plus/icons-vue"; import { ElMessage } from "element-plus"; import { useRouter } from "vue-router"; import { login } from "@/api/user"; import { LoginInput } from "@/model/user/LoginInput"; import { useUserStore } from "../../store/user"; import { storeToRefs } from "pinia"; export default defineComponent({   setup() {     //初始加载     onMounted(() => {       //TestAutofacMsg();     });     const userStore = useUserStore();          const router1 = useRouter();     const userName = ref("");     const password = ref("");     const code = ref("");     const isStarted = ref<boolean>(false);     //调用接口     const TestAutofacMsg = async () => {       var result = await TestAutofac();       console.log(result);     };      const loginForm = reactive<LoginInput>({       UserName: "张三",       Password: "1",     });     const loginClick = function () {       login(loginForm).then(({ data, code, msg }) => {         setTimeout(() => {           if (code == 200) {             userStore.token = data[0].token.toString();             userStore.expiresDate = data[0].expiresDate;             userStore.userInfo = {               userName: data[0].userName,               userId: data[0].userId,             };              ElMessage({               message: "登录成功",               type: "success",             });             router1.push({ path: "/framework" });           }         }, 1000);       });     };      return {       User,       Hide,       Position,       StarFilled,       userName,       password,       code,       isStarted,       loginClick,       loginForm,     };   },   components: {}, }); </script>  <style scoped> .backgroundStyle {   background-image: url(../../../resources/picture/login.png);   height: calc(100vh);   width: 100%;   background-size: 100% 100%;   display: flex; }  .loginStyle {   width: 23%;   height: 55%;   margin-top: 12%;   margin-left: 10%;   border: 2px solid white;   background-color: white;   border-radius: 10px;   box-shadow: 0px 0px 19px 0px rgba(132, 203, 255, 2.5); } .systemTitle {   height: 70px;   font-size: 30px;   justify-content: center;   align-items: center;   display: flex; } .systemSubTitle {   display: flex;   height: 30px;   font-size: 14px;   justify-content: center;   border-bottom: 1px solid #e1dede; }  .loginBottomStyle {   height: 100px;   /* font-size: 30px; */   justify-content: center;   align-items: center; }  .fieldStyle {   display: flex;   margin-top: 10px; }  .featuresFunction {   display: flex; } .featuresFunction > * {   margin-left: 10px; } </style>

修改base-routes.ts文件,添加一下2个菜单。

  {     path: '/framework',     component: Framework,     name: "架构",    },   {     path: '/login',     component: Login,     name: "登录页面",   },

调整app.vue

把template中的内容替换成<router-view></router-view>即可,这个调整的原因是完全根据路由来访问界面,配合路由守卫,做到未登录时就进入登录界面的效果。

状态库持久化

这个是本篇文章的重点,它的作用是可以持久化记录登录人员登录信息。为以后验证token过期、获取登录信息做准备。

安装npm install pinia-plugin-persist插件。

并在main.ts中添加引用

import  persist  from 'pinia-plugin-persist' pinia.use(persist)

这里需要注意的是:pinia.use(persist)一定要在app.use(pinia)前面。

在scr下建立store文件夹,并添加三个文件app.ts、index.ts、user.ts,内容如下

app.ts

import { defineStore } from 'pinia'  export const useAppStore = defineStore({   id: 'app',   state: () => {     return {       tab: true,       logo: true,       level: true,       inverted: false,       routerAlive: true,       collapse: false,       subfield: false,       locale: "zh_CN",       subfieldPosition: "side",       theme: 'light',       breadcrumb: true,       sideWidth: "220px",       sideTheme: 'dark',       greyMode: false,       accordion: true,       tagsTheme: 'concise',       keepAliveList: [],       themeVariable: {         "--global-checked-color": "#5fb878",         "--global-primary-color": "#009688",         "--global-normal-color": "#1e9fff",         "--global-danger-color": "#ff5722",         "--global-warm-color": "#ffb800",       },     }   },   persist: {     enabled: true,     strategies: [       {         // 可以是localStorage或sessionStorage         storage: localStorage,         // 指定需要持久化的属性         paths: ['token', 'expiresDate', 'userInfo']       }     ]   }, })

index.ts

import { createPinia } from 'pinia' import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'  const store = createPinia(); store.use(piniaPluginPersistedstate);  export default store;

user.ts

import { defineStore } from 'pinia' export const useUserStore = defineStore(   'user', {   state: () => ({     token: '',     expiresDate: '',     userInfo: {},   }),    actions: {},   //persist:true   persist: {     enabled: true,     strategies: [       {         // 可以是localStorage或sessionStorage         storage: localStorage,         // 指定需要持久化的属性         paths: ['token','expiresDate','userInfo']       }     ]   }, })

这里说明一下,在paths属性中,你可以选择持久化存储数据的字段。我这里选择存储了token,过期时间、用户信息,朋友们可以自行决定。

路由守卫调整

上一篇文章我们讲过,路由守卫的作用,这里就不再赘述。

调整如下

router.beforeEach方法内容变更成一下代码
  NProgress.start();   const userStore = useUserStore();   const endTime = new Date(userStore.expiresDate);   const currentTime = new Date();   to.path = to.path;   if (to.meta.requireAuth && endTime < currentTime) {     router.push('/login')   }   if (to.meta.requireAuth) {     next();   } else if (to.matched.length == 0) {     next({ path: '/login' })   } else {     next();   }

从上述代码可以看出我们同过useUserStore获取了用户登录信息。然后拿到过期时间判断登录是否过期。过期就需要重新登录。

提示信息调整

找到在api文件夹下的http.ts文件,修改如下

(系列十二)Vue3+.Net8实现用户登录(超详细登录文档)

结合后端返回code,做出准确提示。

演示地址

登录演示

结语

我们的OverallAuth2.0项目也正式迈入功能开发阶段,可能文章内容逐渐开始复杂化,如果你感兴趣的话,也有跟着博主从0到1搭建权限管理系统的兴趣。

那么请加qq群:801913255,进群有什么不懂的尽管问,群主都会耐心解答。

后端WebApi 预览地址:http://139.155.137.144:8880/swagger/index.html

前端vue 预览地址:http://139.155.137.144:8881

关注公众号:发送【权限】,获取前后端代码

有兴趣的朋友,请关注我微信公众号吧(*^▽^*)。

(系列十二)Vue3+.Net8实现用户登录(超详细登录文档)

关注我:一个全栈多端的宝藏博主,定时分享技术文章,不定时分享开源项目。关注我,带你认识不一样的程序世界

发表评论

相关文章