本文是我对本身学习的一个总结,我只是一名菜鸟,若是您有更好的方案或者意见请务必指正出来。个人🐧:1025873823javascript
session是一种服务器机制,是存储在服务器上的信息。存储方式多种多样,能够是服务器的内存中,或者是mongo数据库,redis内存数据库中。而session是基于cookie实现的(服务器会生成sessionID)经过set-cookie的方式写入到客户端的cookie中。每一次的请求都会携带服务器写入的sessionID发送给服务端,经过解析sessionID与服务器端保存的session,来判断用户是否登陆。前端
鉴权步骤以下:java
基于Passport和Express的实现,Passport的详细文档请参考,我这篇文章只是使用的介绍,更详细的方法是阅读文档。node
因为是前端分离的项目,前端的静态资源服务和后端的接口可能不在同一个域名下,这就致使了服务器没法在浏览器上写入cookie。须要经过设置CORS解决。先后端都须要额外的设置,代码以下
// 基于axios代码设置以下, withCredentials: true是否容许跨域修改cookie const Axios = axios.create({ baseURL: 'http://127.0.0.1:3000/', timeout: 1000, withCredentials: true, responseType: "json", headers: { "Content-Type": "application/x-www-form-urlencoded;charset=utf-8" } })
// 使用CORS模块,并配置容许跨域请求 app.use(cors({ origin: 'http://127.0.0.1:8080', credentials: true, methods: ['PUT', 'POST', 'GET', 'DELETE', 'OPTIONS'], allowedHeaders: ['Content-Type', 'Content-Length', 'Authorization', 'Accept', 'X-Requested-With'] }))
⚠️:这里有一个坑,origin不能设置为通配符*,stackoverflow上的解答ios
const session = require('express-session') const MongoStore = require('connect-mongo')(session) const bodyParser = require('body-parser') const passport = require('passport') app.use(bodyParser.urlencoded({ extended: false })) app.use(bodyParser.json()) // 这里我将session存储到mongo中,更好的作法是存储到redis中 app.use(session({ resave: false, saveUninitialized: false, secret: sessionConfig.secret, store: new MongoStore({ mongooseConnection: connection }), cookie: { maxAge: 60 * 1000 * 30 } })) app.use(passport.initialize()) app.use(passport.session())
local验证默认使用密码和用户名验证,首先须要对local策略作出配置。如下是官方示例给出的代码,我直接copy过来使用。代码很是简单。User是Mongoose的Model(须要本身建立),经过findOne方法查找用户名对应的用户,并对查找的结果做出判断,并经过调用passport的done方法做出验证回调。因为User密码不是明文存储的,经过了bcrypt模块进行了加密。因此须要经过bcrypt.compare方法进行密码校验操做。web
done方法是由passport提供的,用于回调操做的方法。对于不一样的结果执行不一样的回调操做。redis
const passport = require('passport') const LocalStrategy = require('passport-local').Strategy const User = require('../models/user') const bcrypt = require('../util/bcrypt') passport.use(new LocalStrategy((username, password, done) => { User.findOne({ username }, (err, user) => { if (err) return done(err) if (!user) { return done(null, false, { message: '用户名不存在' }) } if (!bcrypt.compare(password, user.password)) { return done(null, false, { message: '用户名或密码错误' }) } return done(null, user) }) }))
serializeUser序列化,将用户信息存储到session中,这段信息便是sessionID,同时会将sessionID存储到客户端的cookie中的过程。数据库
deserializeUser反序列化,参数是用户提交的sessionID,若是存在则从数据库中查询user并存储与req.user中。express
passport.serializeUser((user, done) => { done(null, user.id) }) passport.deserializeUser((id, done) => { User.findById(id, (err, user) => { done(null, user) }) })
😖😢对于这段代码具体实现的细节,我一开始也明白。有一天我在stackoverflow上找到解答Understanding passport serialize deserializenpm
Q: serializeUser 作了什么?
A: 经过done将user存储到了session中, 并将sessionID写入到客户端的cookie上, 将用户信息附加到请求对象req.session.passport.user上。
Q:deserializeUser 作了什么?
A:deserializeUser的第一个参数就是你存储的sessionID,经过Model的findById方法查找数据库,并将用户信息附加到请求对象req.user上
passport.serializeUser(function(user, done) { done(null, user.id); | }); | | |____________________> saved to session req.session.passport.user = {id:'..'} | \|/ passport.deserializeUser(function(id, done) { ________________| | \|/ User.findById(id, function(err, user) { done(err, user); |______________>user object attaches to the request as req.user });
passport为request对象扩展的方法
// 用户登陆 router.get('/login', (req, res, next) => { // 登陆认证,使用local策略 passport.authenticate('local', (err, user, info) => { if (err) return next(err) if (!user) return res.status(400).json({ message: info.message }) // 初始化session信息 req.logIn(user, (err) => { if (err) return next(err) res.status(200).json({ code: 200, message: '登录成功' }) }) })(req, res, next) }) // 用户登出 router.get('/logout', isAuthenticated, (req, res) => { // 删除mongo中的session信息 req.logout() res.status(200).json({ code: 200, message: '登出成功' }) }) // 用户详情(须要权限的接口) // isAuthenticated是经过passport提供的isAuthenticated()封装的简单中间件 // 添加isAuthenticated接口则是须要登录权限的接口 router.get('/details', isAuthenticated, (req, res) => { const { _id } = req.user UserService.getUserDetail(_id).then(data => { res.status(200).json({ code: 200, message: 'success', data }) }).catch(err => { res.status(400).json({ message: '用户信息不存在' }) }) })
module.exports = function isAuthenticated (req, res, next) { if (req.isAuthenticated()) return next() res.status(403).json({ message: '没有权限' }) }
Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC 7519).该token被设计为紧凑且安全的,特别适用于分布式站点的单点登陆(SSO)场景。JWT的声明通常被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也能够增长一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。
鉴权的流程:
乍一看,token是相似sessionID的存在。其实token和sessionID仍是有必定的不一样的。sessionID是基于cookie实现的,而token不须要基于cookie。这就致使了sessionID只能用在浏览器上,对于原生的应用没法实现。原生的应用是不具有cookie的特性的。另外sessionID能够实现服务端注销会话,而token不能(固然你能够把用户登录的token存入到redis中,可是不推荐token入库)
示例代码我是基本照抄这一篇教程,英文好的同窗推荐阅读原版 Authenticate a Node.js API with JSON Web Tokens
因为咱们须要经过请求体的headers传递token,因此咱们须要对CORS模块进行额外的配置,代码以下
// 咱们将会经过headers的x-access-token字段向服务端传递token app.use(cors({ origin: 'http://127.0.0.1:8080', credentials: true, methods: ['PUT', 'POST', 'GET', 'DELETE', 'OPTIONS'], allowedHeaders: [ 'Content-Type', 'Content-Length', 'Authorization', 'Accept', 'X-Requested-With', 'x-access-token'] }))
咱们须要下载如下的依赖包, 以及用于加密token的字符串secret(能够是一个随机字符串)
npm install --save jsonwebtoken
咱们一般经过登录操做获取token,服务端生成token后,会交由浏览器端管理token
const jwt = require('jsonwebtoken') const User = require('../models/user') const bcrypt = require('../util/bcrypt') const secret = require('../config/index').secret // ... login (name, password) { return new Promise((resolve, reject) => { User.findOne({ name: name }, (err, user) => { if (err) return reject(err) if (!user) return reject('用户名不存在') // bcrypt用于加密的包 if (!bcrypt.compare(password, user.password)) return reject('用户名或密码错误') // 根据id信息以及secret生成,对应的token,并设置token的过时时间 const token = jwt.sign({ id: user._id }, secret, { expiresIn: 60 * 60 // token过时时间 }) // 返回token resolve(token) }) }) }
有一些API接口,将会受到token的保护,若是请求没有包含token信息,请求将会失败。咱们这里将会封装一个中间件,帮助咱们用来判断请求是否包含token信息,以及token信息是否过时,代码以下
const jwt = require('jsonwebtoken') const secret = require('../config/index').secret module.exports = function (req, res, next) { // 获取请求的token信息 const token = req.body.token || req.query.token || req.headers['x-access-token'] if (token) { // 检验token信息是否过时 jwt.verify(token, secret, function(err, decoded) { if (err) { return res.status(403).json({ code: 'error', error: 'token失效' }) } else { req.decoded = decoded next() } }) } else { res.status(403).json({code: 'error', error: '没有权限'}) } }
接下来咱们将封装的中间件,应用到咱们的接口中。在这里,获取所有用户信息的接口将会收到token的保护,若是不包含token,将会返回403错误
const express = require('express') const router = express.Router() const UserService = require('../service/user.service') const AuthenticationToken = require('../middleware/AuthenticationToken') // AuthenticationToken中间件保护/users接口 router.get('/users', AuthenticationToken, (req, res) => { UserService.users().then(result => { res.status(200).json({code: 'ok', data: result}) }).catch(error => { res.status(500).json({code: 'error', error}) }) })