Filtry

V této části si ukážeme, jak můžeme omezit přístup k servletům pomocí filtrů. A nejen to, jedná se jen o jednu z věcí, které můžeme pomocí filtrů udělat.

Co je Filtr

Filtr je objekt, který se spouští před zpracováním a po zpracování requestu. Používá se třeba k implementaci autentizace a autorizace, k logování, a tak podobně. Výhoda filtru je ta, že jej můžeme použít na více servletů. Není nijak závislý na specifickém servletu. Díky tomu je pro nás i jednodušší jeho správa.

Následující diagram ukazuje, kdy se filter spouští.

Diagram, ukazující kdy se filtr spouští

Filtrů může být pro servlet nastavených klidně i více. Filtry se potom budou volat jeden po druhém až k servletu.

Diagram, ukazující kdy se spouští řetězec filtrů

Rozhraní Filter

Filtr můžeme vytvořit implementací Filter rozhraní. Toto rozhraní definuje tři metody, které popisuje následující tabulka.

MetodaNávratový typPopis
init(FilterConfig filterConfig)voidVolána servlet containerem (Tomcatem) při vytvoření filtru. Slouží k inicializaci (můžeme třeba načíst nějakou konfiguraci atp.).
doFilter(ServletRequest request, ServletResponse response, FilterChain chain)voidVolána pro provedení filteru (když na server přijde request, nebo je filter další na řadě v řetězci filterů).
destroy()voidVolána servlet containerem (Tomcatem), když se filtr chystá vyřadit z provozu.

Metody init a destroy mají defaultní implementaci. Pokud je tedy nepotřebujeme, nemusíme je implementovat. Následující ukázka ukazuje příklad, jak může filtr vypadat.

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;

public class RequestLoggingFilter implements Filter {
    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
        // provedení nějaké akce před zpracováním requestu (v tomto případě vypsání IP adresy, ze které byl request poslán)
        String ipAdresa = req.getRemoteAddr();
        String requestUri = ((HttpServletRequest) req).getRequestURI();
        System.out.println("Request obdržen z " + ipAdresa + " pro " + requestUri);

        // poslání requestu pro zpracování do servletu (nebo dalšího filtru v pořadí)
        chain.doFilter(req, res);

        // provedení nějaké akce po zpracování requestu (v tomto případě vypsání response status kódu)
        int statusCode = ((HttpServletResponse) response).getStatus();
        System.out.println("Response status kód: " + statusCode);
    }
}

Filter chain

V metodě doFilter se jako poslední parametr předává objekt typu FilterChain. Jedná se o rozhraní, které definuje jen jednu metodu jménem doFilter. Tu ve filtru používáme k tomu, abychom předali request ke zpracování servletu nebo dalšímu filtru v řetězci. Slovo chain v češtině vlastně znamená řetězec. FilterChain tedy představuje takový řetězec filtrů směřující k servletu. Pokud v nějakém filtru metodu doFilter objektu FilterChain nezavoláme, tak se řetězec přeruší a request do servletu nedoputuje.

Vytvoření filtru

Pro ukázku si vytvoříme projekt, ve kterém budeme mít dva servlety, které jen vyrenderují stránku. Poté vytvoříme filtr, který na tyto servlety aplikujeme. Bude sloužit k tomu, že nám bude na serveru do konzole vypisovat, kdy proběhl request a na jaký servlet. Založíme tedy nový Maven projekt a vytvoříme první servlet. Ukazuje jej následující ukázka.

  • src
    • main
      • java
      • resources
      • webapp
    • test
      • java
      • resources
  • target
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>io.github.jirkasa</groupId>
    <artifactId>logging-filter</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>war</packaging>
    <name>logging-filter</name>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>17</java.version>
        <maven.compiler.source>${java.version}</maven.compiler.source>
        <maven.compiler.target>${java.version}</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <version>4.0.1</version>
            <scope>provided</scope>
        </dependency>
    </dependencies>
</project>
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;

