在Express中全局重定向所有尾随斜杠

46

我正在使用Node.js和Express,并且我有以下路由:

app.get('/', function(req,res){
    locals.date = new Date().toLocaleDateString();

    res.render('home.ejs', locals);
});

function lessonsRouter (req, res, next)
{
    var lesson = req.params.lesson;
    res.render('lessons/' + lesson + '.ejs', locals_lessons);
}

app.get('/lessons/:lesson*', lessonsRouter);


function viewsRouter (req, res, next)
{
    var controllerName = req.params.controllerName;
    res.render(controllerName + '.ejs', locals_lessons);
}
app.get('/:controllerName', viewsRouter);

我在我的课程页面上放置了一个Disqus小部件,但我发现一个奇怪的行为,当访问myapp.com/lessonsmyapp.com/lessons/时,我会得到两个不同的页面(其中一个页面有我之前在Disqus中添加的评论,而另一个页面没有评论)。

是否有一种方法可以“规范化”所有URL,使其没有尾随斜杠?我尝试将strict routing标志添加到Express中,但结果仍然相同。

谢谢


1
你使用的是 Express 的哪个版本?在 3.x 版本中,默认行为(未开启“严格路由”)会使得 /foo/foo/ 在路由器中看起来相同。鉴于页面两种方式都能呈现,我的第一个猜测是这可能是浏览器缓存问题,但是如果不了解 Disqus 的更多信息,我不能确定。 - David Weldon
@DavidWeldon express 3。这可能是Disqus将其视为两个不同的地址。无论如何,我应该如何将任何带有尾随/的地址重定向到没有/的地址?这样,即使用户在浏览器中输入/,也会被重定向到正确的路径。 - Michael
如果您知道问题始终只会出现在一个特定的路由上,我建议将重定向添加到该特定路由处理程序中。如果不是这样,我会选择像Tolga下面提供的中间件解决方案。 - David Weldon
2
严格路由:启用严格路由,路由器默认将“/foo”和“/foo/”视为相同的路径。 app.set('strict routing', true); ~ 来源:http://expressjs.com/api.html#app-settings - Bas van Ommen
8个回答

104

这个回答Tolga Akyüz提供,启发人心,但如果斜杠后面有任何字符,它就无法正常工作。例如http://example.com/api/?q=a会被重定向到http://example.com/api而不是http://example.com/api?q=a

下面是一个改进版本的中间件,它通过在重定向目标URL的末尾添加原始查询来解决了这个问题。该版本还具有一些安全功能,在更新说明中有描述。

app.use((req, res, next) => {
  if (req.path.slice(-1) === '/' && req.path.length > 1) {
    const query = req.url.slice(req.path.length)
    const safepath = req.path.slice(0, -1).replace(/\/+/g, '/')
    res.redirect(301, safepath + query)
  } else {
    next()
  }
})

更新2016:正如jamesk所指出并在RFC 1738中说明,如果域名后面没有内容,则不能省略结尾的斜杠。因此,http://example.com?q=a是一个无效的URL,而http://example.com/?q=a则是有效的URL。在这种情况下,不应该进行重定向。幸运的是,表达式req.path.length > 1已经解决了这个问题。例如,对于URLhttp://example.com/?q=a,路径req.path等于/,因此避免了重定向。

更新2021:正如Matt所发现的那样,在路径开头有双斜杠//会导致危险的重定向。例如,URLhttp://example.com//evil.example/会创建一个重定向到//evil.example,被受害者的浏览器解释为http://evil.examplehttps://evil.example。此外,正如Van Quyet所指出的那样,可能会存在多个尾随斜杠,应该予以优雅处理。鉴于这些发现,我添加了一行代码来保护路径,将所有连续的斜杠替换为单个/。我相信由此引起的性能开销可以忽略不计,因为正则表达式字面量只编译一次。此外,代码语法已更新为ES6。


1
我想知道有多少人会点赞Tolga的回答,运行几天后才意识到这里缺少了什么,然后再回来。 - Patrick Lee Scott
1
请注意,此做法与RFC 1738规定的格式不同:https://dev59.com/OXI-5IYBdhLWcg3w6dC1#1617074 - jamesk
1
RFC1738规定的格式已被其他RFC(2396和3986)以及URL标准覆盖。一个URL可以是这种形式http://example.com?q=a。请参见https://dev59.com/OXI-5IYBdhLWcg3w6dC1#42193734。 - Félix Brunet
1
如果我们有URL:http://example.com/abc/xyz/////(超过一个斜杠),我们不应该重定向它。我建议编辑条件为: (req.path.substr(-1) === '/' && req.path.substr(-2, 1) !== '/' && req.path.length > 1) - Văn Quyết
1
这段代码很危险。它可能会创建一个开放式重定向。例如,如果我们发出请求http://example.com//evil.com/(注意路径开头处的双斜杠和尾部斜杠),这将重定向到//evil.com,被解释为指向恶意网站的协议相对链接。 - Matt
显示剩余3条评论

