實戰精髓,項目級登錄鑑權方案及權限跳轉【Node vs Vue】

前言

不管是企業項目,還是個人項目,一個優秀的系統必須具有鑑權的能力,何爲鑑權,是指驗證用戶是否擁有訪問系統的權利。

前端鑑權的方式也很多,例如 HTTP Basic Authenticationsession-cookieOAuth(開放授權)JWT ···

本章通過node.js vue爲框架,模擬出一套較爲完整的前後端配合鑑權方案(採用JWT鑑權理念)

❗ PS:本章對基本知識不會有過多講解,主要對涉及技術的應用進行代碼演示


需求分析

  • 登錄成功時,後端返回Token
  • token必須帶有時效性,過期則無效
  • 前端調用接口時,需要攜帶上token才能訪問
  • 前端在跳轉需要權限的頁面時,需要判斷當前是否已經登錄,以及是否登錄過期

技術棧

Vue、Node.js 作爲前後端開發框架

  • axios 作爲請求接口的HTTP庫
  • express 作爲接口開發的框架
  • Node.js涉及庫
    • body-parser - 處理POST數據
    • bcrypt - 密碼加密以及解密匹配
    • jwt
      • jsonwebtoken - 生成token
      • passport、passport-jwt - 解析token
  • jwt-decode - 前端解析token以token獲取有效時間
  • mongoose 連接Mongo數據庫

Node.js 後臺開發

初始化Express

初始化我們的框架,引入所需要的依賴庫,爲一切功能開發做好準備

😊 Tips : 關於mongoose的操作,不會進行解析;關鍵功能代碼會重點標識

  • app.js 入口文件
const express = require('express') // 引入 express
const app = express() // 實例化 express
const mongoose = require('mongoose') // 引入 mongoose
const db = require('./config/mongokey.js').mongoURI // 引入 數據庫路徑
const bodyParser = require('body-parser') // 引入 body-parser 作用:處理 post 請求
const passport = require('passport') // 作用:解析token

const port = process.env.PORT || 5000 // 設置端口號,本地爲5000

// 測試
// app.get('/', (req, res) => {
//     res.send('Test,please ignore!')
// })

// 連接數據庫
mongoose.connect(db, { useNewUrlParser: true, useUnifiedTopology: true }).then(() => {
    // success
    console.log('Mongo Connect Successful')
}).catch((e) => {
    // fail
    console.log('Mongo Connect fail')
})
mongoose.set('useFindAndModify', false) // 屏蔽useFindAndModify廢棄警告

// 使用 body-parser 中間件 處理 POST 數據請求
app.use(bodyParser.urlencoded({
    extended: false
}))
app.use(bodyParser.json())

// 初始化 passport 解析 token (關於passport配置請往下看 👇)
app.use(passport.initialize())
require('./config/passport')(passport)

// ---- CORS setHeader 跨域設置 ----
app.use((req, res, next) => {
    res.header("Access-Control-Allow-Origin", "*");
    res.header("Access-Control-Allow-Headers", "Content-Type,Authorization");
    res.header("Access-Control-Allow-Methods", "PUT,POST,GET,DELETE,OPTIONS");
    next();
})

/**
 * 引入路由表 & 使用路由
 * @users 用戶相關
 */
const users = require('./routes/Api/users')
app.use('/hdgc/users', users)


app.listen(port, () => {
    console.log(`❤  Server running on port ${port} ❤`)
})
  • ./config/passport
/**
 *  passport 配置文件
 *  @引入 passport-jwt
 *  @Strategy 策略
 *  @ExtractJwt 
 *  @options jwtFromRequest 請求攜帶的token, secretOrKey 生成token時的加密名字
 */

const Strategy = require('passport-jwt').Strategy
const ExtractJwt = require('passport-jwt').ExtractJwt
const User = require('../models/User') // 引入數據模型 // 需要用到 mongoose 中的 model
const options = {}
options.jwtFromRequest = ExtractJwt.fromAuthHeaderAsBearerToken()
options.secretOrKey = 'secretKey' // 這裏的secretOrKey需要與生成token時的加密命名一致,此處需注意!!

module.exports = passport => {
    /**
     * @jwt_payload 請求得到的內容
     * @done 表示策略結束,返回信息
     */
    passport.use(new Strategy(options, (jwt_payload, done) => {
        User.findById(jwt_payload.id).then(user => {
            if (user) {
                return done(null, user)
            }
            return done(null, false)
        }).catch(err => {
            console.log(err)
        })
    }))
}


