实战精髓,项目级登录鉴权方案及权限跳转【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万+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章