58

尝试添加一个中间件来解决这个问题;

app.use((req, res, next) => {
  const test = /\?[^]*\//.test(req.url);
  if (req.url.substr(-1) === '/' && req.url.length > 1 && !test)
    res.redirect(301, req.url.slice(0, -1));
  else
    next();
});

6
这个可以。我更喜欢使用req.url.slice(0, -1)而不是req.url.substring(0, req.url.length-1),但这只是风格问题。 - David Weldon
2
@TolgaAkyüz 我猜你是想用 res.redirect 而不是 req.redirect,对吗? - Michael
8
请注意,使用这个中间件时,如果斜杠后面有任何内容,重定向将不起作用。例如,即使 http://example.com/lessons/ 可以重定向,但 http://example.com/lessons/? 将无法重定向。 - Akseli Palén
如果是POST请求,我会添加方法,以便将POST数据传递到新的URL:let method = (req.method === 'POST') ? 307 : 301; res.redirect(method, url); - Vedran
这在 Next.js 环境中不起作用,例如 http://localhost:3000/_next/on-demand-entries-ping?page=/ 这样的请求。该示例完全忽略了任何查询参数的可能性。 - dude
显示剩余7条评论

27

1
这是首选的解决方案,因为它支持连接中间件和模块化。 - Verdi Erel Ergün
2
谢谢!在 Express v4 中也能很好地工作。 - jpunk11

11

我添加这个答案是因为我在使用其他解决方案时遇到了太多的问题。

/**
 * @param {express.Request} req
 * @param {express.Response} res
 * @param {express.NextFunction} next
 * @return {void}
 */
function checkTrailingSlash(req, res, next) {
  const trailingSlashUrl = req.baseUrl + req.url;
  if (req.originalUrl !== trailingSlashUrl) {
    res.redirect(301, trailingSlashUrl);
  } else {
    next();
  }
}

router.use(checkTrailingSlash);

这将被翻译:

/page ==> /page/
/page?query=value ==> /page/?query=value

7

一句话概括:

router.get('\\S+\/$', function (req, res) {
  return res.redirect(301, req.path.slice(0, -1) + req.url.slice(req.path.length));
});

这将仅捕获需要重定向的URL,而忽略其他URL。

示例结果:

/         ==> /
/a        ==> /a
/a/       ==> /a
/a/b      ==> /a/b
/a/b/     ==> /a/b
/a/b/?c=d ==> /a/b?c=d

使用 router.all 是否更好? - sidonaldson

0
以上答案在很多情况下都可以使用,但 GET 变量可能会遇到问题,如果将其放在另一个 Express 中间件中,它对 req.path 的依赖性将导致问题,并且它对 req.url 的依赖性也可能会产生不良的副作用。 如果您正在寻找更紧凑的解决方案,这将起到作用。
// Redirect non trailing slash to trailing slash
app.use(function(req, res, next){
    // Find the query string
    var qsi = req.originalUrl.indexOf('?');
    // Get the path
    var path = req.originalUrl;
    if(qsi > -1) path = path.substr(0, qsi);
    // Continue if the path is good or it's a static resource
    if(path.substr(-1) === '/' || ~path.indexOf('.')) return next();
    // Save just the query string
    var qs = '';
    if(qsi > -1) qs = req.originalUrl.substr(qsi);
    // Save redirect path
    var redirect = path + '/' + qs;
    // Redirect client
    res.redirect(301, redirect);

    console.log('301 redirected ' + req.originalUrl + ' to ' + redirect);
});

如果使用GET变量,它总是很高兴,并且即使将其放入中间件中也不会出错。


0
如果您使用fastify来处理路由,可以尝试将Fastify的ignoreTrailingSlash选项设置为true。
const fastify = require('fastify')({
  ignoreTrailingSlash: true
})

0
/**
 * @param {express.Request} req
 * @param {express.Response} res
 * @param {express.NextFunction} next
 * @return {void}
 */
function checkTrailingSlash(req, res, next) {
    if (req.path.slice(req.path.length-1) !== '/') {
        res.redirect(301, req.path + '/' + req.url.slice(req.path.length));
    } else {
        next();
    }
}
  
app.use(checkTrailingSlash);

示例结果:

/         ==> /
/a        ==> /a/
/a/       ==> /a/
/a/b      ==> /a/b/
/a/b/     ==> /a/b/
/a/b?c=d  ==> /a/b/?c=d
/a/b/?c=d ==> /a/b/?c=d

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