问题
我有两个express节点应用程序——一个纯粹充当REST服务器,另一个是使用ejs的前端,并使用axios向后端发送请求。我正试图在我的开发环境中实现这一点,因为这两个应用程序都在本地主机上运行。
我正在尝试登录http://localhost:3000/login
,它将用户名/密码作为POST请求发送到http://localhost:4000/login
,并且正在为令牌正确设置Set-Cookie
标头:
'set-cookie': 'token=[redacted]; Max-Age=28800; Domain=localhost; Path=/; Expires=Wed, 05 Apr 2023 20:42:47 GMT; HttpOnly; SameSite=Strict',
但是,当成功后重定向到http://localhost:3000/transactions
页面时,cookie不会被发送,因此身份验证失败。
我尝试过的
这段代码使用Postman处理cookie,这让我认为这是Chrome的安全更改和/或CORS问题,尽管我在Chrome控制台中没有收到任何CORS错误消息。
其他stackoverflow问题似乎证实了这一理论,但我仍然无法成功地将cookie和后续请求发送到服务器(即在下面的/transactions
调用中)
我的服务器端CORS配置现在是这样的,我想我已经涵盖了上面问题中的所有基础,但我仍然怀疑我的问题是:
app.use(cors({
origin: ['http://127.0.0.1:3000','http://localhost:3000'],
methods: ['POST', 'PUT', 'GET', 'OPTIONS', 'HEAD'],
credentials: true,
allowedHeaders: ['Content-Type', 'Authorization', 'x-csrf-token'],
exposedHeaders: ['*', 'Authorization', "X-Set-Cookie"]
}));
- 我知道我需要用凭据发送请求,我在每个请求中都尝试过,作为默认值,以防出现axios问题
- 我知道浏览器对localhost有特殊的处理,在某些情况下它被视为一个安全的环境,但我不清楚设置安全cookie的情况是否如此——我已经将
secure: true/false
和sameSite: "Strict","None"
的值切换到了它们的所有组合,但没有更改 - 我尝试将所有内容(服务器和客户端)切换为使用
127.0.0.1
而不是localhost
,但没有任何更改 - 我尝试使用
mkcert
安装本地SSL证书,以确保可以正确设置secure: true
和sameSite: "None"
,但没有更改。注意,我在发布下面的代码之前删除了这段代码,因为其他人似乎已经在不需要本地主机HTTPS的情况下完成了这项工作,所以我放弃了它,也没有将其包含在下面的代码中
如有任何关于下一步尝试的想法,我们将不胜感激。
扩展代码
服务器-http://localhost:4000/index.js
require('dotenv').config({path: './.env'});
const express = require('express');
const logger = require('morgan');
const expressSanitizer = require('express-sanitizer');
const cors = require('cors');
const cookieParser = require('cookie-parser');
let app = express();
app.use(cors({
origin: ['http://127.0.0.1:3000','http://localhost:3000'],
methods: ['POST', 'PUT', 'GET', 'OPTIONS', 'HEAD'],
credentials: true,
allowedHeaders: ['Content-Type', 'Authorization', 'x-csrf-token'],
exposedHeaders: ['*', 'Authorization', "X-Set-Cookie"]
}));
app.use(express.urlencoded({ extended: true }));
app.use(express.json());
app.use(expressSanitizer());
app.use(cookieParser());
app.use(logger('dev'));
app.use(require('./routes/general/authentication.js'));
app.use(require('./handlers/authHandler.js'));
app.use(require('./routes/general/generalRoute.js'));
// Start the server
if(!module.parent){
let port = process.env.PORT != null ? process.env.PORT : 4000;
var server = app.listen(port, 'localhost', function() {
console.log(`Server started on port ${port}...`);
});
}
module.exports = app;
authentication.js
process.env.SECURE_COKIE=错误
const express = require('express');
const router = express.Router();
const jwt = require('jsonwebtoken');
const bcrypt = require('bcryptjs');
const db = require('../../database/database');
const { check, validationResult } = require('express-validator');
require('dotenv').config({path: './.env'});
const secret = process.env['SECRET'];
const dbURL = process.env['DB_URL'];
const saltRounds = 10;
router.post('/login', [
check('username').exists().escape().isEmail(),
check('password').exists().escape()
], async(req, res, next) => {
try {
const errors = validationResult(req);
if (!errors.isEmpty()) {
res.statusCode = 400;
return next('Authentication failed! Please check the request');
}
res.setHeader('content-type', 'application/json');
if (req.body.username && req.body.password) {
let dbhash = await db.getHashedPassword(req.body.username);
bcrypt.compare(req.body.password, dbhash, async function(err, result) {
if (err) {
res.statusCode = 400;
res.error = err;
return next('Authentication failed! Please check the request');
}
if (result) {
let userData = await db.getUserAuthData(req.body.username);
if (userData.app_access) {
let token = jwt.sign(
{ user_id: userData.id },
secret,
{ expiresIn: '24h' }
);
res.cookie("token", JSON.stringify(token), {
secure: process.env.SECURE_COOKIE === "true",
httpOnly: true,
withCredentials: true,
maxAge: 8 * 60 * 60 * 1000, // 8 hours
sameSite: "Strict",
domain: "localhost"
});
res.statusCode = 200;
res.json({
success: true,
response: 'Authentication successful!'
});
} else {
res.statusCode = 401;
return next('User is not authorised');
}
} else {
res.statusCode = 401;
return next('Incorrect username or password');
}
});
} else {
res.statusCode = 400;
return next('Authentication failed! Please check the request');
}
} catch (err) {
return next(err);
}
});
module.exports = router;
authHandler.js
var jwt = require('jsonwebtoken');
require('dotenv').config({path: './.env'});
const secret = process.env['SECRET'];
var checkToken = function(req, res, next) {
let token = req.cookies.token;
console.log(req.headers);
if (token) {
// Remove Bearer from string
if (token.startsWith('Bearer ')) {
token = token.slice(7, token.length);
}
// Remove quotes around token
else if (token.startsWith('"')) {
token = token.substring(1, token.length-1);
}
jwt.verify(token, secret, (err, decoded) => {
if (err) {
res.statusCode = 401;
return next('Authentication failed! Credentials are invalid');
} else {
req.decoded = decoded;
next();
}
});
} else {
res.statusCode = 400;
return next('Authentication failed! Please check the request');
}
};
module.exports = checkToken;
客户端-http://localhost:3000/
const express = require('express');
const app = express();
const axios = require('axios');
axios.defaults.baseURL = "http://localhost:4000";
axios.defaults.withCredentials = true;
app.use(express.urlencoded({ extended: true }));
app.use(express.json());
app.use(express.static('resources'));
app.set('view engine', 'ejs');
app.set('views', 'views');
app.get('/transactions', async (req, res) => {
axios.get('/budget/transactions', {
withCredentials: true,
headers: {
'Content-Type': 'application/json',
}
})
.then(response => {
console.log(response.request);
res.render('transactions', { name : 'transactions', title : 'transactions', ...response.data});
})
.catch(err => {
console.log(err);
res.render('transactions', { name : 'transactions', title : 'transactions', ...status });
});
});
app.post('/login', async (req, res) => {
axios.post('/login',
{
username: req.body.email,
password: req.body.password
},
{
withCredentials: true,
headers: {
'content-type': 'application/json',
}
})
.then(response => {
console.log(response.headers);
if (response.data.success === true) {
res.redirect('/transactions');
} else {
res.redirect('/login');
}
});
});
app.listen(3000, () => {
console.log("LISTENING ON PORT 3000");
});
我已经解决了这个问题。我用CORS找错了树——我找到的所有其他答案都让我有点转移注意力。
我的问题与此类似,即使我的";客户端";该应用程序是通过浏览器访问的,因为我使用的是ejs,这本质上意味着它是两个服务器相互通信:
Browser -> EJS server -> REST endpoint server.
这意味着浏览器不知道由设置的cookie标头发送回的cookie,并且只有当接收设置的cookie标题的客户端是浏览器时,cookie才会默认保存-axios在express服务器内运行时,默认情况下不会保存它们,所以我不得不添加保存cookie并将其发送到浏览器的手动处理。请参阅下面的完整客户端代码。
客户端-http://localhost:3000/
const express = require('express');
const app = express();
const config = require('./config.json');
const axios = require('axios');
const cookieParser = require('cookie');
const params404 = { name : '404', title : "404 Not Found" };
axios.defaults.baseURL = "http://localhost:4000";
axios.defaults.withCredentials = true;
app.use(express.urlencoded({ extended: true }));
app.use(express.json());
app.use(express.static('resources'));
app.set('view engine', 'ejs');
app.set('views', 'views');
app.get('/transactions', async (req, res) => {
let cookie = req.headers.cookie;
let headers = {
'Content-Type': 'application/json',
};
if (cookie) {
headers.Cookie = cookie;
}
axios.get('/budget/transactions', {
withCredentials: true,
headers: headers
})
.then(response => {
res.render('transactions', { name : 'transactions', title : 'transactions', ...response.data});
})
.catch(err => {
if (err?.response?.data != null) {
console.error(err.response.data);
} else {
console.error(err);
}
let status = { success: err.status!=null?err.status:false }
res.render('transactions', { name : 'transactions', title : 'transactions', ...status });
});
});
app.post('/login', async (req, res) => {
axios.post('/login',
{
username: req.body.email,
password: req.body.password
},
{
withCredentials: true,
headers: {
'content-type': 'application/json',
}
})
.then(response => {
if (
response.data.success === true &&
response?.headers["set-cookie"] != null &&
response.headers["set-cookie"][0] != null
) {
let token = cookieParser.parse(response?.headers["set-cookie"][0]).token;
res.cookie("token", token, {
secure: true,
httpOnly: true,
withCredentials: true,
maxAge: 8 * 60 * 60 * 1000, // 8 hours
sameSite: "None"
});
res.redirect('/transactions');
} else {
res.redirect('/login');
}
});
});
app.listen(3000, () => {
console.log("LISTENING ON PORT 3000");
});
尽管CORS设置与实际答案无关,但我还是保留了CORS设置。以下是我在localhost上确定的与上述配置配合使用的最终配置(为了完整性):
authentication.js
res.cookie("token", JSON.stringify(token), {
secure: true,
httpOnly: true,
withCredentials: true,
maxAge: 8 * 60 * 60 * 1000, // 8 hours
sameSite: "None",
path: "/",
});