美文网首页
全栈之鉴权之旅 -- JWT + passport 实现 Tok

全栈之鉴权之旅 -- JWT + passport 实现 Tok

作者: ZHero_ | 来源:发表于2020-04-01 13:56 被阅读0次

    登陆认证 (鉴权),是每个应用都需要的基础功能。但很多的时候,却都被大家所忽略,不仅安全漏洞严重,而且代码紧耦合,混乱不堪。
    Passport & JWT,正是为了解决登陆认证的事情,让认证模块更透明,减少耦合!


    网上关于 JSONWebToken (以下简称 JWT ) && passport.js的中文学习资料较少,学习的时候还蛮吃力的。所以总结出此篇,文章若有错谬,欢迎指出,我会及时更正。


    转载请注明出处: https://blog.csdn.net/q95548854/article/details/103906889


    一、JWT 是什么?

    JSON Web Token(JWT)是一个非常轻巧的规范。这个规范允许我们使用JWT在用户和服务器之间传递安全可靠的信息。
    一个JWT实际上就是一个字符串,它由三部分组成,头部、载荷与签名。
    想了解更多关于 JWT 的,请查看我的另外一篇文章: 全栈之初识JWT -- Web安全的守护神

    二、Passport 是什么?

    1、概述

    passport.js是Nodejs中的一个做登录验证的中间件,极其灵活和模块化,并且可与Express、Sails等Web框架无缝集成。Passport功能单一,即只能做登录验证,但非常强大,支持本地账号验证第三方账号登录验证(OAuth和OpenID等),支持大多数Web网站和服务。

    想了解更多关于 Passport 的,请查看我的另外一篇文章: 全栈之初识 Passport & Passport-jwt – Web安全的守护神

    本文中只涉及最基本最常用的 本地账号验证

    三、整理流程思路

    • 1、当用户登录时,后端会校验用户名密码后,创建 Token 并设置在 Cookie 内返回
    • 2、前端之后的每次请求都会携带 Cookie (自动的,前端无需任何设置)
    • 3、后端通过中间件校验 Token 并获取其中信息校验,通过后再进行正常响应。-
    • 4、另外本文中未使用 redis,并没有将 Token 存储持久化,所以准备在前端请求中间件中每次都判断是否存在token,如不存在,请求后端重新生成token,实现默默登录~

    四、环境依赖

    1、技术栈

    • 前端使用 Vue + Nuxt
    • 后端使用 Node + Express + Mongo

    2、后端依赖

    • express express-session body-parser(express 基础套件)
    • mongoose (操作 Mongo)
    • nodemon(node项目热更新)
    • md5(密码加密)
    • jsonwebtoken(生成token)
    • passport passport-jwt passport-local (passport 套件,验证&解析token)
    • eslint lodash moment uuid(辅助套件)

    五、后端项目搭建

    生成express项目

    npm install express-generator -g
    express -e --git RMS-BE
    

    安装好以上说的各种依赖后,整理项目结构

    RMS-BE
        |----node_modules
        |----src
            |----common # 公共js (配置文件/二次封装)
                |----passport-local.js # passport local 策略
            |----config # 数据库配置模块
                |----index.js
            |----controllers # MVC中的C,用户数据与视图的衔接处理
                |----auth.js # 登录、退出等权限控制
                |----people.js
            |----middleware # 中间件
                |----auth.js # token鉴权中间件
            |----models # 处理响应的数据,是数据模型
                |----user.js
                |----people.js
            |----routes # 路由模块
                |----authRouter.js # 登录等权限控制路由
                |----people.js
        |----server.js # 入口文件
        |----package.json
    

    ==1、server.js==

    const express = require('express');
    const mongoose = require('mongoose');
    const bodyParser = require('body-parser');
    const cookieParser = require('cookie-parser');
    const logger = require('morgan');
    const session = require('express-session');
    const passport = require('passport');
    
    const config = require('./src/config');
    
    const port = process.env.PORT || 8899;
    const app = express();
    
    app.use(logger('dev'));
    app.use(bodyParser.json());
    app.use(bodyParser.urlencoded({ extended: false }));
    app.use(cookieParser());
    
    app.use(session({
      secret: config.secret,
      resave: true,
      saveUninitialized: true
    }));
    app.use(passport.initialize()); // 使 passport 持久化,不只是session
    app.use(passport.session());
    app.use((req, res, next) => {
      req.passport = passport // 为了在中间件中可以调用到 passport
      res.header('Access-Control-Allow-Origin', '*');
      res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept');
      if (req.method == 'OPTIONS') {
        return res.send(200);
      } else {
        next();
      }
    });
    
    const mongoHost  = `mongodb://${config.host}:${config.port || 27017}/${config.database}`
    mongoose.Promise = global.Promise
    mongoose.connect(mongoHost, {
      useCreateIndex: true,
      useNewUrlParser: true,
      useUnifiedTopology: true,
      useFindAndModify: false,
      config: {
        autoIndex: true,
      },
    }).then(() => {
      console.log('connection established:', mongoHost)
    }).catch(err => {
      console.error(err)
    })
    
    require('./src/common/passport-local')(passport);
    
    // Route Section
    require('./src/routes/authRouter')(app);
    require('./src/routes/people')(app);
    
    app.listen(port, () => console.log(`Server running on PORT: ${port}`));
    

    ==2、routes/authRouter.js==

    const authMiddleware = require('../middleware/auth')
    const authController = require('../controllers/auth')
    
    module.exports = function (app) {
      app.post('/login', authController.login)
      app.post('/validateToken', authMiddleware, authController.validateToken)
      app.get('/getUser', authMiddleware, authController.getUser)
      app.post('/register', authMiddleware, authController.register)
      app.post('/changeUserInfo', authMiddleware, authController.changeUserInfo)
      app.post('/deleteUser', authMiddleware, authController.deleteUser)
      app.post('/logout', authMiddleware, authController.logout)
    }
    

    ==3、models/user.js==

    const mongoose = require('mongoose')
    const moment = require('moment')
    const uuid     = require('uuid')
    const Schema = mongoose.Schema
    
    const userSchema = new Schema({
      user_id : { type: Number, required: true },
      user_uuid : { type: String, required: true },
      user_name : { type: String, required: true },
      user_password : { type: String, required: true },
      user_created : { type: Number },
      user_updated : { type: Number },
      user_role : { type: Number, required: true } // 0: 账户锁定(无权限) 1: 普通用户 2: admin 3: superadmin
    })
    
    userSchema.pre('validate', function (next) {
      this.user_uuid = this.user_uuid  || uuid.v4()
      this.user_created = this.user_created || moment().format('X')
      this.user_updated = moment().format('X')
      next()
    })
    
    module.exports = mongoose.model('user', userSchema)
    

    ==4、middleware/auth.js==

    module.exports = function (req, res, next) {
      // if (req.isAuthenticated()) return next()
      req.passport && req.passport.authenticate('jwt', { session: false }, (err, user, info) => {
        if (err) { return next(err) }
        if (!user)   return res.send({ success: true, code: 0, message: '权限禁止' })
        req.userInfo = user
        next()
      })(req, res, next)
    }
    

    ==5、controllers/auth.js==

    const User = require('../models/user')
    const md5 = require('md5')
    // const bcrypt = require('bcrypt')
    const { get } = require('lodash')
    const uuid = require('uuid')
    const jwt = require('jsonwebtoken') //token 认证
    const config = require('../config')
    // const salt = bcrypt.genSaltSync(config.saltRounds)
    
    const GenerateToken = user => {
      return jwt.sign({
        user_id: get(user, 'user_id'),
        user_uuid: get(user, 'user_uuid'),
        user_name: get(user, 'user_name'),
        user_role: get(user, 'user_role')
      }, config.JWT_SECRET, {
        jwtid: uuid.v4(),
        expiresIn: config.JWT_EXPIRY,
        issuer: config.JWT_ISSUER,
        audience: config.JWT_AUDIENCE,
        algorithm: config.JWT_ALG
      })
    }
    
    const ReturnUserInfo = user => {
      return {
        user_id: get(user, 'user_id'),
        user_uuid: get(user, 'user_uuid'),
        user_name: get(user, 'user_name'),
        user_created: get(user, 'user_created'),
        user_updated: get(user, 'user_updated'),
        user_role: get(user, 'user_role')
      }
    }
    
    const login = async function (req, res, next) {
      try {
        const username = get(req, 'body.user_name')
        const password = get(req, 'body.user_password')
        const userInfo = await User.findOne({ user_name: username })
        if (userInfo) {
          const verify = md5(password) === get(userInfo, 'user_password')
          if (verify) {
            // 生成token
            const token = GenerateToken(userInfo)
            res.cookie('authorization', token, {
              httpOnly: true,
              secure: process.env.NODE_ENV === 'production',
              expires: new Date(Date.now() + config.JWT_EXPIRY)
            })
            // 存储token到redis
            return res.send({ success: true, code: 1, token: 'Bearer ' + token, user: ReturnUserInfo(userInfo) })
          } else {
            return res.send({ success: true, code: 0, message: '密码错误!' })
          }
        } else {
          return res.send({ success: true, code: 0, message: '该用户不存在!' })
        }
      } catch (error) {
        return res.send({ success: true, code: 0, message: '登录失败!error:' + error })
      }
    }
    
    const validateToken = async function (req, res, next) {
      const token = GenerateToken(req.userInfo)
      res.cookie('authorization', token, {
        httpOnly: true,
        secure: process.env.NODE_ENV === 'production',
        expires: new Date(Date.now() + config.JWT_EXPIRY)
      })
      return res.send({ success: true, code: 1, user: ReturnUserInfo(req.userInfo) })
    }
    
    const getUser =  async function (req, res, next) {
      if (get(req, 'userInfo.user_role') > 2) {
        try {
          const allUser = await User.find({})
          return res.send({ success: true, code: 1, user_list: allUser || [] })
        } catch (error) {
          return res.send({ success: true, code: 0, message: '获取用户列表失败!error:' + error })
        }
      } else {
        return res.send({ success: true, code: 0, message: '权限不足,禁止访问!' })
      }
    }
    
    const register = async function (req, res, next) {
      if (get(req, 'userInfo.user_role') === 3) {
        try {
          const username = get(req, 'body.user_name')
          const password = get(req, 'body.user_password') || '123456'
          const userRole = get(req, 'body.user_role')
          const checkUsername = await User.findOne({ user_name: username })
          if (checkUsername) {
            return res.send({ success: true, code: 0, message: '该用户已存在!' })
          } else {
            let userid = 0
            const rows = await User.find({}).sort({'user_id':-1}).limit(1)
            if (rows && rows.length) {
              userid = rows[0].user_id + 1
            } else {
              userid = 0
            }
            const newUserInfo = new User({
              user_id: userid,
              user_name: username,
              user_password: md5(password),
              user_role: userRole
            })
            newUserInfo.save().then(result => {
              return res.send({ success: true, code: 1, user: ReturnUserInfo(result) })
            }).catch(error => {
              console.log("Error:" + error)
              return res.send({ success: true, code: 0, message: '注册失败!error:' + error })
            })
          }
        } catch (error) {
          return res.send({ success: true, code: 0, message: '注册失败!error:' + error })
        }
      } else {
        return res.send({ success: true, code: 0, message: '权限不足,禁止注册!' })
      }
    }
    
    const changeUserInfo = async function (req, res, next) {
      const useruuid = get(req, 'body.user_uuid')
      const tokenUserRole = get(req, 'userInfo.user_role')
      if (tokenUserRole === 3 || useruuid === get(req, 'userInfo.user_uuid')) {
        try {
          const isResetPsw = get(req, 'body.is_reset_password')
          const isChangePsw = get(req, 'body.is_change_password')
          const isChangeUsername = get(req, 'body.is_change_username')
          const username = get(req, 'body.user_name')
          const userRole = tokenUserRole === 3 ? get(req, 'body.user_role') : tokenUserRole
          const userInfo = await User.findOne({ user_uuid: useruuid })
          if (userInfo) {
            const params = {
              user_name: username,
              user_role: userRole
            }
            if (isChangeUsername) { // 重置用户名
              const checkNewUsername = await User.findOne({ user_name: username })
              if (checkNewUsername) return res.send({ success: true, code: 0, message: '用户名已存在' })
              params.user_name = username
            }
    
            if (isResetPsw) { // 重置密码
              params.user_password = md5('123456')
            }
    
            if (isChangePsw) { // 通过原密码修改密码
              const password = get(req, 'body.user_password')
              const verify = md5(password) === get(userInfo, 'user_password')
              if (!verify) return res.send({ success: true, code: 0, message: '原密码错误!' })
              const newPassword = get(req, 'body.new_user_password')
              params.user_password = md5(newPassword)
            }
            
            await User.update({ user_uuid: useruuid }, params)
            const newUserInfo = await User.findOne({ user_uuid: useruuid })
            return res.send({ success: true, code: 1, user: ReturnUserInfo(newUserInfo) })
          } else {
            return res.send({ success: true, code: 0, message: '该用户不存在!' })
          }
        } catch (error) {
          return res.send({ success: true, code: 0, message: '修改失败!error:' + error })
        }
      } else {
        return res.send({ success: true, code: 0, message: '权限不足,禁止修改!' })
      }
    }
    
    const deleteUser = async function (req, res, next) {
      if (get(req, 'userInfo.user_role') === 3) {
        try {
          const useruuid = get(req, 'body.user_uuid')
          const checkUser = await User.findOne({ user_uuid: useruuid })
          if (checkUser) {
            const newUserInfo = await User.remove({
              user_uuid: useruuid
            })
            return res.send({ success: true, code: 1, user: ReturnUserInfo(newUserInfo) })
          } else {
            return res.send({ success: true, code: 0, message: '该用户不存在!' })
          }
        } catch (error) {
          return res.send({ success: true, code: 0, message: '删除失败!error:' + error })
        }
      } else {
        return res.send({ success: true, code: 0, message: '权限不足,禁止删除!' })
      }
    }
    
    const logout = async function (req, res, next) {
      // 清除redis中的token
      res.clearCookie('authorization')
      return res.send({ success: true, code: 1, message: '退出成功!' })
    }
    
    module.exports = {
      login,
      validateToken,
      getUser,
      register,
      changeUserInfo,
      deleteUser,
      logout
    }
    
    

    ==6、config/index.js==

    module.exports = {
      secret : 'renyide',
      host : process.env.DB_HOST || 'localhost',
      port : process.env.DB_PORT || '27017',
      database: 'rms',
      JWT_SECRET: 'renyide',
      JWT_EXPIRY: 86400000,
      JWT_ISSUER: 'RMS',
      JWT_AUDIENCE: 'RMS_XH',
      JWT_ALG: 'HS256'
    }
    

    ==7、common/passport-local.js==

    const JwtStrategy = require('passport-jwt').Strategy
    const ExtractJwt = require('passport-jwt').ExtractJwt
    const User = require('../models/user')
    
    const config = require('../config')
    
    const opts = {
      // Prepare the extractor from the header.
      jwtFromRequest: ExtractJwt.fromExtractors([
        req => req.cookies['authorization'],
        ExtractJwt.fromUrlQueryParameter('access_token'),
        ExtractJwt.fromAuthHeaderWithScheme('Bearer'),
      ]),
      // Use the secret passed in which is loaded from the environment. This can be
      // a certificate (loaded) or a HMAC key.
      secretOrKey: config.JWT_SECRET,
      // Verify the issuer.
      issuer: config.JWT_ISSUER,
      // Verify the audience.
      audience: config.JWT_AUDIENCE,
      // Enable only the HS256 algorithm.
      algorithms: [config.JWT_ALG],
      // Pass the request object back to the callback so we can attach the JWT to it.
      passReqToCallback: true
    }
    
    module.exports = passport => {
      passport.use(new JwtStrategy(opts, async function (req, jwt_payload, done) {
        try {
          const userInfo = await User.findOne({
            user_uuid: jwt_payload.user_uuid
          })
          if (userInfo && userInfo.user_role > 0) {
            done(null, userInfo)
          } else {
            done(null, false)
          }
        } catch (e) {
          return done(e)
        }
      }))
    }
    

    六、前端部分代码

    ==1、中间件==

    
    export default async function ({ app, store, error, redirect, req }) {
      if (req && (req.url === '/__webpack_hmr' || req.url === '/__webpack_hmr/client' || req.url === '/api/v1/validateToken')) return
      await store.dispatch('auth/validateToken')
      if (!store.state.auth.user.user_id) {
        return redirect('/login')
      } else if (store.state.auth.user.user_role === 0) {
        alert('您的账户被冻结,请联系管理员!')
        return redirect('/login')
      }
    }
    
    

    ==2、以vuex中是否存在userid判断是否有token,登录校验请求==

    import Vue from 'vue'
    import Vuex from 'vuex'
    import { deleteCookie } from '~/utils/cache'
    
    Vue.use(Vuex)
    
    export const state = () => ({
      user: {}
    })
    
    export const mutations = {
      setUser (state, data) {
        state.user = data
      }
    }
    
    export const actions = {
      async login ({ commit, dispatch }, params) {
        try {
          const { data } = await this.$axios.post(`/api/v1/login`, params)
          if (data.code !== 0) {
            this.$message.success('登陆成功!')
            commit('setUser', data.user)
          } else {
            console.log(data.message)
            this.$message.error('登录失败!' + data.message)
          }
        } catch (e) {
          console.log(e)
          await dispatch('logout')
          throw e
        }
      },
      async logout ({ commit }) {
        try {
          const { data } = await this.$axios.post(`/api/v1/logout`)
          if (data.code !== 0) {
            this.$message.success('退出成功!')
          } else {
            console.log(data.message)
            this.$message.error('退出失败!' + data.message)
          }
        } catch (e) {
          console.log(e)
        } finally {
          commit('setUser', {})
          deleteCookie('authorization')
          window.location.href = '/login'
        }
      },
      async validateToken ({ commit, dispatch }) {
        try {
          const { data } = await this.$axios.post(`/api/v1/validateToken`)
          if (data.code === 0) {
            console.log(data.message)
            this.$message.error(data.message)
          } else {
            commit('setUser', data.user)
          }
        } catch (e) {
          console.log(e)
        }
      }
    }
    
    

    觉得有帮助的小伙伴右上角点个赞~

    在这里插入图片描述

    扫描上方二维码关注我的订阅号~

    本文由博客一文多发平台 OpenWrite 发布!

    相关文章

      网友评论

          本文标题:全栈之鉴权之旅 -- JWT + passport 实现 Tok

          本文链接:https://www.haomeiwen.com/subject/xszouhtx.html