前言
不管是企业项目,还是个人项目,一个优秀的系统必须具有鉴权的能力,何为鉴权,是指验证用户是否拥有访问系统的权利。
前端鉴权的方式也很多,例如 HTTP Basic Authentication、session-cookie、OAuth(开放授权)、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
鉴权方案涉及前后端的一个配合,比较适合企业级,个人级的项目开发,权限认证较严格,所涉及的技术栈也比较简单,特别适合新手练手,打造一个自己的鉴权系统
如果大家喜欢,欢迎点个赞 👍