說明
涉及到以下知識點:
- 登陸的具體流程
- express、vue2.x、elementUI、axios、jwt、assert 登陸方面的API使用
- 中間件的使用
- 前后端通過http狀態碼,進行響應的操作(這里主要是401)
- 密碼驗證(bcrypt的hashSync方法對明文密碼進行加密,compareSync方法對加密的密碼進行驗證)
- token的使用(后端jwt.sign生成token, 前端使用瀏覽器緩存存儲token,后端驗證token)
登錄
界面
使用Vue2.x + elementUI實現的登錄界面
<template><div class="login-container"><el-card header="請先登錄" class="login-card"><!-- @submit.native.prevent阻止表單的默認提交行為,并將提交的事件處理函數,綁定到login上 --><el-form @submit.native.prevent="login"><el-form-item label="用戶名"><el-input v-model="model.username"></el-input></el-form-item><el-form-item label="密碼"><el-input type="password" v-model="model.password"></el-input></el-form-item><el-form-item><el-button type="primary" native-type="submit">登錄</el-button></el-form-item></el-form></el-card></div>
</template>
邏輯
- 前端傳入用戶名和密碼
<template><el-form @submit.native.prevent="login"><el-form-item label="用戶名"><el-input v-model="model.username"></el-input></el-form-item><el-form-item label="密碼"><el-input v-model="model.password" type="password"></el-input></el-form-item></el-form>
</template>
<script>export default {data(){return {model: {}}},methods:{async login(){// post提交數據,返回的是一個tokenconst res = await this.$http.post('login', this.model)}}}
</script>
- 后端得到數據后,與數據庫進行對比
- 根據用戶名,查找數據庫中的用戶信息
- 對密碼使用bcrypt的對比操作
- 通過驗證后,返回一個token
app.post('/admin/api/login', async(req, res)=>{const { username, password } = req.bodyconst AdminUser = require('../../models/AdminUser')// 根據用戶名 查找數據庫中的用戶信息const user = await AdminUser.findOne({username}).select('+password')if(!user) return res.status(422).send({message:'用戶名不存在'})// 走到這里,用戶查找到了const isValid = password && user.password && require('bcrypt').compareSync(password, user.password)if(!isValid) return res.status(422).send({ message:'密碼錯誤'}) // 到這里就使用jwt生成token并返回了// 首先需要安裝: npm i jsonwebtokenconst token = jwt.sign({id: username._id}, app.get('secret'))res.send({token})
})
注意: 上面的jwt.sign接收2個參數,第一個應該是作為區分的一個對象.如用戶的id. 第二個是一個密鑰字符串.在express中使用app.set(‘secret’, ‘Marron’),將密鑰保存在全局app中. 之后使用app.get(‘secret’)獲取該密鑰.
jwt.sign返回一個散列值,就是我們需要的token
異常處理
http響應并不總是成功的,比如在登錄這一塊.用戶名或密碼很容易不成功.這個時候就會返回一個403 forbidden,其中一個比較好的解決方案是: 使用axios的攔截器,對http的返回進行攔截. 若發生錯誤,使用Vue原型上面的$message.error
(前面已經使用elementUI掛載了)方法給出錯誤提示
// admin/src/http.js
import axios from 'axios'
import Vue from 'vue'const http = axios.create({baseURL: 'http://localhost:3000/admin/api'
})// 攔截返回的http
http.interceptors.response.use(res=>{// http響應是成功的return res},err=>{if(err.message.response.data.message){// http響應失敗.使用Vue原型上面的方法彈出錯誤Vue.prototype.$message.error({type: 'error',message: err.message.response.data.message})return Promise.reject(err)}}
)
前端得到token
- 前端得到token中,首先將其保存在瀏覽器緩存中(sessionStorage或localStorage)
- 然后跳轉到首頁,在vue中使用
this.$router.push('/')
- 彈出提示框,登錄成功, 在vue2.x + elementUI中使用
this.$message({ type:'success', message:'登錄成功'})
<template><el-form @submit.native.prevent="login"><el-form-item label="用戶名"><el-input v-model="model.username"></el-input></el-form-item><el-form-item label="密碼"><el-input v-model="model.password" type="password"></el-input></el-form-item></el-form>
</template>
<script>export default {data(){return {model: {}}},methods:{async login(){// post提交數據,返回的是一個tokenconst res = await this.$http.post('login', this.model)// 將token存入localStorage中(前端磁盤),瀏覽器關閉了,下次還能訪問到localStorage.token = res.data.token// 跳轉到首頁this.$router.push('/')// 彈出登錄成功this.$message({type: 'success',message: '登錄成功'})}}}
</script>
登錄驗證
前端請求添加token
如果用戶登錄了,那么在瀏覽器的localStorage中必然會保存token.在發送登錄請求給后端時,需要帶上這個token.在axios中使用全局的請求攔截(http.interceptors.request.use
)來實現這個功能
const http = axios.create({baseURL: 'http://localhost:3000/admin/api'
})
http.interceptors.request.use(config => {if(localStorage.token) {// 給請求頭部加tokenconfig.headers.Authorization = 'Bearer ' + localStorage.token}return config},err => {return Promise.reject(err)}
)
// 在使用`config.headers.Authorization`之后,每次http請求都會附帶一個 Authorization 請求頭.
// Authorization值的最前面加 'Bearer '的原因是為了符合規范
在添加好了對所有路由的前端請求攔截后,如果因為登錄原因,出現的錯誤.后端一般返回的是401狀態碼,這個時候,需要跳轉到登陸頁面,下面對狀態碼 401 的返回值進行處理
const http = axios.create({baseURL: 'http://localhost:3000/admin/api'
})
http.interceptors.response.use(res => { return res},err => {// 這里處理報錯if(err.response.data.message){Vue.prototype.$message.error({type: 'error',message: err.response.data.message})// 處理401狀態碼: 跳轉到登陸頁面if(err.response.status == 401) router.push('/login')}}
)
后端解析請求頭,驗證token
以上實現了前端在發送http請求時,攜帶token(放在Authorization請求頭部中),后端需要在前端通過URL請求非登陸接口時,判斷用戶是否登錄.可以寫一個登陸驗證的中間件. 如果在對正常的路徑進行處理之前,先通過中間件驗證.
中間件的邏輯如下:
- 首先通過
req.headers.authorization
獲取token,若無token則設置為''
- 若token存在則繼續下一步,否則使用
assert
拋出異常
- 若token存在則繼續下一步,否則使用
- 然后使用
jwt
驗證token.得到解碼后的id- 若存在id,則證明token沒有被篡改,繼續下一步,否則
assert
拋出異常
- 若存在id,則證明token沒有被篡改,繼續下一步,否則
- 最后根據id在數據庫中查詢user.并將user賦給
req.user
.- 若存在req.user則跳轉到下一個中間件,否則使用
assert拋出異常
- 若存在req.user則跳轉到下一個中間件,否則使用
// sever/middleware/auth.js
module.exports = options =>{const assert = require('http-assert') // 簡化代碼const jwt = require('jsonwebtoken') // 用于將token密文解析為明文const AdminUser = require('../models/AdminUser') // 獲取AdminUser模型return async(req, res, next)=>{// 獲取tokenconst token = String(req.headers.authorization || '').split(' ').pop()assert(token, 401, '請先登陸') // 這里若無token: 則代表未登陸const {id} = jwt.verify(token, req.app.get('secret'))assert(id, 401, '請先登陸') // 這里若無id: 則代表token過期或被篡改了req.user = AdminUser.findById(id)assert(req.user, 401, '請先登錄') // 這里若沒用找到user, 則代表給的是假idawait next()}
}
以上提供了一個中間件: 它判斷token是否存在(正確),若不正確則拋出異常.若正確則繼續下一個中間件.在主路由中導入.并在需要的地方添加這個中間件
// server/router/admin/index.js
module.exports = app => {const express = require('express')const authMiddleware = require('../../middleware/auth')const router = express.router({mergeParams: true})app.use('/admin/api/rest/:resource', authMiddleware(), router)
}
上面使用了assert拋出異常,因此還需要一個錯誤捕捉中間件,放在所有路由的最后,用于捕捉錯誤,它會將捕獲到的異常,作為參數傳遞給前端.這樣前端就能根據狀態碼做出響應的處理了
// server/router/admin/index.js
module.exports = app => {const express = require('express')const authMiddleware = require('../../middleware/auth')const router = express.router({mergeParams: true})// 放到所有路由的后面app.use(async (err, req, res, next)=>{console.log(err)res.status(err.statesCode || 500).send({ message: err.message})})
}
前端路由校驗
上面已經完成了,在發送Http請求時,進行路由校驗.若401則跳轉到登陸頁面.但是,那些不需要HTTP請求的網頁還是可以不需要登陸就能直接訪問.下面有必要做前端的路由校驗
【具體實現如下】:
- 將不需要登陸的就能訪問的(主要是是Login組件)路由,添加一個
{isPublic: true}
屬性 - 然后使用全局前置守衛,在進入路由前判斷
localStorage
中是否存在token.若存在,則進行下一步,否則跳轉到登陸頁面. 而登陸頁面設置了公開訪問屬性,因此不會觸發全局前置守衛
// admin/src/router/index.js
import Vue from 'vue'
import VurRouter from 'vue-router'
const routes = [{ path: '/login',name: 'login',component: Login,meta: { isPublic: true}},{path: '/',name: 'main',component: Main,children: [ ... ]}
]
const router = new VueRouter({routes
})router.beforeEach((to, from, next) =>{if(!to.meta.isPublic && localStorage.token){return next('/login')}next()
})
export default router
在主函數中加載路由,并渲染更新
// admin/src/main.js
import Vue from 'vue'
import App from './App.vue'
import './plugins/element.js'
import router from './router'import './style.css'Vue.config.productionTip = falsenew Vue({router,render: h => h(App)
}).$mount('#app')