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

说明

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

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

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

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

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

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

关注我,学不会你来打我

前言

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

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

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

流程图

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

实现功能

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文件,修改如下

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

演示地址

登录演示

结语

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

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

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

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

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

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

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

相关推荐
西哥写代码14 小时前
基于cornerstone3D的dicom影像浏览器 第二十五章 自定义VR调窗工具
javascript·3d·vue3·vr·cornerstonejs
放逐者-保持本心,方可放逐1 天前
浅谈 JavaScript 性能优化
开发语言·javascript·性能优化·vue3·v-memo·vue3性能优化·v-once
西哥写代码3 天前
基于cornerstone3D的dicom影像浏览器 第二十四章 显示方位、坐标系、vr轮廓线
javascript·3d·vue3·vr·dicom·cornerstonejs
_xaboy4 天前
开源 FcDesigner 表单设计器组件事件详解
前端·vue.js·低代码·开源·表单
_xaboy4 天前
开源Vue表单设计器 FcDesigner 组件提供的方法详解
前端·vue.js·低代码·开源·表单
西哥写代码4 天前
基于cornerstone3D的dicom影像浏览器 第二十三章 mpr预设窗值与vr preset
javascript·3d·vue3·dicom·cornerstonejs
linweidong5 天前
汇量科技前端面试题及参考答案
webpack·vue3·react·前端面试·hooks·懒加载·flex布局
EndingCoder5 天前
从零基础到最佳实践:Vue.js 系列(9/10):《单元测试与端到端测试》
前端·javascript·vue.js·性能优化·单元测试·vue3
egoist20236 天前
【Linux仓库】权限的量子纠缠:用户/组/other如何编织Linux访问控制网?
linux·运维·服务器·编辑器·权限·文件权限
blues_C10 天前
二、【环境搭建篇】:Django 和 Vue3 开发环境准备
后端·python·django·vue3·测试平台