nodejs使用express-session中间件
关于req.session无法获取,跨页面登录状态保存的总结
- 前端使用jQuery的ajax
- 后端使用nodejs和express框架
- 跨域使用了cors
- 数据库用mysql
- 关键点:做跨域处理的时候,不但要做到能接口通讯,还要注意令浏览器自动保存登录返回的set-cookie,这样下次访问其他页面才会自动带上cookie.
排查错误的过程:
- 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点。
- 浏览器保存set-cookie的几点要素前提:首先跨域请求,是需要后台设置Access-Control-Allow-Origin,Access-Control-Allow-Credentials,credentials,同时前端也要设置xhrFields: {withCredentials: true},同时Access-Control-Allow-Origin不能为*号,要指定具体的域名。并且前端域名和后端域名是同域,换句话说,你可能解决了跨域,能获取到后台返回的数据了,但是origin不同域,浏览器依然不会自动保存cookie.所以需要满足的条件很多,总的来说就是,前后端都要配置四个字段,并且还要origin域名和后端域名一致。附图如下:
- 注销操作后,再次手动刷新首页,不能提示重新登录竟然还可以保持登录状态:经过我反复排查,竟然是注销调用接口时,前端没有带上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>
本文章(和掘金作者为同一人):express-session登录状态保持