使用node.js/express实现自动的HTTPS连接和重定向

229

我一直在尝试为我正在开发的node.js项目设置HTTPS。我基本上按照node.js文档中的示例进行操作:

// curl -k https://localhost:8000/
var https = require('https');
var fs = require('fs');

var options = {
  key: fs.readFileSync('test/fixtures/keys/agent2-key.pem'),
  cert: fs.readFileSync('test/fixtures/keys/agent2-cert.pem')
};

https.createServer(options, function (req, res) {
  res.writeHead(200);
  res.end("hello world\n");
}).listen(8000);

现在,当我执行

curl -k https://localhost:8000/

我明白了

hello world

作为预期结果。但如果我执行

curl -k http://localhost:8000/

我得到

curl: (52) Empty reply from server

回顾起来,这种方式似乎很显然可以生效,但与此同时,最终访问我项目的人不会输入 https://yadayada,我希望所有流量在访问网站时都是 https。

我该如何让 node(以及我正在使用的 Express 框架)将所有传入流量交接给 https,无论是否指定了 https?我还没有找到任何有关这一点的文档。或者假设在生产环境中,node前面有个用于处理这种重定向的程序(例如nginx)吗?

这是我第一次涉足 Web 开发,请原谅我的无知。


这个回答解决了你的问题吗?Heroku NodeJS http to https ssl forced redirect - TylerH
23个回答

222

Ryan,感谢你指导我正确的方向。我详细阐述了你的答案(第二段)并加入了一些代码,现在它可以工作了。在这种情况下,这些代码片段被放在我的Express应用程序中:

// set up plain http server
var http = express();

// set up a route to redirect http to https
http.get('*', function(req, res) {  
    res.redirect('https://' + req.headers.host + req.url);

    // Or, if you don't want to automatically detect the domain name from the request header, you can hard code it:
    // res.redirect('https://example.com' + req.url);
})

// have it listen on 8080
http.listen(8080);

目前,HTTPS Express服务器正在3000端口侦听。我设置了以下iptables规则,以使node不必以root身份运行:

iptables -t nat -A PREROUTING -i eth0 -p tcp --dport 80 -j REDIRECT --to-port 8080
iptables -t nat -A PREROUTING -i eth0 -p tcp --dport 443 -j REDIRECT --to-port 3000

总的来说,这正是我想要的。

为了防止通过HTTP窃取cookie,请参见此答案(来自评论)或使用以下代码:

const session = require('cookie-session');
app.use(
  session({
    secret: "some secret",
    httpOnly: true,  // Don't let browser javascript access cookies.
    secure: true, // Only use cookies over https.
  })
);

19
重要的问题(涉及安全性)。在重定向发生之前,攻击者是否有可能嗅探并窃取Cookie(会话ID)? - Costa Michailidis
5
我该如何修复这个错误?Error 310 (net::ERR_TOO_MANY_REDIRECTS): There were too many redirects - bodine
18
实际上,这个看起来更好,只需要用if(!req.secure){}包装重定向即可。 - bodine
6
我想指出在另一篇文章中@Costa的重要安全问题的答案 - http://stackoverflow.com/questions/8605720/how-to-force-ssl-https-in-express-js#comment-14832113 - ThisClark
3
你可能想把它包装在一个 if(req.protocol==='http') 语句中。 - Max
显示剩余2条评论

185

4
使用 app.get('X-Forwarded-Proto') != 'http' 代替 req.secure 来配置 AWS ELB 的转发协议。 - shak
好的回答!只需使用三元表达式进行改进:app.use(function(req, res, next) { req.secure ? next() : res.redirect('https://' + req.headers.host + req.url); }); - Shi Wei
即使设置IP表路由sudo iptables -t nat -A PREROUTING -i eth0 -p tcp --dport 80 -j REDIRECT --to-port 8443,我也无法使此解决方案工作。 接受的答案有效; 记得为cookie设置“secure”标志。 - miguelmorin
1
这个答案只涉及 Express 框架,如果您正在寻找纯 Node.js 的解决方案,则无法提供帮助,这也是 OP 代码的所在之处,并且会在搜索中出现此页面。basarat 在下面的答案仍然是最好的,即使六年过去了。 - Chris Robinson
没有设置crt和key文件? - Iago Coutinho Campos
为了确保POST请求也能重定向,请参见此问题 - Josh

137

如果您遵循传统端口,因为HTTP默认尝试端口80,HTTPS默认尝试端口443,那么您可以在同一台机器上简单地拥有两个服务器:

