深入浅出 Express 中间件 Express-session

深入浅出 Express 中间件 Express-session

仓库地址入口: gitee.com/ayachensiyuan

顾名思义,这个中间件是解决session的问题。通常用户的状态会用两种形式保存:

  1. 通过cookie将数据保存在在客户端中,也就是在浏览器 。这种形式的特点是轻量级,容易实现也容易复制,因为cookie是保存在客户端,你可以拷贝cookie骗过服务器来实现登录验证,只要是学过web的大学生基本都能办到,很多爬虫也就是利用cookie来模拟登录状态。所以这种完全交给用户来保管关键数据的方式逐渐被后面一种形式取代。
  2. 将数据以session的形式保存在服务端,通过cookie返回给客户端session identifier存储在客户端。也就是客户端只有钥匙,信息都是在服务器上,服务器可以更安全的验证用户。当然没有绝对的安全,毕竟钥匙也能伪造的嘛,只是相对于cookie来说,session的安全性更高。但session也有本身的问题,当用户数据变得庞大,势必会对服务器的数据储存和读取产生压力,而且如今手机app的普及,单页面应用(SPA)趋于主流化,更多需求是用到api的调用,在处理这些授权有一种更好的技术—使用token验证可以取代session。但session轻量级在网站桌面应用还是可以胜任的。

Express-session这个中间件组件替代cookie-parser和cookie-session中间件成为处理用户状态的首选。

什么是cookie

cookie是一种客户端的状态管理技术

当浏览器向服务器发送请求时,服务器会将少量的数据以set-cookie消息头的方式发送给浏览器

浏览器会将这些数据保存下来。当浏览器再次访问服务器时,会将这些数据以cookie消息头的方式发送给服务器。

什么是session

session是一种服务器端的状态管理技术。

当浏览器访问服务器时,服务器创建一个session对象(该对象有一个唯一的id号,称之为sessionId),服务器在默认情况下,会将sessionId以cookie的方式(set-cookie消息头)发送给浏览器,浏览器会将sessionId保存到内存。当浏览器再次访问服务器时,会将sessionId发送给服务器,服务器依据sessionId就可找到之前创建的session对象。

实例

需要安装express-session中间件

> npm install express-session


const session = require('express-session')
const express = require('express')
const fs = require('fs')

const app = express()

app.use(session({
    secret: 'keyboard cat',
    resave: false,
    saveUninitialized: true,
    cookie: { secure: false, maxAge: 800000 },
    name: 'ivan'
}))

app.get('/', (req, res) => {
    const html = fs.readFileSync('./views/index.html', 'utf-8')
    const session = req.session  // 获得session
    session['key'] = 'value'  // 设置session
    console.log(session)
    res.setHeader('set-cookies', session['key']) // 保存cookie在headers中(这里修改了session,服务器会自动生成set-cookie字段)
    res.end(html)  // 发送给客户端
})

app.listen(3000)

页面的js代码

以上配置下的cookie获取情况



客户端


服务端


配置

session(options)

使用options的配置创建一个cookie会话,session数据不保存在 cookie 中,只保存在会话 ID 中。 session数据存储在服务器端。

这个中间件会将session添加到req中,根据这个标识,当页面重新加载就可以辨认是不是需要新的请求。

如果req.session被手动更改,这个中间件就会自动添加set-cookie请求头,方便用户识别。

