全棧之鑒權(quán)之旅 -- JWT + passport 實現(xiàn) Token 驗證(Node + Express)

登陸認(rèn)證 (鑒權(quán))钾挟,是每個應(yīng)用都需要的基礎(chǔ)功能。但很多的時候组哩,卻都被大家所忽略等龙,不僅安全漏洞嚴(yán)重,而且代碼緊耦合伶贰,混亂不堪蛛砰。
Passport & JWT,正是為了解決登陸認(rèn)證的事情黍衙,讓認(rèn)證模塊更透明泥畅,減少耦合!


網(wǎng)上關(guān)于 JSONWebToken (以下簡稱 JWT ) && passport.js的中文學(xué)習(xí)資料較少琅翻,學(xué)習(xí)的時候還蠻吃力的位仁。所以總結(jié)出此篇,文章若有錯謬方椎,歡迎指出聂抢,我會及時更正。


轉(zhuǎn)載請注明出處: https://blog.csdn.net/q95548854/article/details/103906889


一棠众、JWT 是什么琳疏?

JSON Web Token(JWT)是一個非常輕巧的規(guī)范。這個規(guī)范允許我們使用JWT在用戶和服務(wù)器之間傳遞安全可靠的信息闸拿。
一個JWT實際上就是一個字符串空盼,它由三部分組成,頭部新荤、載荷與簽名揽趾。
想了解更多關(guān)于 JWT 的,請查看我的另外一篇文章: 全棧之初識JWT -- Web安全的守護(hù)神

二苛骨、Passport 是什么篱瞎?

1苟呐、概述

passport.js是Nodejs中的一個做登錄驗證的中間件,極其靈活和模塊化俐筋,并且可與Express掠抬、Sails等Web框架無縫集成。Passport功能單一校哎,即只能做登錄驗證,但非常強(qiáng)大瞳步,支持本地賬號驗證第三方賬號登錄驗證(OAuth和OpenID等)闷哆,支持大多數(shù)Web網(wǎng)站和服務(wù)。

想了解更多關(guān)于 Passport 的单起,請查看我的另外一篇文章: 全棧之初識 Passport & Passport-jwt – Web安全的守護(hù)神

本文中只涉及最基本最常用的 本地賬號驗證

三抱怔、整理流程思路

  • 1、當(dāng)用戶登錄時嘀倒,后端會校驗用戶名密碼后屈留,創(chuàng)建 Token 并設(shè)置在 Cookie 內(nèi)返回
  • 2、前端之后的每次請求都會攜帶 Cookie (自動的测蘑,前端無需任何設(shè)置)
  • 3灌危、后端通過中間件校驗 Token 并獲取其中信息校驗,通過后再進(jìn)行正常響應(yīng)碳胳。-
  • 4勇蝙、另外本文中未使用 redis,并沒有將 Token 存儲持久化挨约,所以準(zhǔn)備在前端請求中間件中每次都判斷是否存在token味混,如不存在,請求后端重新生成token诫惭,實現(xiàn)默默登錄~

四翁锡、環(huán)境依賴

1、技術(shù)棧

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

2夕土、后端依賴

  • express express-session body-parser(express 基礎(chǔ)套件)
  • 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

安裝好以上說的各種依賴后隘弊,整理項目結(jié)構(gòu)

RMS-BE
    |----node_modules
    |----src
        |----common # 公共js (配置文件/二次封裝)
            |----passport-local.js # passport local 策略
        |----config # 數(shù)據(jù)庫配置模塊
            |----index.js
        |----controllers # MVC中的C哈踱,用戶數(shù)據(jù)與視圖的銜接處理
            |----auth.js # 登錄、退出等權(quán)限控制
            |----people.js
        |----middleware # 中間件
            |----auth.js # token鑒權(quán)中間件
        |----models # 處理響應(yīng)的數(shù)據(jù)梨熙,是數(shù)據(jù)模型
            |----user.js
            |----people.js
        |----routes # 路由模塊
            |----authRouter.js # 登錄等權(quán)限控制路由
            |----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 // 為了在中間件中可以調(diào)用到 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: 賬戶鎖定(無權(quán)限) 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: '權(quán)限禁止' })
    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 認(rèn)證
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: '權(quán)限不足绊袋,禁止訪問毕匀!' })
  }
}

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: '權(quán)限不足躁垛,禁止注冊!' })
  }
}

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: '權(quán)限不足活玲,禁止修改!' })
  }
}

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: '權(quán)限不足穗熬,禁止刪除镀迂!' })
  }
}

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('您的賬戶被凍結(jié)授翻,請聯(lián)系管理員瓢谢!')
    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)
    }
  }
}


覺得有幫助的小伙伴右上角點(diǎn)個贊~

在這里插入圖片描述

掃描上方二維碼關(guān)注我的訂閱號~

本文由博客一文多發(fā)平臺 OpenWrite 發(fā)布起意!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市病瞳,隨后出現(xiàn)的幾起案子揽咕,更是在濱河造成了極大的恐慌,老刑警劉巖套菜,帶你破解...
    沈念sama閱讀 219,427評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件心褐,死亡現(xiàn)場離奇詭異,居然都是意外死亡笼踩,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,551評論 3 395
  • 文/潘曉璐 我一進(jìn)店門亡嫌,熙熙樓的掌柜王于貴愁眉苦臉地迎上來嚎于,“玉大人,你說我怎么就攤上這事挟冠∮诠海” “怎么了?”我有些...
    開封第一講書人閱讀 165,747評論 0 356
  • 文/不壞的土叔 我叫張陵知染,是天一觀的道長肋僧。 經(jīng)常有香客問我,道長控淡,這世上最難降的妖魔是什么嫌吠? 我笑而不...
    開封第一講書人閱讀 58,939評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮掺炭,結(jié)果婚禮上辫诅,老公的妹妹穿的比我還像新娘。我一直安慰自己涧狮,他們只是感情好炕矮,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,955評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著者冤,像睡著了一般肤视。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上涉枫,一...
    開封第一講書人閱讀 51,737評論 1 305
  • 那天邢滑,我揣著相機(jī)與錄音,去河邊找鬼拜银。 笑死殊鞭,一個胖子當(dāng)著我的面吹牛遭垛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播操灿,決...
    沈念sama閱讀 40,448評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼锯仪,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了趾盐?” 一聲冷哼從身側(cè)響起庶喜,我...
    開封第一講書人閱讀 39,352評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎救鲤,沒想到半個月后久窟,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,834評論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡本缠,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,992評論 3 338
  • 正文 我和宋清朗相戀三年斥扛,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片丹锹。...
    茶點(diǎn)故事閱讀 40,133評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡稀颁,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出楣黍,到底是詐尸還是另有隱情匾灶,我是刑警寧澤,帶...
    沈念sama閱讀 35,815評論 5 346
  • 正文 年R本政府宣布租漂,位于F島的核電站阶女,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏哩治。R本人自食惡果不足惜秃踩,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,477評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望业筏。 院中可真熱鬧吞瞪,春花似錦、人聲如沸驾孔。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,022評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽翠勉。三九已至妖啥,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間对碌,已是汗流浹背荆虱。 一陣腳步聲響...
    開封第一講書人閱讀 33,147評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人怀读。 一個月前我還...
    沈念sama閱讀 48,398評論 3 373
  • 正文 我出身青樓诉位,卻偏偏與公主長得像,于是被迫代替她去往敵國和親菜枷。 傳聞我的和親對象是個殘疾皇子苍糠,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,077評論 2 355

推薦閱讀更多精彩內(nèi)容