这是代码:

var https = require('https');

var fs = require('fs');
var options = {
    key: fs.readFileSync('./key.pem'),
    cert: fs.readFileSync('./cert.pem')
};

https.createServer(options, function (req, res) {
    res.end('secure!');
}).listen(443);

// Redirect from http port 80 to https
var http = require('http');
http.createServer(function (req, res) {
    res.writeHead(301, { "Location": "https://" + req.headers['host'] + req.url });
    res.end();
}).listen(80);

测试https:

$ curl https://127.0.0.1 -k
secure!

使用http协议:

$ curl http://127.0.0.1 -i
HTTP/1.1 301 Moved Permanently
Location: https://127.0.0.1/
Date: Sun, 01 Jun 2014 06:15:16 GMT
Connection: keep-alive
Transfer-Encoding: chunked

更多细节: Node.js如何在同一端口上使用HTTP和HTTPS?


7
res.writeHead(301, etc.) 只对 GET 请求有效,因为 301 不会告诉客户端使用相同的方法。如果您想保留使用的方法(和所有其他参数),您需要使用 res.writeHead(307, etc.)。如果仍然无法正常工作,您可能需要进行一些代理。来源:https://dev59.com/CmMm5IYBdhLWcg3wburb#17612942 - ocramot

28

Nginx可以利用"x-forwarded-proto"头部:

function ensureSec(req, res, next){
    if (req.headers["x-forwarded-proto"] === "https"){
       return next();
    }
    res.redirect("https://" + req.headers.host + req.url);  
}

6
我发现req.headers["x-forwarded-proto"] === "https"不太可靠,但是req.secure可行! - Aaron Silverman
我也发现了,这看起来非常不可靠。 - dacopenhagen
2
req.secure是正确的方式,但如果您在代理后面,则存在错误,因为req.secure等同于proto ==“https”,但在代理后面,express可能会说您的proto是https,http。 - dacopenhagen
8
你需要添加app.enable('trust proxy');:表示应用程序位于前置代理后面,并使用 X-Forwarded-* 标头来确定客户端的连接和 IP 地址。参考文档:http://expressjs.com/en/4x/api.html#app.set - dskrvk
1
不要将URL视为字符串,而应使用url模块。 - arboreal84
显示剩余3条评论

13

从0.4.12版本开始,我们在使用Node的HTTP / HTTPS服务器监听HTTP和HTTPS的同一端口方面缺乏真正干净的解决方法。

有些人通过让Node的HTTPS服务器(这也适用于Express.js)侦听443(或其他某个端口),同时还有一个小型HTTP服务器绑定到80并将用户重定向到安全端口来解决此问题。

如果您绝对必须能够在单个端口上处理两种协议,则需要在该端口上放置nginx、lighttpd、apache或其他某个Web服务器,并使其充当Node的反向代理。