public class Servlet1 extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException {
        res.setContentType("text/html; charset=utf-8");
        try (PrintWriter out = res.getWriter()) {
            out.println("<!DOCTYPE html>");
            out.println("<html>");
            out.println("<head>");
            out.println("<meta charset=\"UTF-8\">");
            out.println("<title>Servlet1</title>");
            out.println("</head>");
            out.println("<body>");
            out.println("<h1>Servlet1</h1>");
            out.println("<p>Stránka pro Servlet1.</p>");
            out.println("</body>");
            out.println("</html>");
        }
    }
}

Druhý servlet bude v podstatě stejný.

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;

public class Servlet2 extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException {
        res.setContentType("text/html; charset=utf-8");
        try (PrintWriter out = res.getWriter()) {
            out.println("<!DOCTYPE html>");
            out.println("<html>");
            out.println("<head>");
            out.println("<meta charset=\"UTF-8\">");
            out.println("<title>Servlet2</title>");
            out.println("</head>");
            out.println("<body>");
            out.println("<h1>Servlet2</h1>");
            out.println("<p>Stránka pro Servlet2.</p>");
            out.println("</body>");
            out.println("</html>");
        }
    }
}

Ve web.xml můžeme servlety namapovat třeba na "/servlet-1" a "/servlet-2", jak ukazuje následující ukázka.

  • src/main/webapp/WEB-INF
  • src/main/webapp/WEB-INF
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:web="http://Java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" 
id="WebApp_ID" version="3.0">
    <servlet>
        <servlet-name>Servlet1</servlet-name>
        <servlet-class>Servlet1</servlet-class>
    </servlet>
    <servlet>
        <servlet-name>Servlet2</servlet-name>
        <servlet-class>Servlet2</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>Servlet1</servlet-name>
        <url-pattern>/servlet-1</url-pattern>
    </servlet-mapping>
    <servlet-mapping>
        <servlet-name>Servlet2</servlet-name>
        <url-pattern>/servlet-2</url-pattern>
    </servlet-mapping>
</web-app>

Teď se již můžeme pustit do toho hlavního, o co nám tu jde. Vytvoříme si filtr, který nám bude do konzole vypisovat, že proběhl request. Filtr můžeme pojmenovat třeba jako LoggingFilter. Následující ukázka ukazuje jeho implementaci.

import java.io.IOException;
import java.util.Date;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;

public class LoggingFilter implements Filter {
    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
        String requestUri = ((HttpServletRequest) req).getRequestURI();
        // vypsání do konzole, že proběhl request
        System.out.println(new Date() + ": proběhl request na " + requestUri);
        
        // předání requestu servletu pro zpracování
        chain.doFilter(req, res);
    }
}

Filtry můžeme namapovat na jakýkoliv servlet chceme. To představuje jejich výhodu. Můžeme je přidávat a odstraňovat ze servletů, aniž bychom museli měnit servlety samotné. Dělá se to v souboru web.xml. Nejdříve musíme filtr nadefinovat pomocí elementu filter a poté jej namapovat pomocí elementu filter-mapping. Je to podobné jako u servletů. Element filter má podelement filter-name, který představuje název filtru a podelement filter-class, kde se definuje třída pro filtr. Element filter-mapping má podelement filter-name, který určuje pro jaký filtr provádíme mapování a podelement url-pattern, ve kterém definujeme vzor, podle kterého se filtr na servlety použije. Jelikož chceme náš filtr použít na všechny servlety v aplikaci, tak můžeme jako url-pattern nastavit "/*". Následující ukázka ukazuje náš upravený soubor web.xml.

  • src/main/webapp/WEB-INF
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:web="http://Java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" 
id="WebApp_ID" version="3.0">
    <servlet>
        <servlet-name>Servlet1</servlet-name>
        <servlet-class>Servlet1</servlet-class>
    </servlet>
    <servlet>
        <servlet-name>Servlet2</servlet-name>
        <servlet-class>Servlet2</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>Servlet1</servlet-name>
        <url-pattern>/servlet-1</url-pattern>
    </servlet-mapping>
    <servlet-mapping>
        <servlet-name>Servlet2</servlet-name>
        <url-pattern>/servlet-2</url-pattern>
    </servlet-mapping>
    <filter>
        <filter-name>Logging</filter-name>
        <filter-class>LoggingFilter</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>Logging</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>