//備註:passport 會在接口處使用,將請求攜帶的token進行解析,然後判斷User中是否存在此用戶存在則認證成功並將用戶返回,否則認證失敗

Api層開發

  • ./routes/Api/users
const express = require('express')
const router = express.Router()
const bcrypt = require('bcryptjs') // 加密插件
const jsonwebtoken = require('jsonwebtoken') // 生成 token
const passport = require('passport') // 解析token
const User = require('../../models/User') // 引入數據模型

/**
 * 用戶相關登錄、註冊接口
 * @json
 *  - code: 信息碼
 *  - data:數據
 *  - messgae:提示信息
 * @表單驗證由前端處理
 */
router.post('/register', (req, res) => {
    console.log(req.body)
        // 1- 判斷數據庫是否已存在該用戶名
    User.findOne({
        username: req.body.username
    }).then((user) => {
        if (user) {
            return res.json({
                code: '-1',
                email: '用戶名已存在'
            })
        } else {
            const newUser = new User({
                username: req.body.username,
                password: req.body.password
            })

            // 使用bcrypt對password加密處理
            bcrypt.genSalt(10, (err, salt) => {
                bcrypt.hash(newUser.password, salt, (err, hash) => {
                    // hash - 加密後的密碼
                    if (err) {
                        // 加密異常捕獲
                        res.json({
                            code: '-1',
                            message: `密碼加密異常捕獲:${err}`
                        })
                        return
                    }
                    newUser.password = hash

                    // 存入數據庫
                    newUser.save().then(user => {
                        res.json({
                            code: '0',
                            data: user,
                            message: 'register successful'
                        })
                    }).catch(err => {
                        // 異常捕獲
                        res.json({
                            code: '-1',
                            message: `異常捕獲:${err}`
                        })
                    })
                })
            })
        }
    })
})

router.post('/login', (req, res) => {
    console.log(req.body)
    const username = req.body.username
    const password = req.body.password
        // 查詢當前用戶是否存在
    User.findOne({
        username: username
    }).then((user) => {
        if (!user) {
            return res.json({
                code: '-1',
                message: '當前用戶未註冊'
            })
        }
        // 使用bcrypt對加密密碼進行解密匹配
        bcrypt.compare(password, user.password).then(isMatch => {
            if (isMatch) {
                // 匹配成功
                const rule = {
                        id: user.id,
                        username: user.username
                    }
                    /**
                     * jsonwebtoken 參數意義
                     * @規則
                     * @加密名字 - 這個名字必須與passport配置的secretOrKey一致
                     * @過期時間
                     * @箭頭函數
                     * @返回token
                     */
                jsonwebtoken.sign(rule, 'secretKey', { expiresIn: 3600 }, (err, token) => {
                    if (err) {
                        // token生成異常捕獲
                        res.json({
                            code: '-1',
                            message: `token生成異常捕獲:${err}`
                        })
                        return
                    }
                    res.json({
                        code: '0',
                        data: user,
                        token: 'Bearer ' + token, // 必須在前面加上 'Bearer ' !!!!
                        message: 'Login successful'
                    })
                })
            } else {
                // 匹配失敗
                return res.json({
                    code: '-1',
                    message: '用戶名或密碼錯誤'
                })
            }
        })
    })
})

// 獲取當前用戶信息,需要進行鑑權認證!!!注意此處passport認證策略就是在passport.js中配置的
router.get('/', passport.authenticate('jwt', { session: false }), (req, res) => {
    User.findOne({
        username: req.user.username
    }).then((user) => {
        if (!user) {
            return res.json({
                code: '-1',
                message: '用戶信息不存在'
            })
        }
        req.user.password = '******' // user 爲 passport 執行 done() 所傳入的信息,注意password不能明文出現
        res.json({
            code: '0',
            data: req.user,
            message: 'success'
        })
    })
})
module.export = router

Vue 前臺開發

需求分析

  • axios 封裝並實現請求攔截,響應攔截
  • router 路由實現特定頁面鑑權跳轉
  • vuex 對認證狀態進行管理

axios 封裝與攔截

import axios from 'axios'
import store from '../store'

/**
 * Axios 基本配置封裝(默認配置,可被覆蓋)
 * @baseURL 基礎路徑(前綴)
 * @timeout 超時時間
 * @responseType 響應數據類型 (json)
 * @withCredentials 是否允許帶cookie等
 * @header 根據不同請求設置
 */
