В посте Отображение на сервлет всех запросов к web-приложению мы рассмотрели шаблон проектирования Front Controller и научились перенаправлять все запросы к web-приложению через этот контроллер. Но что если сервлет-контроллер, получающий все запросы, захочет при определенных условиях направить поступивший запрос на специализированную обработку другому сервлету? Если все остальные отображения на сервлеты удалены из web.xml, а шаблон URL в стиле invoker (/servlets/*) также отображен на контроллер, то даже сам сервлет-контроллер не сможет передать запрос другому сервлету! Как обойти это ограничение?


Решение заключается во введении в файле web.xml персональных отображений на сервлеты. Для этого используются элементы security-constraint, предотвращающие возможность обращения с запросами к сервлетам-неконтроллерам со стороны пользователей. Если вы еще не знакомы с ограничениями безопасностями, то прочитайте пост Ограничение запросов к определенным сервлетам. Когда сервлет-контроллер хочет перенаправить запрос к другому сервлету, он испоьзует объект, реализующий интерфейс javax.servlet.RequestDispatcher (диспетчер запросов). Диспетчеры запросов (RequestDispatcher) не ограничены в возможности передавать запросы (с помощью метода RequestDispatcher.forward(request, response)) на шаблоны URL, заданные в элементах security-constraint. В следующем примере сервлет с именем Controller использует диспетчер запросов для передачи запроса другому сервлету.

package ru.topcode.servletconstrainttest;
 
import java.io.IOException;
import java.rmi.ServerException;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
 
/**
 *
 * @author dmitrii.leontiev
 */
public class Controller extends HttpServlet {
 
    @Override
    public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        RequestDispatcher dispatcher = null;
        String param = request.getParameter("go");
 
        if (param == null) {
            throw new ServerException("Параметр не задан");
        } else if (param.equals("weather")) {
            dispatcher = request.getRequestDispatcher("/weather");
        } else if (param.equals("maps")) {
            dispatcher = request.getRequestDispatcher("/maps");
        } else {
            throw new ServerException("Неправильный параметр");
        }
 
        if (dispatcher != null) {
            dispatcher.forward(request, response);
        } else {
            throw new ServerException("Dispather is NULL");
        }
    }
}

Сервлет Weather:

package ru.topcode.servletconstrainttest;
 
import java.io.IOException;
import java.io.PrintWriter;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
 
/**
 *
 * @author dmitrii.leontiev
 */
public class Weather extends HttpServlet {
 
    @Override
    public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        request.setCharacterEncoding("Cp1251");
 
        // Устанавливаем MIME
        response.setContentType("text/html;charset=Windows-1251");
 
        PrintWriter out = response.getWriter();
        out.println("<html><head>");
        out.println("<title>Прогноз погоды</title></head><body>");
        out.println("<h1>Сервлет-контроллер перенаправил запрос сервлету Weather.</h1>");
 
        out.println("</body></html>");
    }
}