</web-app>

Pokud si nyní zkusíte jeden ze servletů otevřít, tak se vám v konzoli ve vašem IDE vypíše, že proběhl request.

Sat Dec 09 21:30:32 CET 2023: proběhl request na /logging-filter/servlet-2

Zablokování přístupu k vybraným servletům

Pokud ve filtru nezavoláme metodu doFilter FilterChain objektu, tak se request nedostane k cílovému servletu a nebude jím tedy zpracován. Tímto způsobem bychom tedy mohli vyřešit problém, se kterým jsme se setkali v minulé části.

Pro ukázku si vytvoříme projekt, kde budeme mít servlet renderující stránku. Přístup k tomuto servletu poté zamezíme pomocí filtru. Založíme tedy nový Maven projekt a vytvoříme servlet, který můžeme pojmenovat třeba jako MujServlet. Ukazuje jej následující ukázka.

  • src
    • main
      • java
      • resources
      • webapp
    • test
      • java
      • resources
  • target
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>io.github.jirkasa</groupId>
    <artifactId>blocking-filter</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>war</packaging>
    <name>blocking-filter</name>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>17</java.version>
        <maven.compiler.source>${java.version}</maven.compiler.source>
        <maven.compiler.target>${java.version}</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <version>4.0.1</version>
            <scope>provided</scope>
        </dependency>
    </dependencies>
</project>
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;

public class MujServlet extends HttpServlet{
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException {
        res.setContentType("text/html; charset=utf-8");
        try (PrintWriter out = res.getWriter()) {
            out.println("<!DOCTYPE html>");
            out.println("<html>");
            out.println("<head>");
            out.println("<meta charset=\"UTF-8\">");
            out.println("<title>Stránka</title>");
            out.println("</head>");
            out.println("<body>");
            out.println("<h1>Stránka</h1>");
            out.println("<p>Nějaký text stránky.</p>");
            out.println("</body>");
            out.println("</html>");
        }
    }
}

Servlet můžeme namapovat třeba na "/stranka". Vytvoříme tedy soubor web.xml a uděláme to.

  • src/main/webapp/WEB-INF
  • src/main/webapp/WEB-INF
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:web="http://Java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" 
id="WebApp_ID" version="3.0">
    <servlet>
        <servlet-name>Stranka</servlet-name>
        <servlet-class>MujServlet</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>Stranka</servlet-name>
        <url-pattern>/stranka</url-pattern>
    </servlet-mapping>
</web-app>

Pokud si aplikaci spustíte a otevřete http://localhost:8080/blocking-filter/stranka, tak uvidíte stránku, kterou ukazuje následující obrázek.

Stránka

Nyní si náš vytvořený servlet zkusíme zablokovat, aby na něj uživatel nemohl poslat request. Vytvoříme si na to filtr, který pojmenujeme třeba jako BlockingFilter. V tomto filtru nezavoláme metodu doFilter FilterChain objektu, ale pošleme klientovi chybu prostřednictvím sendError metody HttpServletResponse objektu. Následující ukázka filtr ukazuje. Metodou sendError posíláme chybu se status kódem 403, což znamená, že server klientovi odmítá vrátit zdroj, pro který poslal request. Namísto konstanty SC_FORBIDDEN bychom klidně mohli použít i číslo 403.

import java.io.IOException;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletResponse;

public class BlockingFilter implements Filter {
    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
        // castnutí ServletResponse na HttpServletResponse
        HttpServletResponse httpRes = (HttpServletResponse) res;
        
        // poslání chyby se status kódem 403
        httpRes.sendError(HttpServletResponse.SC_FORBIDDEN, "Přístup odepřen");
    }
}