参数

  cookie: {
  // Cookie Options
  // 默认为{ path: '/', httpOnly: true, secure: false, maxAge: null }
   /** maxAge: 设置给定过期时间的毫秒数(date)
  * expires: 设定一个utc过期时间,默认不设置,http>=1.1的时代请使用maxAge代替之(string)
  * path: cookie的路径(默认为/)(string)
  * domain: 设置域名,默认为当前域(String)
  * sameSite: 是否为同一站点的cookie(默认为false)(可以设置为['lax', 'none', 'none']或 true)
  * secure: 是否以https的形式发送cookie(false以http的形式。true以https的形式)true 是默认选项。 但是,它需要启用 https 的网站。 如果通过 HTTP 访问您的站点,则不会设置 cookie。 如果使用的是 secure: true,则需要在 express 中设置“trust proxy”。
  * httpOnly: 是否只以http(s)的形式发送cookie,对客户端js不可用(默认为true,也就是客户端不能以document.cookie查看cookie)
  * signed: 是否对cookie包含签名(默认为true)
  * overwrite: 是否可以覆盖先前的同名cookie(默认为true)*/
  },
    
  // 默认使用uid-safe这个库自动生成id
  genid: req => genuuid(),  
    
  // 设置会话的名字,默认为connect.sid
  name: 'value',  
  
  // 设置安全 cookies 时信任反向代理(通过在请求头中设置“X-Forwarded-Proto”)。默认未定义(boolean)
  proxy: undefined,
    
  // 是否强制保存会话,即使未被修改也要保存。默认为true
  resave: true, 
    
  // 强制在每个响应上设置会话标识符 cookie。 到期重置为原来的maxAge,重置到期倒计时。默认值为false。
  rolling: false,
    
  // 强制将“未初始化”的会话保存到存储中。 当会话是新的但未被修改时,它是未初始化的。 选择 false 对于实现登录会话、减少服务器存储使用或遵守在设置 cookie 之前需要许可的法律很有用。 选择 false 还有助于解决客户端在没有会话的情况下发出多个并行请求的竞争条件。默认值为 true。
  saveUninitialized: true,
    
  // 用于生成会话签名的密钥,必须项  
  secret: 'secret',
  
  // 会话存储实例,默认为new MemoryStore 实例。
  store: new MemoryStore(),
  
  // 设置是否保存会话,默认为keep。如果选择不保存可以设置'destory'
  unset: 'keep'


API

req.session

要存储或访问会话数据,只需使用请求属性 req.session,它以JSON的形式存储序列化,对JS开发非常友好。

方法

.regenerate(callback)

要重新生成会话,只需调用该方法。 完成后,将在 req.session 处初始化一个新的 SID 和 Session 实例,并调用回调。

.destroy(callback)

销毁会话并取消设置 req.session 属性。 完成后,将调用回调。

.reload(callback)

从存储重新加载会话数据并重新填充 req.session 对象。 完成后,将调用回调。

.save(callback)

将会话保存回 store,用内存中的内容替换 store 上的内容。

如果会话数据已更改,则在 HTTP 响应结束时自动调用此方法。

在某些情况下调用此方法很有用,例如重定向、long-lived请求或在 WebSockets 中。

.touch()

更新 .maxAge 属性。 通常不需要调用,因为会话中间件会为您执行此操作。

属性

.id

每个会话都有一个与之关联的唯一 ID。 该属性是 req.sessionID 的别名,不能修改。 添加它是为了使会话 ID 可从会话对象访问。

.cookie

每个会话都有一个唯一的 cookie 对象。 这允许您更改每个访问者的会话 cookie。 例如,我们可以将 req.session.cookie.expires 设置为 false 以使 cookie 仅在用户代理的持续时间内保留。

.Cookie.maxAge

req.session.cookie.maxAge 将返回以毫秒为单位的剩余时间,我们也可以调整 req.session.cookie.expires 属性,expires是返回Date()对象。安全性上说使用maxAge更好,过期时间是服务器给的,倒计时一过自动就没了。而expires是写死的时间,很容易修改浏览器的时间达到骗过有效期的目的。

.Cookie.originalMaxAge

属性返回会话 cookie 的原始 maxAge,以毫秒为单位。

req.sessionID

要获取已加载会话的 ID,请访问请求属性 req.sessionID。 这只是在加载/创建会话时设置的只读值。


Store

