前言
本文是我对自己学习的一个总结,我只是一名菜鸟,如果您有更好的方案或者意见请务必指正出来。我的?:1025873823
session鉴权
什么是session?
session是一种服务器机制,是存储在服务器上的信息。存储方式多种多样,可以是服务器的内存中,或者是mongo数据库,redis内存数据库中。而session是基于cookie实现的(服务器会生成sessionID)通过set-cookie的方式写入到客户端的cookie中。每一次的请求都会携带服务器写入的sessionID发送给服务端,通过解析sessionID与服务器端保存的session,来判断用户是否登录。
鉴权步骤如下:
- 客户端发起登录请求,服务器端创建session,并通过set-cookie将生成的sessionID写入的客户端的cookie中。
- 在发起其他需要权限的接口的时候,客户端的请求体的Header部分会携带sessionID发送给服务端。然后根据这个sessionId去找服务器端保存的该客户端的session,然后判断该请求是否合法。
session鉴权的示例
基于Passport和Express的实现,Passport的详细文档请参考,我这篇文章只是使用的介绍,更详细的方法是阅读文档。
跨域的解决
由于是前端分离的项目,前端的静态资源服务和后端的接口可能不在同一个域名下,这就导致了服务器无法在浏览器上写入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上的解答
Passport local本地验证
环境配置
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方法进行密码校验操作。
done方法是由passport提供的,用于回调操作的方法。对于不同的结果执行不同的回调操作。
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)
})
}))
复制代码
session序列化与反序列化
serializeUser序列化,将用户信息存储到session中,这段信息即是sessionID,同时会将sessionID存储到客户端的cookie中的过程。
deserializeUser反序列化,参数是用户提交的sessionID,如果存在则从数据库中查询user并存储与req.user中。
passport.serializeUser((user, done) => {
done(null, user.id)
})
passport.deserializeUser((id, done) => {
User.findById(id, (err, user) => {
done(null, user)
})
})
复制代码
??对于这段代码具体实现的细节,我一开始也明白。有一天我在stackoverflow上找到解答Understanding passport serialize deserialize
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
});
复制代码
logIn, logOut, isAuthenticated
passport为request对象扩展的方法
- logIn(), 用户登陆操作,即初始化session
- logOut(), 用户登出操作,删除用户的session信息
- isAuthenticated(), 用来判断用户是否登陆
接口示例
// 用户登录
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: '没有权限' })
}
复制代码
jwt鉴权
什么是jwt?
Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC 7519).该token被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。
jwt鉴权的流程
鉴权的流程:
- 浏览器发起登录请求
- 请求通过后,服务器会向浏览器返回token
- 浏览器接收到token后需要讲token保存到本地(比如localStorage)
- 浏览器在下一次请求的时候会携带token信息
- 服务器收到请求,去验证token验证成功后会返回信息
乍一看,token是类似sessionID的存在。其实token和sessionID还是有一定的不同的。sessionID是基于cookie实现的,而token不需要基于cookie。这就导致了sessionID只能用在浏览器上,对于原生的应用无法实现。原生的应用是不具备cookie的特性的。另外sessionID可以实现服务端注销会话,而token不能(当然你可以把用户登陆的token存入到redis中,但是不推荐token入库)
jwt鉴权的示例
示例代码我是基本照抄这一篇教程,英文好的同学推荐阅读原版Authenticate a Node.js API with JSON Web Tokens
CORS设置
由于我们需要通过请求体的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后,会交由浏览器端管理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})
})
})
复制代码