null

Скрываем .xhtml средствами nginx

Доброе утро!

В этой заметке я опишу один из способов скрытия .xhtml в адресной строке.

Дано приложение на JSF+Spring, проксируемое при помощи nginx. По просьбе заказчика возникла необходимость спрятать из адресной строки .xhtml, стандартное расширение страниц JSF-приложений. Один из пришедших в голову подходов — а пусть nginx поправит адрес, который видит пользователь, а в приложение отдаст «правильный». Одно из преимуществ такого подхода — нет нужды править приложение. Попробуем это реализовать.

Будем работать со следующим кусочком конфига:

server {
  listen 80 default_server;

  server_name kk.free;

  location / { 
    proxy_set_header        X-Real-IP $remote_addr; 
    proxy_set_header        X-Forwarded-For $proxy_add_x_forwarded_for; 
    proxy_set_header        X-Proxy-Forwarded-SSL $ssl_client_verify; 
    proxy_set_header        X-Proxy-Forwarded-CN $ssl_client_s_dn; 
    proxy_set_header        Host 127.0.0.1;
    proxy_pass              http://localhost:8080; 
    proxy_redirect          default; 
  } 

}

В данном виде всё, что делает nginx — передаёт запросы приложению на другой порт на этой же машине. Давайте порежем .xhtml и отправим пользователя на обычную обработку:

server {
  listen 80 default_server;

  server_name kk.free;

  location ~ "\.xhtml" {
    rewrite "(.*)\.xhtml(.*)" $1$2 redirect;
  }

  location / { 
    proxy_set_header        X-Real-IP $remote_addr; 
    proxy_set_header        X-Forwarded-For $proxy_add_x_forwarded_for; 
    proxy_set_header        X-Proxy-Forwarded-SSL $ssl_client_verify; 
    proxy_set_header        X-Proxy-Forwarded-CN $ssl_client_s_dn; 
    proxy_set_header        Host 127.0.0.1;
    proxy_pass              http://localhost:8080; 
    proxy_redirect          default; 
  } 

}

Это, естественно, приводит к тому, что пользователь хоть и видит в браузере строки вида example.com/index вместо example.com/index.xhtml, но и приложение видит их такими же, а значит отдаёт 404 и ничего не работает. Исправим это:

server {
  listen 80 default_server;

  server_name kk.free;

  location ~ "\.xhtml" {
    rewrite "(.*)\.xhtml(.*)" $1$2 redirect;
  }

  location / { 
    rewrite "^(.*/[^.]+)(\?|$)" $1.xhtml$2 break;

    proxy_set_header        X-Real-IP $remote_addr; 
    proxy_set_header        X-Forwarded-For $proxy_add_x_forwarded_for; 
    proxy_set_header        X-Proxy-Forwarded-SSL $ssl_client_verify; 
    proxy_set_header        X-Proxy-Forwarded-CN $ssl_client_s_dn; 
    proxy_set_header        Host 127.0.0.1;
    proxy_pass              http://localhost:8080; 
    proxy_redirect          default; 
  } 

}

Уже лучше. Что сейчас происходит? Пользователь приходит на страницу, в которой есть .xhtml, его перенаправляют на такую же, но без него (rewrite … redirect), а локейшен для «всего остального» подставляет страницам без расширения его обратно, продолжая обрабатывать запрос (rewrite … brak). Но это всё равно не работает — отваливаются скрипты и стили. Если посмотреть, какие запросы делает браузер, то можно увидеть, что среди них есть что-то вроде /javax.faces.resource/…/style.css.xhml?…, расширение в которых обрубается, но не возвращается назад, потому что есть ещё одно. Решение — запретить в именах xhtml-файлов, созданных самостоятельно использовать более одной точки (что, обычно, в проектах выполняется и так) и слегка поправить конфиг:

server {
  listen 80 default_server;

  server_name kk.free;

  location ~ "^[^.]*\.xhtml" {
    rewrite "(.*)\.xhtml(.*)" $1$2 redirect;
  }

  location / { 
    rewrite "^(.*/[^.]+)(\?|$)" $1.xhtml$2 break;

    proxy_set_header        X-Real-IP $remote_addr; 
    proxy_set_header        X-Forwarded-For $proxy_add_x_forwarded_for; 
    proxy_set_header        X-Proxy-Forwarded-SSL $ssl_client_verify; 
    proxy_set_header        X-Proxy-Forwarded-CN $ssl_client_s_dn; 
    proxy_set_header        Host 127.0.0.1;
    proxy_pass              http://localhost:8080; 
    proxy_redirect          default; 
  } 

}

Таким образом, мы подменяем расширения только в путях, где нет лишних точек, а остальные пользователь и не видит. Теперь почти всё работает, за исключением POST-запросов. Браузер, делая POST-запрос, видит редирект, переходит, но тело уже не повторяет. В нашем случае, POST-запросы генерируются лишь JSF-ом, внутри скриптов, и в адресной строке нет путей, по которым они происходят. Так что это лечится достаточно просто:

server {
  listen 80 default_server;

  server_name kk.free;

  location ~ "^[^.]*\.xhtml" {
    if ($request_method != POST) {
      rewrite "(.*)\.xhtml(.*)" $1$2 redirect;
    }
    rewrite "(.*)\.xhtml(.*)" $1$2 last;
  }

  location / { 
    rewrite "^(.*/[^.]+)(\?|$)" $1.xhtml$2 break;

    proxy_set_header        X-Real-IP $remote_addr; 
    proxy_set_header        X-Forwarded-For $proxy_add_x_forwarded_for; 
    proxy_set_header        X-Proxy-Forwarded-SSL $ssl_client_verify; 
    proxy_set_header        X-Proxy-Forwarded-CN $ssl_client_s_dn; 
    proxy_set_header        Host 127.0.0.1;
    proxy_pass              http://localhost:8080; 
    proxy_redirect          default; 
  } 

}

С этого момента POST-запросы будут обрезаться и обрабатываться nginx-ом заново (rewrite … last), без перенаправления, тогда как для остальных всё осталось по-прежнему. Ну и если вдруг происходили какие-то запросы к страницам без расширений, для них я не нашёл лучшего способа, кроме как сделать отдельную проверку:

server {
  listen 80 default_server;

  server_name kk.free;

  location ~ "^[^.]*\.xhtml" {
    if ($request_method != POST) {
      rewrite "(.*)\.xhtml(.*)" $1$2 permanent;
    }
    rewrite "(.*)\.xhtml(.*)" $1$2 last;
  }

  location / { 
    if ($request_uri !~ "^(/path1/)|(/other/prefix/to/pgs/wo/ext/)") {
      rewrite "^(.*/[^.]+)(\?|$)" $1.xhtml$2 break;
    }

    proxy_set_header        X-Real-IP $remote_addr; 
    proxy_set_header        X-Forwarded-For $proxy_add_x_forwarded_for; 
    proxy_set_header        X-Proxy-Forwarded-SSL $ssl_client_verify; 
    proxy_set_header        X-Proxy-Forwarded-CN $ssl_client_s_dn; 
    proxy_set_header        Host 127.0.0.1;
    proxy_pass              http://localhost:8080; 
    proxy_redirect          default; 
  } 

}

Когда всё проверено и отлажено, можно заменить redirect на permanent, как в последнем примере, дабы браузеры не делали лишних запросов.

Теперь у нас есть способ убрать расширение страниц из адресной строки без (или почти без) правок приложения, на этом можно закончить.