前面提到服务器会保存session,那具体保存在哪里呢?在配置session选项中有个store,如果不指定的话,默认会使用new MemoryStore()保存在内存中。内存有个特点就是断电或服务器重启数据就没了,所以通常我们可以指定其他的store中间件来保存session,比如file-store,或是数据库redis等等。如果要查看默认的store的话,你可以提前先创建一个变量,当store有了名字,就可以后面使用store的api来调用了。

const store = new MemoryStore() // 创建个MemoryStore实例
app.use(session({
    ...
    store
}))

app.use((req, res, next) => {
  store.get(req.sessionId, (err, session) => {
    // 这里就可以操作内存中的store数据了。
  })
})

store.all(callback)

此可选方法用于将存储中的所有会话作为数组获取。 callback中第一个为error,第二个是sessions。

store.destroy(sid, callback)

这个必需的方法用于在给定会话 ID (sid) 的情况下从存储中销毁/删除会话。 callback的对象为error。

store.clear([callback])

此方法用于从存储中删除所有会话.callback的对象为error。

store.length(callback)

此方法用于获取商店中所有会话的数量。 callback中第一个为error,第二个是len。

store.get(sid, callback)

这个方法第一个参数为会话 ID (sid) 。 callback中第一个为error,第二个是session。

找不到不会错误,而是在session返回null 或 undefined。

store.set(sid, session, callback)

这个方法用于新建或修改session 保存在store中。 callback的对象为error。

store.touch(sid, session, callback)

这个方法用给定会话 ID (sid) 和会话 (session)来“touch”对应的session。callback的对象为error。

这主要用于当存储将自动删除空闲会话并且此方法用于向存储发出信号给定会话处于活动状态时,可能会重置空闲计时器。

可适用的store中间件

常用的store

connect-mongodb-session Lightweight MongoDB-based session store built and maintained by MongoDB.

connect-redis A Redis-based session store.

express-mysql-session A session store using native MySQL via the node-mysql module.

express-sessions A session store supporting both MongoDB and Redis.

session-file-store A file system-based session store.

应用

实现一个标准化验证登录,用户信息保存流程,实现方式就是使用session技术

需要用到的技术

  • javascript
  • node.js
    • fs
    • Http-error
  • express
    • express-session
    • session-file-store

目录结构

| + node-modules
| package.json
| package-lock.json
| - router
|   | userRouter.js
|   | strictRouter.js
|   | user.json 
| app.js

这里的strictRouter是处理需要授权的页面只有授权后才能访问。

userRouter就是让用户注册和登录使用。

user.json 是代替数据库保存用户信息

网站登录逻辑



路由设计

我这里就不写页面了,使用postman来测试代码。


app.js

// app.js
const express = require('express')
const session = require('express-session')
const createError = require('http-errors')  

// fileStore中间件可以把用户的session数据保存在文件中。默认配置当收到session时候会自动在根目录下创建sessions文件夹,在其中以json的形式保存收到的用户session。
const FileStore = require('session-file-store')(session)
const app = express()

// 使用session的必须配置
app.use(session({
    secret: '12345-67890-09876-54321', // 必选配置
    resave: false, //必选,建议false,只要保存登录状态的用户,减少无效数据。
    saveUninitialized: false, //必选,建议false,只要保存登录状态的用户,减少无效数据。
    cookie: { secure: false, maxAge: 800000, httpOnly: false }, // 可选,配置cookie的选项,具体可以参考文章的配置内容。
    name: 'session-id', // 可选,设置个session的名字
    store: new FileStore() // 可选,使用fileStore来保存session,如未指定就会默认使用memoryStore
}))

// 以下时解析post数据用到的中间件,使用后可以在req.body获取用户的post数据
app.use(express.json())
app.use(express.urlencoded({ extended: true }))


// 首页路由,无论谁都可以访问
app.get('/', (req, res)=> {
    res.end('welcome to my homepage')
})