Сервлет Controller проверяет значение параметра go. Запрос к нему может выглядеть так: http://localhost:8080/servlet-constraint-test?go=weather. Сервлет Controller из приведенного примера, с помощью отображения, настроен на получение всех запросов у web-приложению servlet-constraint-test. Другими словами, его элемент servlet-mapping в web.xml содержит шаблон URL /*.

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
    <display-name>servlet-constraint-test</display-name>
 
    <servlet>
        <servlet-name>Controller</servlet-name>
        <servlet-class>ru.topcode.servletconstrainttest.Controller</servlet-class>
    </servlet>
 
    <servlet-mapping>
        <servlet-name>Controller</servlet-name>
        <url-pattern>/*</url-pattern>
    </servlet-mapping>
 
    <servlet-mapping>
        <servlet-name>Controller</servlet-name>
        <url-pattern>/servlets/*</url-pattern>
    </servlet-mapping>
 
</web-app>

Для передачи запроса в зависимости от параметра go, сервлет Controller создает объект RequestDispatcher с тем или иным URL назначения. Сначала сервлет получает объект RequestDispatcher, для чего вызывает метод getRequestDispatcher(String path) объекта request. Значение параметра path может быть задано относительно корневого каталога контекста web-приложения, но оно не должно выходить за пределы текущего контекста сервлета. Предположим, шаблон URL /weather отображен на зарегистрированное имя сервлета Weather.

    <servlet-mapping>
        <servlet-name>Weather</servlet-name>
        <url-pattern>/weather</url-pattern>
    </servlet-mapping>

В этом случае путь, передаваемый методу getRequestDispatcher выглядит так: getRequestDispatcher(“/weather”). Если параметр go задан неверно или вообще не задан, Controller вызывает исключение ServletException с соответствующим сообщением. Сервлет Weather не доступен web-пользователям, поскольку доступ к нему ограничен элементом security-constraint, однако на метод RequestDispatcher.forward(request, response) эти ограничения не распространяются.

Теперь нужно защитить сервлет с именем Weather от прямого доступа пользователей. Для этого необходимо изменить дескриптор развертывания web.xml следующим образом:

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
    <display-name>servlet-constraint-test</display-name>
 
    <servlet>
        <servlet-name>Controller</servlet-name>
        <servlet-class>ru.topcode.servletconstrainttest.Controller</servlet-class>
    </servlet>
 
    <servlet>
        <servlet-name>Weather</servlet-name>
        <servlet-class>ru.topcode.servletconstrainttest.Weather</servlet-class>
    </servlet>
 
    <servlet-mapping>
        <servlet-name>Controller</servlet-name>
        <url-pattern>/*</url-pattern>
    </servlet-mapping>
 
    <servlet-mapping>
        <servlet-name>Controller</servlet-name>
        <url-pattern>/servlets/*</url-pattern>
    </servlet-mapping>
 
    <servlet-mapping>
        <servlet-name>Weather</servlet-name>
        <url-pattern>/weather</url-pattern>
    </servlet-mapping>
 
    <security-constraint>
        <web-resource-collection>
            <web-resource-name>Weather</web-resource-name>
            <url-pattern>/weather</url-pattern>
            <http-method>GET</http-method>
            <http-method>POST</http-method>
        </web-resource-collection>
        <auth-constraint>
            <role-name>nullrole</role-name>
        </auth-constraint>
        <user-data-constraint>
            <transport-guarantee>NONE</transport-guarantee>
        </user-data-constraint>
    </security-constraint>
 
    <security-role>
        <role-name>nullrole</role-name>
    </security-role>
</web-app>

Следующий шаг по защите сервлета Weather – убедиться, что в файле tomcat-user.xml роль nullrole не назначена никому из пользователей.

В web-приложениях, сконфигурированных подобным образом, на любой запрос к шаблону URL /weather будет возвращен ответ типа “HTTP статус 403 – в доступе к запрашиваемому ресурсу отказано”. Однако сервлет-контроллер, испоьзуя метод RequestDispatcher.forward(request, response), по-прежнему может направлять запросы к URL /weather на обработку.

Для получения объекта RequestDispatcher можно также использовать метод javax.servlet.ServletContext.getNamedDispatcher(String name). При использовании этого метода не нужно включать никаких элементов servlet-mapping для сервлетов назначения. Метод getNamedDispatcher() принимает в качестве параметра зарегистрированное в web.xml имя сервлета. В следующем примере показан тот же сервлет, но с использованием метода getNamedDispatcher() и зарегистрированного имени сервлета.

package ru.topcode.servletconstrainttest;
 
import java.io.IOException;
import java.rmi.ServerException;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
 
/**
 *
 * @author dmitrii.leontiev
 */
public class Controller extends HttpServlet {
 
    @Override
    public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        RequestDispatcher dispatcher = null;
        String param = request.getParameter("go");
 
        if (param == null) {
            throw new ServerException("Параметр не задан");
        } else if (param.equals("weather")) {
            // dispatcher = request.getRequestDispatcher("/weather");
            dispatcher = getServletContext().getNamedDispatcher("Weather");
        } else if (param.equals("maps")) {
            // dispatcher = request.getRequestDispatcher("/maps");
            dispatcher = getServletContext().getNamedDispatcher("Maps");
        } else {
            throw new ServerException("Неправильный параметр");
        }
 
        if (dispatcher != null) {
            dispatcher.forward(request, response);
        } else {
            throw new ServerException("Dispather is NULL");
        }
    }
}

Если ServletContext вернет null из-за того, что кто-то пропустил в web.xml требуемый XML-элемент, то doGet() вызовет исключение ServletException с объяснением, что объект dispatcher пуст.

Сервлет Maps я описывать в этом посте не буду, т.к. он полностью аналогичен сервлету Weather. Если хотите, то можете скопировать сервлет Weather, переименовать его в Maps и создать для него соответствующий маппинг со всеми ограничениями в дескрипторе развертывания.

В следующих постах мы рассмотрим альтернативную стратегию – использование слушателей (listener) для проверки запроса до того как он найдет путь к сервлету.