nodejs使用express-session做登录状态保持

1,287 阅读5分钟

关于nodejs使用express-session做跨页面登录状态保存的总结

  1. 前端使用jQuery的ajax
  2. 后端使用nodejs和express框架
  3. 跨域使用了cors
  4. 数据库用mysql
  5. 关键点:做跨域处理的时候,不但要做到能接口通讯,还要注意令浏览器自动保存登录返回的set-cookie,这样下次访问其他页面才会自动带上cookie.

排查错误的过程:

  1. req.session为undefined,无法判断之前保存在session的isLogin:网上查阅,包括我自己也出现这个情况,后来我发现一个诡异地方:就是登录成功后,后台已经返回set-cookie,为啥我下次请求不带上这个cookie呢?经过网上查阅资料有几个原因会导致:1 cookie过期,浏览器会自动筛选有效期在内的。2 cookie根本没有存在appliacation的本地上,那下次请求肯定就不带上了。于是,我发现我的响应头返回cookie时间为凌晨5点,6点是过期时间,但是接口发出时间是晚上21点,会不会是时区问题导致cookie过期了呢?于是我将cookie过期时间设置为24小时,这样就算是时区问题,依然在有效期呢。可是经过验证,依然下次请求没有带上cookie。再细心一看,原来是本地没有存到cookie,再去查资料,浏览器收到set-cookie后自动保存cookie的前提这就是最关键地方了,见第2点。
  2. 浏览器保存set-cookie的几点要素前提:首先跨域请求,是需要后台设置Access-Control-Allow-Origin,Access-Control-Allow-Credentials,credentials,同时前端也要设置xhrFields: {withCredentials: true},同时Access-Control-Allow-Origin不能为*号,要指定具体的域名。并且前端域名和后端域名是同域,换句话说,你可能解决了跨域,能获取到后台返回的数据了,但是origin不同域,浏览器依然不会自动保存cookie.所以需要满足的条件很多,总的来说就是,前后端都要配置四个字段,并且还要origin域名和后端域名一致。附图如下:

image.png

image.png 这样浏览器终于能自动保存cookie了: image.png 此时登录后,跳到主页也确实自动携带上cookie了:

image.png 接口也如期返回:

image.png

3. 注销操作后,再次手动刷新首页,不能提示重新登录竟然还可以保持登录状态:经过我反复排查,竟然是注销调用接口时,前端没有带上xhrFields: {withCredentials: true}。其实道理和第二点一样。只有配置好了,注销接口才会带上这个cookie.这里可以用两种方法,一种方法 req.session.cookie.maxAge = 0,通过有效期过期给浏览器带上去,会发现点击注销的一瞬间,浏览器本地cookie就消失了(消失原因是后台响应头返回了最新的过期时间,浏览器判断已经过期就自动删除本地cookie了,所以推测如果不过期就会更新本地的cookie),此时提示重新登录。另一种方法req.session.destroy,销毁会话,也需要通过浏览器携带cookie,所以两种方法都需要配置前端配置withCredentials为true。区别只是后者,可以发现浏览器没有清空cookie,是因为后台没有返回set-cookie这个字段了但是后台自己已经销毁了cookie,所以保存在session的isLogin自然就为false。

ps:登录接口,首页查询用户接口,注销接口,每次都要前端带上withCredentials也挺麻烦的,用axios,统一配置,省了粗心大意出错,避免不必要的时间成本

话不多说:nodejs代码

const express = require('express')
const Router = require('./router')
const cors = require('cors')
const session = require('express-session')
const app = express()

app.use(express.json())
app.use(express.urlencoded({
  extended: false
}))
app.use(session({
  secret: 'lernning', //随意填写
  resave: false, //固定写法
  saveUninitialized: false,//true表示,第一次访问页面不需要登录就生成sessionId,
  cookie: {
    maxAge: 1000 * 60 * 60 * 1 , //1小时
  
  },
  rolling:true  //用户最后一次请求开始计算,重新刷新session的有效期,类似淘宝中午不吃饭一直刷,1小时不过期,如果出去午休了,回来再刷新,需要重新登录。

}))


//配置cors中间件,解决跨域资源共享
app.use(cors({//后台启动服务器ip是127.0.0.1
  origin: 'http://127.0.0.1:5500',//这里可以解决跨域问题,但是如果要让浏览器自动保存登录成功后的set-cookie,后台必须和前端同域名,不然浏览器不会保存,除非做代理转发?(转发我没试验过)
  // origin:'http://localhost:52330',//虽然可以解决跨域,可以进行请求的接收,但是浏览器无法保存cookie,下次请求就无法带上
  credentials:true,  //告知前端可以请求头携带cookie请求,同时前端也要设置withCredentials为true
}))