// 用户路由入口
app.use('/user', require('./router/userRouter'))

// 授权中间件,在这个之后的路由,除了错误处理,都是需要授权的。
app.use((req,res,next)=> {
    if (req.session.status) next() // 如果有授权就能next下去,“status“是授权后会产生的,自己设定的,可以按照自己的习惯去设定一个名字
    else next(createError(401)) // 如果未授权就返回错误码
})

// 需要授权的页面路由
app.use(require('./router/strictRouter'))


// 错误处理中间件
// 处理用户无效的路由返回404,通常写在路由的最后,当之前的路由都不能匹配就匹配404
app.use((req, res, next) => {
    next(createError(404))
})

// 根据状态码返回err的信息给用户,没有状态码就返回500服务器错误信息
app.use((err, req, res,next)=> {
    res.status(err.status || 500)
    res.end(err.toString())
})

app.listen(3000)

userRouter.js

// userRouter.js
const express = require('express')
const fs = require('fs')
const userRouter = express.Router()
const createError = require('http-errors')

userRouter.route('/login')
    // 登录验证路由
    .post((req, res, next) => {
        // 获取用户输入数据
        const user = req.body
        // 查看是否有用户
        fs.readFile('./router/user.json', 'utf-8', (err, data) => {
            if (err) next(createError('no user'))
            else {
                const users = JSON.parse(data)
                // 查找文件匹配用户
                const DbUser = users.filter(v => v.name === user.name)
                if (DbUser.length === 0) next(createError(403, 'no user')) // 处理没有存在用户的情况
                else if (DbUser[0].pwd !== user.pwd) next(createError(403, 'wrong password'))// 处理密码错误的情况
                else {
                  // 一切正常的情况
                  // 写入session,创建status的属性用来判断用户是否登录
                    req.session.status = 'login success'
                    res.end('login success!')
                }
            }
        })
    })

    // 登录页面
    .get((req, res, next) => {
        // 如果登录就重定向到首页
        if (req.session.status) {
            res.redirect('/')
        } else res.end('welcome to login')

    })


userRouter.route('/signup')
    // 用户注册验证
    .post((req, res, next) => {
 const user = req.body
        // 查看是否有同名用户
        // 有的话返回错误
        // 如果没有就可以注册
        fs.readFile('./router/user.json', 'utf-8', (err, data) => {
            if (err) next(createError('no file'))
            else {
                const users = JSON.parse(data)
                const DbUser = users.filter(v => v.name === user.name)
                console.log(DbUser)
                if (DbUser.length !== 0) next(createError(403, '已存在用户'))
                else {
      users.push(user)
                    fs.writeFile('./router/user.json', JSON.stringify(users), (err, result)=> {
                        if (err) next(createError('注册失败!'))
                        else res.end('注册成功!')
                    }) 
                    })
                    
                }
            }
        })
    })

    // 注册页面
    .get((req, res) => {
        if (req.session.status) {
            res.redirect('/')
        } else res.end('welcome to signup')

    })

// 退出登录状态页面
userRouter.route('/logout')
    .get((req, res) => {
        req.session.status = null
        res.redirect('/')
    })
module.exports = userRouter

strictRouter.js

// strictRouter.js
const express = require('express')
const docRouter = express.Router()

strictRouter.route('/document')
    .get((req, res, next) => {
        res.end('docpage')
    })

strictRouter.route('/user')
    .get((req, res, next) => {
        res.end('userpage')
    })

module.exports = strictRouter

完成测试

首页


未登录状态访问授权页面


用户注册:注册已存在的用户


用户注册:成功注册


使用未存在的用户登录


使用错误密码登录


登录后访问授权页面


目录结构下filestore自动保存的session


总结

使用session中间件快速解决用户状态,管理隐私信息。在桌面级应用中还是很实用的一个方法。

之后会介绍一个passport中间件,和app常用的JWT验证的使用。

发布于 2021-09-13 13:38