谢谢Ryan。通过“...绑定到80并重定向...”我想你是指另一个node http服务器。我认为我有一个想法,但你能指出任何示例代码吗?此外,您是否知道在节点路线图中是否有更“清洁”的解决方案来解决此问题? - Jake
你的Node.js应用程序可以拥有多个http(s)服务器。我查看了Node.js的问题(http://github.com/joyent/node/issues)和邮件列表(http://groups.google.com/group/nodejs),没有看到任何报告的问题,但在邮件列表上看到了一些关于该问题的帖子。据我所知,这并不是在进行中的工作。我建议在github上报告此问题,并查看您获得的反馈。 - Ryan Olds
非常重要的问题(关于安全性)。在重定向实际发生之前,攻击者是否有可能嗅探并窃取cookie(会话ID)? - Costa Michailidis
是的,会发送一个3xx状态码和可能的Location头到代理,此时代理会请求由Location头指定的URL。 - Ryan Olds
这是2015年,这仍然是事实吗? - TheEnvironmentalist
是的,我不认为会有所改变。随着我们转向HTTP/2+,非SSL/TLS连接将消失,除非支持遗留代码或浏览器,否则这将不再是一个问题。 - Ryan Olds

12

我使用了Basarat提出的解决方案,但我还需要覆盖端口,因为我曾经为HTTP和HTTPS协议使用过两个不同的端口。

res.writeHead(301, { "Location": "https://" + req.headers['host'].replace(http_port,https_port) + req.url });

我更喜欢使用非标准端口来启动Node.js,这样就不需要使用root权限。我喜欢使用8080和8443,因为我已经在Tomcat上编程多年了。

我的完整文件如下:

var fs = require('fs');
var http = require('http');
var http_port    =   process.env.PORT || 8080; 
var app = require('express')();

// HTTPS definitions
var https = require('https');
var https_port    =   process.env.PORT_HTTPS || 8443; 
var options = {
   key  : fs.readFileSync('server.key'),
   cert : fs.readFileSync('server.crt')
};

app.get('/', function (req, res) {
   res.send('Hello World!');
});

https.createServer(options, app).listen(https_port, function () {
   console.log('Magic happens on port ' + https_port); 
});

// Redirect from http port to https
http.createServer(function (req, res) {
    res.writeHead(301, { "Location": "https://" + req.headers['host'].replace(http_port,https_port) + req.url });
    console.log("http request, will go to >> ");
    console.log("https://" + req.headers['host'].replace(http_port,https_port) + req.url );
    res.end();
}).listen(http_port);

然后我使用 iptables 将 80 和 443 流量转发到我的 HTTP 和 HTTPS 端口。

sudo iptables -t nat -A PREROUTING -i eth0 -p tcp --dport 80 -j REDIRECT --to-port 8080
sudo iptables -t nat -A PREROUTING -i eth0 -p tcp --dport 443 -j REDIRECT --to-port 8443

谢谢Lorenzo,这对我有用。我使用了letsencrypt而不是选项。我删除了var options。我添加了letsencrypt参数(privateKey、certificate、ca和credentials),并将options更改为credentials:https.createServer(credentials, app)。 - Nhon Ha

10

您可以使用express-force-https模块:

npm install --save express-force-https

var express = require('express');
var secure = require('express-force-https');

var app = express();
app.use(secure);

3
请谨慎使用此软件包,因为它多年未更新。我在AWS上遇到了编码问题。 - Martavis P.
1
姊妹包:https://www.npmjs.com/package/express-to-https - 但我不知道它是否有效/更好等。 - quetzalcoatl
请注意,如果您正在使用中间件来提供静态文件(包括单页应用程序),任何重定向中间件都必须首先附加到“app”上。(请参见此答案 - Matthew R.

8

如果您的应用程序位于受信任的代理后面(例如AWS ELB或正确配置的nginx),则此代码应该有效:

app.enable('trust proxy');
app.use(function(req, res, next) {
    if (req.secure){
        return next();
    }
    res.redirect("https://" + req.headers.host + req.url);
});

注:

  • 这里假设您将站点托管在80和443端口上,如果不是,请在重定向时更改端口。
  • 这也假设您在代理服务器上终止了SSL。如果您正在进行端到端的SSL,请使用@basarat的答案。端到端的SSL是更好的解决方案。
  • app.enable('trust proxy')允许express检查X-Forwarded-Proto头部信息。

8

这个答案需要更新以适用于Express 4.0。以下是我成功实现独立HTTP服务器的方法:

var express = require('express');
var http = require('http');
var https = require('https');

// Primary https app
var app = express()
var port = process.env.PORT || 3000;
app.set('env', 'development');
app.set('port', port);
var router = express.Router();
app.use('/', router);
// ... other routes here
var certOpts = {
    key: '/path/to/key.pem',
    cert: '/path/to/cert.pem'
};
var server = https.createServer(certOpts, app);
server.listen(port, function(){
    console.log('Express server listening to port '+port);
});


// Secondary http app
var httpApp = express();
var httpRouter = express.Router();
httpApp.use('*', httpRouter);
httpRouter.get('*', function(req, res){
    var host = req.get('Host');
    // replace the port in the host
    host = host.replace(/:\d+$/, ":"+app.get('port'));
    // determine the redirect destination
    var destination = ['https://', host, req.url].join('');
    return res.redirect(destination);
});
var httpServer = http.createServer(httpApp);
httpServer.listen(8080);

1
我通过将 httpApp.use('*', httpRouter); 更改为 httpApp.use('/', httpRouter); 并将其移动到创建 hhtp 服务器之前的行上,使其正常工作。 - KungWaz

8

我发现在使用express时,req.protocol起作用(尚未测试无需使用,但我怀疑它有效)。 使用当前的node 0.10.22和express 3.4.3。

app.use(function(req,res,next) {
  if (!/https/.test(req.protocol)){
     res.redirect("https://" + req.headers.host + req.url);
  } else {
     return next();
  } 
});

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接