Express/React 使用 CORS - 设置 HTTP-Only React SPA 的安全 Cookie
Express/React with CORS - Setting HTTP-Only Secure Cookie for React SPA
我正在尝试设置一个简单的 API/SPA,使用 Express 作为 API (api.mysite.co) 并在 AWS ElasticBeanstalk/S3部署。对于身份验证,我正在尝试在 HTTP-only 安全 cookie 中设置 JWT。但由于某种原因,cookie 从未到达客户端。
为了安全起见,我已将域更改为 mysite.co。
这是我的 app.js 快递文件:
const dotenv = require('dotenv');
const express = require('express');
const cookieParser = require('cookie-parser');
const logger = require('morgan');
const helmet = require('helmet');
const passport = require('passport');
const cors = require('cors');
const usersRouter = require('./routes/users');
const env = process.env.NODE_ENV || 'development';
const port = process.env.PORT || '3000';
const app = express();
// Setup dev env variables and modules
if (env === 'development') {
dotenv.config();
app.use(logger('dev'));
}
app.use(helmet());
// Setup CORS requests based on environment
if (env === 'development') {
app.use(cors());
} else {
app.use(
cors({
origin: 'https://app.mysite.co',
credentials: true,
}),
);
}
app.use(cookieParser());
// Handle CORS pre-flight reqests
app.options('*', cors());
app.use(passport.initialize());
require('./lib/auth/passport')(passport); // eslint-disable-line global-require
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use('/users', usersRouter);
// Catch all route that masks 404 for security
app.get('*', (req, res) => {
res.status(401).json({ error: 'Unauthorized access' });
});
app.listen(port);
还有我的 ./routes/users 文件和登录路径。我已经确认它正在运行,因为响应已发送并且用户已登录到该应用程序。但由于某种原因,cookie 从未在 chrome.
中设置
const express = require('express');
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');
const router = express.Router();
const mongodb = require('../db/mongodb');
const validateRegisterInput = require('../lib/validation/register');
const validateLoginInput = require('../lib/validation/login');
// // POST /users/login - User Login
router.post('/login', (req, res) => {
mongodb.initDb().then((db) => {
const collection = db.collection('users');
const { errors, isValid } = validateLoginInput(req.body);
if (!isValid) {
return res.status(400).json(errors);
}
const { email, password } = req.body;
return collection
.find({ email })
.limit(1)
.toArray((connectErr, users) => {
if (connectErr) {
errors.email = 'Could not verify email. Please try again later.';
return res.status(500).json(errors);
}
// Pull user out of array
const user = users[0];
if (!user) {
errors.email = 'Username and Password do not match.';
return res.status(400).json(errors);
}
return bcrypt.compare(password, user.password).then((isMatch) => {
if (isMatch) {
const payload = {
id: user._id, // eslint-disable-line no-underscore-dangle
expire_date: Date.now() + parseInt(process.env.JWT_EXPIRATION_MS, 10),
};
return jwt.sign(JSON.stringify(payload), process.env.PASSPORT_SECRET, (err, token) => {
if (err) {
return res.status(500).json({
password: 'There was an internal error. Please try again later',
});
}
res.cookie('jwt', token, {
domain: 'app.mysite.co',
httpOnly: true,
secure: true,
maxAge: parseInt(process.env.JWT_EXPIRATION_MS, 10),
});
return res.status(200).json({
name: user.name,
});
});
}
errors.password = 'Username and Password do not match.';
return res.status(401).json(errors);
});
});
});
});
module.exports = router;
在 React SPA 中,我使用 Axios 进行 POST 调用。在我设置的 app.js 文件中:
axios.defaults.baseURL = 'https://api.mysite.co';
axios.defaults.withCredentials = true;
然后这样调用:
/**
* @description Logins in new user and sets their token in localStorage
* @param {object} user Input from the login form.
*/
export const loginUser = user => dispatch => axios
.post('/users/login', user)
.then((res) => {
dispatch(setCurrentUser(res.data));
})
.catch((err) => {
dispatch(handleError(err));
});
需要注意的一件事是,当我发送请求时,pre-flight OPTIONS 和 POST 调用都针对请求 headers:
Provisional headers are shown
Access-Control-Request-Headers: content-type
Access-Control-Request-Method: POST
Origin: https://app.mysite.co
Referer: https://app.mysite.co/login
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.119 Safari/537.36
我做了一些研究,似乎当显示 Provisional headers are shown
警告时,这意味着 API 调用没有正常工作。但是我在 POST 电话中收到了 API 的后续响应:
Request URL: https://api.mysite.co/users/login
Request Method: POST
Status Code: 200
Remote Address: 52.201.140.183:443
Referrer Policy: no-referrer-when-downgrade
access-control-allow-credentials: true
access-control-allow-origin: https://app.mysite.co
content-length: 16
content-type: application/json; charset=utf-8
date: Sat, 23 Feb 2019 15:22:33 GMT
etag: W/"10-pyeidcXwb9ByfCNz+iqLTARQNP8"
server: nginx/1.14.1
status: 200
vary: Origin
x-powered-by: Express
Provisional headers are shown
Accept: application/json, text/plain, */*
Content-Type: application/json;charset=UTF-8
Origin: https://app.mysite.co
Referer: https://app.mysite.co/login
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.119 Safari/537.36
显示成功,用户登录
任何帮助将不胜感激,因为我似乎已经超出了我的理解范围。我没有记录任何服务器或客户端错误,从我阅读的所有内容来看,在 Express 和 Axios 上设置 credentials: true
标志应该可以解决问题。
谢谢!
所以在尝试了一些不同的事情之后,问题在于将 res.cookie
中的域设置为子域。我将该代码更改为:
res.cookie('jwt', token, {
domain: 'mysite.co',
secure: true,
httpOnly: true,
maxAge: parseInt(process.env.JWT_EXPIRATION_MS, 10),
});
现在正在设置 cookie。
我正在尝试设置一个简单的 API/SPA,使用 Express 作为 API (api.mysite.co) 并在 AWS ElasticBeanstalk/S3部署。对于身份验证,我正在尝试在 HTTP-only 安全 cookie 中设置 JWT。但由于某种原因,cookie 从未到达客户端。
为了安全起见,我已将域更改为 mysite.co。
这是我的 app.js 快递文件:
const dotenv = require('dotenv');
const express = require('express');
const cookieParser = require('cookie-parser');
const logger = require('morgan');
const helmet = require('helmet');
const passport = require('passport');
const cors = require('cors');
const usersRouter = require('./routes/users');
const env = process.env.NODE_ENV || 'development';
const port = process.env.PORT || '3000';
const app = express();
// Setup dev env variables and modules
if (env === 'development') {
dotenv.config();
app.use(logger('dev'));
}
app.use(helmet());
// Setup CORS requests based on environment
if (env === 'development') {
app.use(cors());
} else {
app.use(
cors({
origin: 'https://app.mysite.co',
credentials: true,
}),
);
}
app.use(cookieParser());
// Handle CORS pre-flight reqests
app.options('*', cors());
app.use(passport.initialize());
require('./lib/auth/passport')(passport); // eslint-disable-line global-require
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use('/users', usersRouter);
// Catch all route that masks 404 for security
app.get('*', (req, res) => {
res.status(401).json({ error: 'Unauthorized access' });
});
app.listen(port);
还有我的 ./routes/users 文件和登录路径。我已经确认它正在运行,因为响应已发送并且用户已登录到该应用程序。但由于某种原因,cookie 从未在 chrome.
中设置const express = require('express');
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');
const router = express.Router();
const mongodb = require('../db/mongodb');
const validateRegisterInput = require('../lib/validation/register');
const validateLoginInput = require('../lib/validation/login');
// // POST /users/login - User Login
router.post('/login', (req, res) => {
mongodb.initDb().then((db) => {
const collection = db.collection('users');
const { errors, isValid } = validateLoginInput(req.body);
if (!isValid) {
return res.status(400).json(errors);
}
const { email, password } = req.body;
return collection
.find({ email })
.limit(1)
.toArray((connectErr, users) => {
if (connectErr) {
errors.email = 'Could not verify email. Please try again later.';
return res.status(500).json(errors);
}
// Pull user out of array
const user = users[0];
if (!user) {
errors.email = 'Username and Password do not match.';
return res.status(400).json(errors);
}
return bcrypt.compare(password, user.password).then((isMatch) => {
if (isMatch) {
const payload = {
id: user._id, // eslint-disable-line no-underscore-dangle
expire_date: Date.now() + parseInt(process.env.JWT_EXPIRATION_MS, 10),
};
return jwt.sign(JSON.stringify(payload), process.env.PASSPORT_SECRET, (err, token) => {
if (err) {
return res.status(500).json({
password: 'There was an internal error. Please try again later',
});
}
res.cookie('jwt', token, {
domain: 'app.mysite.co',
httpOnly: true,
secure: true,
maxAge: parseInt(process.env.JWT_EXPIRATION_MS, 10),
});
return res.status(200).json({
name: user.name,
});
});
}
errors.password = 'Username and Password do not match.';
return res.status(401).json(errors);
});
});
});
});
module.exports = router;
在 React SPA 中,我使用 Axios 进行 POST 调用。在我设置的 app.js 文件中:
axios.defaults.baseURL = 'https://api.mysite.co';
axios.defaults.withCredentials = true;
然后这样调用:
/**
* @description Logins in new user and sets their token in localStorage
* @param {object} user Input from the login form.
*/
export const loginUser = user => dispatch => axios
.post('/users/login', user)
.then((res) => {
dispatch(setCurrentUser(res.data));
})
.catch((err) => {
dispatch(handleError(err));
});
需要注意的一件事是,当我发送请求时,pre-flight OPTIONS 和 POST 调用都针对请求 headers:
Provisional headers are shown
Access-Control-Request-Headers: content-type
Access-Control-Request-Method: POST
Origin: https://app.mysite.co
Referer: https://app.mysite.co/login
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.119 Safari/537.36
我做了一些研究,似乎当显示 Provisional headers are shown
警告时,这意味着 API 调用没有正常工作。但是我在 POST 电话中收到了 API 的后续响应:
Request URL: https://api.mysite.co/users/login
Request Method: POST
Status Code: 200
Remote Address: 52.201.140.183:443
Referrer Policy: no-referrer-when-downgrade
access-control-allow-credentials: true
access-control-allow-origin: https://app.mysite.co
content-length: 16
content-type: application/json; charset=utf-8
date: Sat, 23 Feb 2019 15:22:33 GMT
etag: W/"10-pyeidcXwb9ByfCNz+iqLTARQNP8"
server: nginx/1.14.1
status: 200
vary: Origin
x-powered-by: Express
Provisional headers are shown
Accept: application/json, text/plain, */*
Content-Type: application/json;charset=UTF-8
Origin: https://app.mysite.co
Referer: https://app.mysite.co/login
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.119 Safari/537.36
显示成功,用户登录
任何帮助将不胜感激,因为我似乎已经超出了我的理解范围。我没有记录任何服务器或客户端错误,从我阅读的所有内容来看,在 Express 和 Axios 上设置 credentials: true
标志应该可以解决问题。
谢谢!
所以在尝试了一些不同的事情之后,问题在于将 res.cookie
中的域设置为子域。我将该代码更改为:
res.cookie('jwt', token, {
domain: 'mysite.co',
secure: true,
httpOnly: true,
maxAge: parseInt(process.env.JWT_EXPIRATION_MS, 10),
});
现在正在设置 cookie。