1. 簡介
登錄做為一個系統的門面,也是阻擋外界的一道防線,那在vuestic-ui中如何做登錄功能呢。在這里就之間沿用初始版本的Login頁面,作為一個演示模板,后續需要改進的讀者可以在此篇文章的基礎上修改。
2. 登錄接口相關api 與 type編寫
在上一篇獲取動態數據中 我們已經定義好了與ts整合的axios,實現發送異步請求與遠程服務器交互(對于ts語法像是函數定義、基本數據類型還是有不懂的讀者可以跳轉到上一篇的2.1再學習學習)這里就直接引入登錄接口的api編寫, 具體位置如下我個人習慣創建一個api文件夾,里面專門存放一些與后端交互的api方法和類型定義(初始quickstart版本是寫在上面的page中的,就看個人的編寫習慣吧😁只要功能實現了就沒問題)
對于index.ts
中主要實現了三個基本的方法,登錄登出和獲取用戶信息
這里沒有實現注冊功能,因為我實現該網站主要是做一個流量監控的系統,注冊功能對于用戶不多的情況下其實不太需要,管理員可以直接操作加入數據庫中,要是對這快感興趣的讀者也可以自己嘗試嘗試注冊模塊的功能實現
本質就是add一個user到數據庫中,不過需要注意的是添加驗證碼等防護措施,防止有不法分子大量注冊短時間內打爆服務器!!
import { http } from '../../../utils/request'
import type { LoginData , UserInfoRes} from './types'const requestContent = '/simple/cloud/access'
/*** 登錄*/
export function login(loginVo: LoginData) {return http.post<UserInfoRes>(`${requestContent}/login`, loginVo);
}/*** 獲取登錄用戶信息*/
export function getUserInfo() {return http.post<UserInfoRes>(`${requestContent}/info`)
} /*** 退出登錄*/
export function logout() {return http.post<string>(`${requestContent}/logout`)
}
假設訪問的后端服務器使用URL - http:localhost:9001/simple/cloud/access/login 這里由于uri前綴是一樣的都是 ‘/simple/cloud/access’ 所以把它提取出來做為一個常量簡化編寫。只需要在使用的地方通過變量占位符引入就好啦(注意不是 ‘’ ,剛開始也踩過這個坑在vscode中看到上面的requestContent 由灰色變成高亮則說明引用成功)
`${key}`
而對于api中引入的數據類型定義在types.ts
中,其中的返回值類型就根據后端提供的接口方法來寫,像是我后端返回的類型為一個Map<String,Object> 類型的對象如下圖所示,那我就根據這個map中的key和value一一對應寫出如下的接口UserInfoRes
,然后使用export導出給外部使用
注意編寫的過程中只用指定ts基本的類型(java的List對應的就是ts中的數組 - 使用 [ ] 進行初始化 ),而要是需要返回一個User類型的對象,那就需要重新定義一個UserInfoInterface ,或者在user對應的api處定義types.ts 再在該文件下在引入(我是更推薦這種做法👍)
/* 登錄接口參數類型 */
export interface LoginData {email: string,password: string,
}/* 用戶信息接口返回值類型 */
export interface UserInfoRes {routers: [],buttons: [],roles: [],name: string,token: string,
}
3. 修改Login.vue
定義好與后端交互的方法api后,我們就可以回到前面的Login.vue處修改具體登錄邏輯啦,由于初始版本使用的全是靜態數據,所以很多功能其實都是不用的,具體刪除修改后的模板如下(只保留了一個忘記密碼的選項,該功能后續再完善😭先把主要的邏輯跑通先,感興趣的讀者可以先占個坑,后續我一定會回來填坑的!)
<template><VaForm ref="form" @submit.prevent="submit"><h1 class="font-semibold text-4xl mb-4">Log in</h1><VaInputv-model="formData.email":rules="[validators.required, validators.email]"class="mb-4"label="Email"type="email"/><VaValue v-slot="isPasswordVisible" :default-value="false"><VaInputv-model="formData.password":rules="[validators.required]":type="isPasswordVisible.value ? 'text' : 'password'"class="mb-4"label="Password"@clickAppendInner.stop="isPasswordVisible.value = !isPasswordVisible.value"><template #appendInner><VaIcon:name="isPasswordVisible.value ? 'mso-visibility_off' : 'mso-visibility'"class="cursor-pointer"color="secondary"/></template></VaInput></VaValue><div class="auth-layout__options flex flex-col sm:flex-row items-start sm:items-center justify-between"><RouterLink :to="{ name: 'recover-password' }" class="mt-2 sm:mt-0 sm:ml-1 font-semibold text-primary">Forgot password?</RouterLink></div><div class="flex justify-center mt-4"><VaButton class="w-full" @click="submit"> Login</VaButton></div></VaForm>
</template>
重寫綁定的submit
點擊事件邏輯
const submit = () => {if (validate()) {login(formData).then((data: UserInfoRes) => {if (data) {// 在這里添加需要執行的操作const token = data.token;// 將token存儲到authStore中const authStore = useAuthStore()authStore.setToken(token)authStore.setIsAuthenticated(true)window.sessionStorage.setItem('isAuthenticated', 'true')authStore.setName(data.name)authStore.setButtons(data.buttons)authStore.setRoles(data.roles)authStore.setRouters(data.routers)init({ message: "logged in success", color: 'success' });// 登陸成功后就重定向到主頁面dashboardpush({ name: 'dashboard' })}}).catch(() => {init({ message: "logged in fail , please check carefully!", color: '#FF0000' });});}else{Message.error('error submit!!')return false}
}
看到這里我相信你肯定會疑惑,為什么我需要獲取到數據又存儲到store中,那這個store又在哪里定義的呢,作者也沒講啊😡
別急別急,請聽我細細道來
4. store實現
在Vue應用程序中,當需要管理共享狀態時,通常會使用Vuex庫,而store就是Vuex中用于存儲這些狀態的地方,而我們登錄后自然需要圍護當前登錄角色的一些關鍵信息(權限,姓名等等)需要的時候就直接到store中拿去,而不是反復的去數據庫中查找,廢話不多說下面就來定義一個store ,在初始版本中就已經定義好了store,只不過這個store里面是沒東西的,如下圖所示
那我們就可以在原有的基礎上添加修改,下面的代碼都是在index.ts中實現的,如下代碼就是一個模板,對應pinia庫的描述如下
Pinia是Vue的另一種狀態管理方案,與Vuex類似,但設計上更簡潔、更易于上手。以下是關于Pinia的一些詳細說明:
- 簡單易用:Pinia的目標是提供一個更簡單的狀態管理解決方案,它的API設計非常直觀,使得開發者可以快速上手并有效地管理狀態。
- 獨立模塊:與Vuex不同,Pinia中的每個store都是一個獨立的模塊,它們可以單獨導入和導出,這有助于更好地組織和維護代碼。
- 響應式:Pinia中的狀態是響應式的,當狀態發生變化時,依賴于這些狀態的組件會自動更新。
- Devtools支持:Pinia具有良好的Devtools支持,可以幫助開發者更方便地跟蹤和調試狀態變化。
- 插件化:Pinia被設計為一個插件,可以輕松地集成到現有的Vue應用中。
- 與Vuex兼容:雖然Pinia是一個全新的狀態管理庫,但它也允許與Vuex共存于同一個項目中,方便開發者逐步遷移。
本次項目中store就基于Pinia實現,首先通過defineStore方法定義一個全局可供調用的store, 其中包括了一些屬性像是
- id (自己設定,但是要保證全局唯一)
- state (定義的所有狀態)
- getters (獲取狀態的方法)
- actions (有獲取肯定就有設置的方法啦)
// store.ts
import { createPinia, defineStore } from 'pinia'export const useAuthStore = defineStore({id: 'auth',state: () => ({}),getters: {},actions: {},
})export default createPinia()
4.1 state
在state中定義的狀態就是在一個瀏覽器會話內需要存儲的用戶信息(登錄后賦值,登出或者會話結束就銷毀)根據第2點中types定義的UserInfoRes 可以設計出來, 由于ts不像js一樣是弱語言,ts是有類型的上一講也提到過,所以為了能在后續的get set中拿到指定和設置其中的屬性值,我們需要通過as 參數類型
的方式來指定
isAuthenticated 本意是為了阻止用戶登錄前就訪問其他的頁面(會被駁回,重定向到登錄頁面)后面發現存到瀏覽器緩存中也是可以的,這里就做個備選,看讀者喜歡哪一種方式
state: () => ({token : '',isAuthenticated : false,routers : [] as RouterVo[],buttons : [] as string[],name : '',roles : [] as RoleData[],}),
這里的RoleDta和RouterVo就分別對應了角色和菜單列表,具體實現如下(編寫在types.ts文件中,具體位置看下邊4.4的總體代碼)
/* sysUser參數類型 */
export interface RoleData {id: number,roleName: string,roleCode: string,description: string
}/* RouterVo參數類型 */
export interface RouterVo {path: string,hidden: boolean,alwaysShow: boolean,meta: MetaVo,children: RouterVo[],
}
4.2 getters
根據如下的指定格式獲取存在store中的參數
getters: {getButtons: (state) => state.buttons,getToken: (state) => state.token,getIsAuthenticated: (state) => state.isAuthenticated,getRouters: (state) => state.routers,getName: (state) => state.name,getRoles: (state) => state.roles,
},
4.3 actions
actions中定義了一系列set方法,可以發現這里()內的參數都是指定類型的,如果我們在定義的時候不指定類型這就會報錯!!
actions: {setRoles(roles : RoleData[]) {this.roles = roles},setButtons(buttons : string[]) {this.buttons = buttons},setRouters(routers : RouterVo[]) {this.routers = routers},setName(name : string) {this.name = name},setToken(token : string) {this.token = token},setIsAuthenticated(isAuthenticated : boolean){this.isAuthenticated = isAuthenticated},// 登出后的資源重置reset(){this.roles = []this.name = ''this.buttons = []this.routers = []this.isAuthenticated = falsethis.token = ''},
},
4.4 總體代碼
// store.ts
import { createPinia, defineStore } from 'pinia'
import { RoleData } from '@/api/system/sysRole/types'
import { RouterVo } from '@/api/system/sysMenu/types'export const useAuthStore = defineStore({id: 'auth',state: () => ({token : '',isAuthenticated : false,routers : [] as RouterVo[],buttons : [] as string[],name : '',roles : [] as RoleData[],}),getters: {getButtons: (state) => state.buttons,getToken: (state) => state.token,getIsAuthenticated: (state) => state.isAuthenticated,getRouters: (state) => state.routers,getName: (state) => state.name,getRoles: (state) => state.roles,},actions: {setRoles(roles : RoleData[]) {this.roles = roles},setButtons(buttons : string[]) {this.buttons = buttons},setRouters(routers : RouterVo[]) {this.routers = routers},setName(name : string) {this.name = name},setToken(token : string) {this.token = token},setIsAuthenticated(isAuthenticated : boolean){this.isAuthenticated = isAuthenticated},reset(){this.roles = []this.name = ''this.buttons = []this.routers = []this.isAuthenticated = falsethis.token = ''},},
})
// 記得要導出,不在就白定義了 外部通過調用createPinia() 獲取示例
export default createPinia()
4.5 main.ts中App引入
在Vue中引入App是因為App.vue通常作為項目的主組件和頁面入口文件,負責構建定義及頁面組件的歸集和切換。定義的組件自然要添加到其中,在初始化的時候就一同創建。在文件原有基礎上添加如下代碼
import stores from './stores'
import { createPinia } from 'pinia'app.use(createPinia)
app.use(stores)
最后保存就好啦,到這里在回看第3點的submit
方法是不是就一目了然
這里提煉出使用store的核心代碼,有需要的讀者可以直接復制使用😁
// 導入剛剛定義的方法
import { useAuthStore } from '@/stores'// 外部調用創建一個示例(唯一的)
const authStore = useAuthStore()
// 在對應的操作方法里面使用我們在getters和actions中定義的方法
// set
authStore.setToken(token)
// get
const token = authStore.getToken
5. vue限制實現不登錄無法進入其他頁面
這個模塊可用的方法有很多網上也是有各種各樣的教程,在這里使用的是設置路由守衛的方法,在router/index.ts下修改,具體做三種判斷
- 防止重復登錄: 登錄后的用戶不能在登錄了,只能主動退出或者關閉瀏覽器(token失效也是一個,這個后面講)
- 白名單直接放行:對于可以供給全部用戶訪問的一些靜態資源、頁面(比如登錄頁面,和一些docs幫助文檔是可以直接訪問的)
- 沒有登錄:對于沒有登錄的用戶無法訪問系統的資源,為了提防有些通過導航欄修改URL的方法訪問
// 設置哪些頁面是屬于白名單的
const witheList = ["/auth/login"];function isWitheRoute(path : string) {return witheList.includes(path);
}// 全局前置守衛
router.beforeEach((to, from, next) => {const isAuthenticated = window.sessionStorage.getItem('isAuthenticated');//防止重復登錄if (isAuthenticated && (to.path === "/auth/login")) {Message.info("You have successfully logged in. Please avoid logging in repeatedly! (You can log out if you wish)");return next({ path: from.path ? from.path : "/" });}// 判斷如果是白名單就直接放行if (isWitheRoute(to.path)) {next();return;}// 沒有登錄,強制跳轉到登錄頁面if (!isAuthenticated && to.path != "/auth/login") {Message.info("Please logging first");next({ path: "/auth/login" });return;} next()
});
5.1. 瀏覽器緩存
上邊埋了一個坑,可以使用瀏覽器緩存的方法實現該功能,上邊代碼也看到了window.sessionStorage. 那么這到底是嘛玩意,作用范圍生命周期又是什么呢?下面將一一解答:
- sessionStorage為Web開發者提供了一種在用戶的瀏覽器中臨時存儲數據的方式。這種存儲方式特定于用戶打開的特定窗口或標簽頁,并且數據只在這個特定的窗口或標簽頁有效。當用戶關閉這個窗口或標簽頁時,存儲在sessionStorage中的所有數據將被清除。這就意味著不同的瀏覽器窗口或標簽頁,即使是打開相同的網頁,它們之間的sessionStorage數據是不共享的。
- sessionStorage的生命周期與用戶打開的窗口或標簽頁的持續時間同步。只要窗口或標簽頁保持打開狀態,即便是進行頁面刷新或切換到同源的其他頁面,sessionStorage中的數據都將持續存在。然而,一旦窗口或標簽頁被關閉,sessionStorage中的所有數據將立即失效并被清除。
可以見得通過該方法保存用戶的登錄狀態也是不錯之選,而且非正常退出時候也不用擔心數據泄露(會自動銷毀,后端的數據就需要通過勾子函數回調,或者直接設置redis過期時間就等它自動過期)下邊就是三個常用的方法:
對于我們的登錄功能來說,在登錄成功后設置為true,此時路由守衛判斷時候就能獲取到該值,而在登出的時候就刪除掉該數據,這樣就能保證統一
//設置對應的key-value
window.sessionStorage.setItem('isAuthenticated', 'true');//通過getItem獲取 (取不到時為null)
const isAuthenticated = window.sessionStorage.getItem('isAuthenticated');//去除瀏覽器緩存
window.sessionStorage.removeItem('isAuthenticated')
6. 登出功能實現
登出功能本質上是跟登錄沒什么區別的,就是后端清除存儲的數據token , reids中權限數據等,前端清除login獲取到的所有數據(回到出廠設置的感覺)在初始版本中是沒有登出這個按鈕的,經常登錄網頁的朋友都知道,登出的按鈕一般是在右上角,那這里我們就遵循慣例先找找最上邊的欄目是在哪一個vue頁面里面(最笨的方法就是一個一個去搜索是否有相應的字眼)
那么我就以我的理解來告訴大家如何快速找到相應的模塊。首先要知道的是所有的組件都是放在src/components
文件夾下的,那我們就去下邊找,一展開就很明顯看到navbar
的字眼(導航欄嘛,也就是我們要找的上邊欄所在位置)點開后發現又有個components
(根據上面的知識不用我說都知道這是放組件的吧)點開就看到GitHubButton
這不就是我們要找的上邊欄上的github按鈕嗎,說明我們找對地方了,最終就鎖定范圍在這兩個vue文件中,是不是一下子節省很多工作量😁 , 具體示例文件所在處如下圖所示
找到這個文件后我們預期的效果是跟下圖這樣加一個Logout 按鈕 用戶點擊就可以退出登錄
在AppNavbarActions這個文件中點開就發現其實實現起來很簡單,就是依葫蘆畫瓢,照抄原來有的button組件就好啦,具體代碼如下
<VaButtonv-if="!isMobile"preset="secondary"@click="logoutOper" <!--自定義點擊事件-->target="_blank"color="textPrimary"class="app-navbar-actions__item flex-shrink-0 mx-0"
>{{ t('Logout') }}
</VaButton>
因為我們綁定了點擊事件,自然要實現的啦(如下代碼在script中原有的基礎上添加)
import { logout } from '@/api/system/auth/index'
import { useAuthStore } from '@/stores'
import { useToast } from 'vuestic-ui'
import { useRouter } from 'vue-router'
const { push } = useRouter()
const { init } = useToast()const logoutOper = () => {logout().then(() => {const store = useAuthStore() // 獲取store實例store.reset() // 重置store//去除瀏覽器緩存window.sessionStorage.removeItem('isAuthenticated')//跳轉路由init({ message: "logout success", color: 'success' });push({ name: 'login' })}).catch(() => {init({ message: "logged out fail , please contact administration", color: '#FF0000' });});
}
7. 每次請求時帶上token訪問服務器
由于加入了權限認證功能,所以登錄后的每一次請求都必須攜帶上token(這里的token就遵循OAuth2的規范以"Bearer "
開頭),不然會認為沒有登錄跳轉的登錄頁面重新登錄,在每一次請求中添加請求頭是不是就是定義一個全局filter,也就是在上一講中提到的axios請求攔截器,那如下代碼就在utils/request.ts
下修改(還沒有的請看上一講)
import { useAuthStore } from '@/stores'/* 請求攔截器 */
service.interceptors.request.use((config: InternalAxiosRequestConfig) => { const authStore = useAuthStore()if (authStore != undefined) {//獲取tokenconst token = authStore.getTokenconfig.headers.Authorization = `Bearer ${token}`;} else {// 如果不存在 token,則拒絕請求并跳轉到登錄頁面window.location.href = '/auth/login';//去除瀏覽器緩存window.sessionStorage.removeItem('isAuthenticated')return Promise.reject('Authenticated fail');}return config; }, (error: AxiosError) => {Message.error(error.message);return Promise.reject(error)
})
終于講完啦,這篇內容挺多的,給看到這里的讀者點贊👍,希望能夠對你們有所幫助(本篇主要實現前端的功能,后續會結合權限管理給出后端認證授權功能實現,敬請期待…)
各位讀者我回來填坑啦,對于上面的后端實現我又寫了點自己的想法,感興趣的讀者可以點擊查閱后端認證授權功能實現