const Axios = axios.create({
    baseURL: '/',
    timeout: 10000,
    responseType: 'json',
    withCredentials: true,
    headers: {
        'Content-Type': 'application/json;charset=UTF-8'
    }
})

// 請求攔截,一旦調用接口,將vuex中的loading設置爲true,顯示加載頁面
Axios.interceptors.request.use(config => {
    store.dispatch('setLoading', true)
    if (localStorage.Token) {
        // 當緩存中存在Token時,將Token設置爲請求頭的 Authorization
        config.headers.Authorization = localStorage.Token
    }
    return config
}, error => {
    // 請求報錯時 loading 更新爲 false
    store.dispatch('setLoading', false)
    return Promise.reject(error)
})

// 響應攔截,一旦接口返回,將vuex中的loading設置爲false,顯示加載頁面
Axios.interceptors.response.use(response => {
    store.dispatch('setLoading', false)
    return response
}, error => {
    // 響應報錯時 loading 更新爲 false
    store.dispatch('setLoading', false)
    return Promise.reject(error)
})

export default Axios

/*
 * 存放 Api 接口文件,在頁面中直接引入對於接口,即可使用
 * @userRegister 註冊接口
 *      registerInfo 註冊表單數據
 * @userLogin 登錄接口
 *      loginInfo 登錄表單數據
 * @getUserInfo 獲取登錄人信息 - 此接口需要進行鑑權認證,不通過是無法調用
 */
 
import Axios from './http.js'

export function userRegister(registerInfo) {
    return Axios({
        url: '/hdgc/users/register',
        data: registerInfo,
        method: 'post',
        headers: {
            'Content-Type': 'application/json;charset=UTF-8'
        }
    })
}
export function userLogin(loginInfo) {
    return Axios({
        url: '/hdgc/users/login',
        data: loginInfo,
        method: 'post',
        headers: {
            'Content-Type': 'application/json;charset=UTF-8'
        }
    })
}
export function getUserInfo(username) {
    return Axios({
        url: '/hdgc/users/',
        data: username,
        method: 'get'
    })
}

beforeEach() 實現頁面權限跳轉

import Vue from 'vue'
import Router from 'vue-router'
import store from '../store';
import jwt_decode from 'jwt-decode'
import { Message } from 'element-ui'
Vue.use(Router)

// ...

route.beforeEach((to, from, next) => {
    // 若返回首頁,無須鑑權
    if (to.path == '/') {
        next()
    } else {
        /**
         * 判斷當前是否存在Token
         * @存在則進行鑑權判斷
         * @不存在則返回首頁
         */
        if (localStorage.Token) {
            /**
             * 判斷當前Token是否過期
             * @過期則跳回首頁
             * @未過期則成功跳轉
             */
            const decoded = jwt_decode(localStorage.Token)
            const currentTime = Date.now() / 1000
            console.log('Token_Decode & currentTime', decoded, currentTime)
            if (decoded.exp < currentTime) {
                Vue.prototype.$notify({
                    title: 'Tips',
                    message: 'Token過期,重新登錄',
                    type: 'error',
                    duration: 3000
                })
                store.dispatch('clearCurrentState') // 清空vuex
                next('/')
            } else {
                next()
            }
        } else {
            Vue.prototype.$notify({
                title: 'Tips',
                message: '請先登錄!',
                type: 'error',
                duration: 3000
            })
            store.dispatch('clearCurrentState')  // 清空vuex
            next('/')
        }
    }
})

接口調用 (重點爲登錄接口)

signinClcik:function(){
            // loginInfo 爲參數
            userLogin(this.loginInfo).then( res => {
                if(res.data.code == 0){
                    // 獲取 token 存入緩存 (重點!!!)
                    const token = res.data.token
                    window.localStorage.setItem('Token',token)
                    // 更新授權狀態
                    this.$store.dispatch('setIsAuthenticated',true)
                }else{
                    this.$notify({
                        title: 'Tips',
                        message: res.data.message,
                        type: 'error',
                        duration:3000
                    })
                }
            })
         },

總結

以上這套JWT鑑權方案涉及前後端的一個配合,比較適合企業級,個人級的項目開發,權限認證較嚴格,所涉及的技術棧也比較簡單,特別適合新手練手,打造一個自己的鑑權系統

如果大家喜歡,歡迎點個贊 👍

發佈了12 篇原創文章 · 獲贊 26 · 訪問量 3萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章