Ve web.xml souboru si můžeme filtr namapovat na náš servlet, který renderuje stránku. Tím pádem již nebudeme mít možnost, jak se k němu jako uživatelé dostat.

  • src/main/webapp/WEB-INF
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:web="http://Java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" 
id="WebApp_ID" version="3.0">
    <servlet>
        <servlet-name>Stranka</servlet-name>
        <servlet-class>MujServlet</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>Stranka</servlet-name>
        <url-pattern>/stranka</url-pattern>
    </servlet-mapping>
    <filter>
        <filter-name>BlockingFilter</filter-name>
        <filter-class>BlockingFilter</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>BlockingFilter</filter-name>
        <url-pattern>/stranka</url-pattern>
    </filter-mapping>
</web-app>

Pokud si aplikaci restartujete a zkusíte znovu navštívit http://localhost:8080/blocking-filter/stranka, tak se vám již nezobrazí stránka jako předtím, ale zobrazí se vám chybová stránka.

Chybová stránka

Filter Config

Podobně jako si můžeme v souboru web.xml nadefinovat inicializační parametry pro servlety, můžeme si je nadefinovat i pro filtry. Jedná se o hodnoty, které Tomcat předá do filtru když jej inicializuje prostřednictvím init metody. Metoda init přijímá jako parametr objekt typu FilterConfig, ze kterého můžeme inicializační parametry dostat. Jedná se o rozhraní, které definuje čtyři metody, které popisuje následující tabulka.

MetodaNávratový typPopis
getFilterName()StringVrací název filtru, který je specifikován v souboru web.xml.
getInitParameter(String name)StringVrací hodnotu inicializačního parametru.
getInitParameterNames()Enumeration<String>Vrací enumeraci všech jmen inicializačních parametrů pro filtr.
getServletContext()ServletContextVrací referenci k servlet contextu, ve kterém se filtr spouští.

Pro ukázku použití inicializačních parametrů si uprávíme projekt, který jsme si vytvořili pro ukázku zablokování přístupu k servletu. V souboru web.xml si budeme moci nastavit, jaká chybová zpráva se pro zablokovaný servlet vypíše. K definování inicializačních parametrů pro filtry se stejně jako u servletů používá element init-param. Následující ukázka ukazuje náš upravený web.xml soubor.

  • src/main/webapp/WEB-INF
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:web="http://Java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" 
id="WebApp_ID" version="3.0">
    <servlet>
        <servlet-name>Stranka</servlet-name>
        <servlet-class>MujServlet</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>Stranka</servlet-name>
        <url-pattern>/stranka</url-pattern>
    </servlet-mapping>
    <filter>
        <filter-name>BlockingFilter</filter-name>
        <filter-class>BlockingFilter</filter-class>
        <init-param>
            <param-name>error_message</param-name>
            <param-value>Přístup k tomuto servletu je zablokovaný.</param-value>
        </init-param>
    </filter>
    <filter-mapping>
        <filter-name>BlockingFilter</filter-name>
        <url-pattern>/stranka</url-pattern>
    </filter-mapping>
</web-app>

Teď můžeme nadefinovaný inicializační parametr ve filtru získat a použít. Implementujeme metodu init, ve které z FilterConfig objektu parametr získáme a uložíme si jej. Poté jej budeme používat v metodě doFilter. Následující ukázka ukazuje upravený kód.

import java.io.IOException;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletResponse;

public class BlockingFilter implements Filter {
    private String errorMessage;
    
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        // získání inicializačního parametru
        this.errorMessage = filterConfig.getInitParameter("error_message");
    }
    
    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
        // castnutí ServletResponse na HttpServletResponse
        HttpServletResponse httpRes = (HttpServletResponse) res;
        
        // poslání chyby se status kódem 403
        httpRes.sendError(HttpServletResponse.SC_FORBIDDEN, this.errorMessage);
    }
}

Pokud nyní zkusíte servlet navštívit na http://localhost:8080/blocking-filter/stranka, tak uvidíte, že se chybová zpráva nastavená jako inicializační parametr na chybové stránce použila.

Chybová stránka obsahující chybovou zprávu nastavenou přes inicializační parametr

Pro tuto část je to vše. Nyní již víte k čemu filtry slouží a jak je použít. V příští části se začneme zabývat JSP.