說明
該文章是屬於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,過期時間、使用者資訊,朋友們可以自行決定。
路由守衛調整
上一篇文章我們講過,路由守衛的作用,這裡就不再贅述。
調整如下
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
關注公眾號:傳送【許可權】,獲取前後端程式碼
有興趣的朋友,請關注我微信公眾號吧(*^▽^*)。
關注我:一個全棧多端的寶藏博主,定時分享技術文章,不定時分享開源專案。關注我,帶你認識不一樣的程式世界