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

前言

無論是企業項目,仍是我的項目,一個優秀的系統必須具備鑑權的能力,何爲鑑權,是指驗證用戶是否擁有訪問系統的權利。前端

前端鑑權的方式也不少,例如 HTTP Basic Authenticationsession-cookieOAuth(開放受權)JWT ···vue

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

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


需求分析

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

技術棧

Vue、Node.js 做爲先後端開發框架web

  • 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

初始化咱們的框架,引入所須要的依賴庫,爲一切功能開發作好準備vue-router

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

  • 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.exports = 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
                    })
                }
            })
         },
複製代碼

效果展現

  • 未登陸(頁面沒法跳轉)
  • 登陸過時(這裏我將過時時間設置爲5s)

  • 調用需鑑權接口(攜帶上token)

總結

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

若是你們喜歡,但願爸爸們點個贊 😭express

相關文章
相關標籤/搜索