app.use('/api', Router) //注册路由模块

app.listen(80, () => {
  
})

路由route.js

const express = require('express')
const Router = express.Router() //创建路由实例
const mysql = require('mysql')

const db = mysql.createPool({
  host: '127.0.0.1', //数据库的IP地址
  user: 'root', //数据库的账号
  password: '****', //数据库的密码
  database: 'my_db_01', //指定哪个数据库
  port: 3306
})



//登录
Router.post('/login', (req, res) => {
  //判断用户登录信息是否正确
  let userInfo = req.body.username + req.body.password
  let isLogin = false
  const sqlStr = 'select * from users' //当数据字段和数据库中一样
  db.query(sqlStr, (err, sqlRes) => {
    if (err) return console.log(err.message);
    for (const per of sqlRes) {

      isLogin = userInfo === (per.username + per.password) ? true : false
      if (isLogin) break
    }
    if (isLogin) {
      //下面两个操作响应头才会返回set-cookie字段,但浏览器还不会存起来。
      // 要浏览器存起来,需要后端和前端共同设置跨域资源共享cors,并且同域名
      req.session.user = req.body //将用户的信息储存到session中
      req.session.isLogin = true //储存用户的登录状态
      console.log('登录', req.session);
      res.send({
        code: 200,
        msg: '登录成功',
        data: req.body.username
      })
    } else {
      res.send({
        code: -1,
        msg: '登录失败'
      })
    }
  })
})

//注销
Router.post('/logout', (req, res) => {  
  //方法1:清空当前客户端对应的
  // req.session.cookie.maxAge = 1000
  // res.send({
  //   code: 200,
  //   msg: '注销成功'
  // })

//方法2:express-session包的destory方法
  req.session.destroy((err) => {
    if (err) {
      res.send('注销失败')
      return 
    }

    res.send({
      code: 200,
      msg: '注销成功'
    })

  })


})


//查询用户
Router.get('/searchUser', (req, res) => {

  console.log('查询', req.session);
  if (!req.session.isLogin) {
    res.send({
      code: -1,
      msg: '你尚未登录,请先登录'
    })
    return
  }
  res.send({
    code: 200,
    data: req.session.user.username,
    msg: '查询用户信息成功'
  })
})




module.exports = Router

前端代码:login.html

<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <script src="../jquery.js"></script>
 
</head>

<body>
  <form action="" id="loginForm">
    用户名:<input type="text" name="username">
    密码:<input type="password" name="password">
    <button type="submit">登录</button>
  </form>



  <script>
    $(document).ready(function () {//在DOM元素被加载完成的情况下执行
      $('#loginForm').on("submit", (ev) => {
        //阻止浏览器跳转     
        ev.preventDefault();
        // 自己发请求
        let loginPrams = {}
        const formData = new FormData($('#loginForm')[0]);
        formData.forEach((val, key) => {
          // console.log(val,key)  val-值,key-表单的name
          loginPrams[key] = val
        })
        $.ajax({
          type: 'POST',
          url: 'http://127.0.0.1/api/login',
          data: loginPrams,
          xhrFields: {
            withCredentials: true  //必须和后端配合设置origin,credentials,且origin同域才能浏览器自动保存
          },
          success: function (res) {
            // console.log(res)
            if (res.code === 200 && res.msg === '登录成功') {
              alert('欢迎你,' + res.data)
              window.location.href = './index.html';
            }
            if(res.code===-1){
              alert('登录失败')
            }
          }
        })
      })
    });

  </script>
</body>

</html>

首页:index.html

<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <script src="../jquery.js"></script>
</head>

<body>
  <h1>首页</h1>
  <button id="getBtn">退出登录</button>


  <script>


    $(document).ready(function () {
      $.ajax({
        type: 'get',
        url: "http://127.0.0.1/api/searchUser",
        xhrFields: {
          withCredentials: true
        },
        success(res) {

          if (res.code === -1) {
            alert('请先登录')
            window.location.href = './login.html';
          }
        }

      })
      //登出
      $('#getBtn').on('click', function () {
        $.ajax({
          url: 'http://127.0.0.1/api/logout',
          type: 'post',
          xhrFields: {
            withCredentials: true
          },
          success(res) {
            // window.location.href='./login.html';
          }
        })
      })
    })
    this
  </script>
</body>

</html>