说明
该文章是属于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和过期时间。
前端结构调整
移动一下文件(不是跟着系列文章的,请忽略,主要是前端结构调整)
把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,过期时间、用户信息,朋友们可以自行决定。
路由守卫调整
上一篇文章我们讲过,路由守卫的作用,这里就不再赘述。
调整如下
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
关注公众号:发送【权限】,获取前后端代码
有兴趣的朋友,请关注我微信公众号吧(*^▽^*)。
关注我:一个全栈多端的宝藏博主,定时分享技术文章,不定时分享开源项目。关注我,带你认识不一样的程序世界