登陸認(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ā)布起意!