(系列十二)Vue3+.Net8實現使用者登入(超詳細登入文件)

陈逸子风發表於2024-11-25

說明

該文章是屬於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

關注公眾號:傳送【許可權】,獲取前後端程式碼

有興趣的朋友,請關注我微信公眾號吧(*^▽^*)。

關注我:一個全棧多端的寶藏博主,定時分享技術文章,不定時分享開源專案。關注我,帶你認識不一樣的程式世界

相關文章