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