Bezpečnost aplikačního serveru JBoss

2. 4. 2008
Doba čtení: 12 minut

Sdílet

Dnes se seznámíme s možnostmi zabezpečení enterprise aplikací v aplikačním serveru JBoss. Zabezpečení je v současnosti žhavým tématem, protože nikdo nechce zpřístupnit své služby nepovolaným uživatelům. Naštěstí nemusíme kód pro autentizaci a autorizaci psát sami, ale můžeme jednoduše využít nabízené služby.

První webová aplikace

Autentizace (prokázání totožnosti) a autorizace (ověření přístupových práv) jsou řešeny snad v každé aplikaci. Dnes se podíváme na to, jak je možné zabezpečit enterprise aplikace na aplikačním serveru JBoss. Uvidíme také jak se propaguje bezpečnostní kontext do business komponent v aplikaci. Pro tento účel si vyvineme jednoduchou aplikaci, na které si vše vyzkoušíme. Na úvod bych rád zdůraznil, že aplikace je skutečně pouze pro demonstraci bezpečnosti. Proto si dovolíme míchat v JSP stránkách kód v Javě s HTML tagy. V praxi bychom se tomu měli vyhnout použitím technologií pro šablony (např. Tapestry), nebo komponentovým frameworkem JSF.

Začneme u jednoduché webové aplikace (generator_pla­in), která obsahuje jednu JSP stránku a generuje nám náhodné přísloví. Webová aplikace se nasazuje na server v archívu WAR (Web Archive), což je de facto JAR archív s pozměněnou příponou a vnitřní strukturou. Kromě JSP stránky (quote.jsp) se v archívu nalézají soubory:

  • META-INF/MANIFEST.MF  – meta informace o archívu. Povinný soubor, který se automaticky generuje při vytváření archívu.
  • WEB-INF/jboss-web.xml  – vlastnosti webové aplikace typické pro JBossAS. V našem případě zde uvádíme URL kontext, kam se má aplikace nainstalovat.
    <jboss-web>
      <context-root>/quote</context-root>
    </jboss-web>
  • WEB-INF/web.xml  – soubor podle specifikace JEE, popisuje standardní vlastnosti webové aplikace. My zde uvádíme JSP stránku jako servlet. Umožňuje nám to pak přistoupit ke stránce pod jiným URL ( /quote místo  /qoute.jsp).
    <web-app>
        <servlet>
            <servlet-name>QuoteServlet</servlet-name>
            <jsp-file>/quote.jsp</jsp-file>
        </servlet>
    
        <servlet-mapping>
            <servlet-name>QuoteServlet</servlet-name>
            <url-pattern>/quote</url-pattern>
        </servlet-mapping>
    </web-app>
    
    
  • WEB-INF/lib/  – adresář pro knihovny potřebné ve webové aplikaci, například knihovna s JSTL tagy.
  • index.html  – HTML soubor, který se standardně načítá. Provádíme zde pouze přesměrování na generátor přísloví.

Jádrem aplikace je soubor quote.jsp, který obsahuje seznam přísloví, jedno náhodně vybere a vypíše na výstup.

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ page import="java.util.ArrayList" %>
<%@ page import="java.util.Random" %>
<%@ page import="java.util.Arrays" %>

<%
  //Seznam prislovi
        String[] quotes = {
                "Tak dlouho se chodí se džbánem pro vodu až se ucho utrhne.",
                "Jak se do lesa volá, tak se z lesa ozývá.",
                "Kdo chce psa bít, hůl si vždycky najde.",
                "Co není v hlavě, musí být v nohou.",
                "Opakování je matka moudrosti.",
                "Není kouře bez ohně.",
                "Dvakrát do stejné řeky nevstoupíš.",
                "Na hrubý pytel hrubá záplata."
  };
  ArrayList list = new ArrayList(Arrays.asList(quotes));
        Random r = new Random();
        int x = r.nextInt(list.size());
        String saying = (String)list.get(x);
        pageContext.setAttribute("saying", saying);
%>

<html>
  <head>
        <title>Generátor přísloví</title>
        <meta http-equiv="content-type" content="text/html;charset=utf-8" />
  </head>

  <body>
        <c:set var="sessionCount" scope="session"
                        value="${sessionCount + 1}" />
        <h1>Skvělý generátor přísloví</h1>
        <p><strong>${saying}</strong></p>

        <p><small>Během tohoto sezení byla stránka navštívena: ${sessionCount}x</small></p>
  </body>
</html>

Aplikace (stejně jako všechny ostatní v dnešním dílu) je doplněna o sestavovací skript pro Ant ( build.xml) a o soubor build.properties, kde musíte správně nastavit cestu k vaší instalaci aplikačního serveru a název konfigurace, kterou používáte. Sestavení příkladů provedete příkazem ant v adresáři se skriptem. Vystavení na server pak pomocí ant deploy. Odinstalování zase zadáním ant undeploy. Vyčištění zkompilovaných souborů a sestavených archívů zařídí ant clean.

Můžete se podívat do build.xml, jakým způsobem nasazujeme aplikaci na server. Jedná se o pouhé okopírování souboru do adresáře deploy příslušné konfigurace. Využíváme tady technologii hot deployment, kdy AS pravidelně sleduje adresář s aplikacemi a při změně souborů provede i příslušné změny v instalovaných aplikacích a službách.

Po vystavení na server naleznete aplikaci na adrese http://localhos­t:8080/quote/. Při každém načtení by nám měla ukázat náhodné přísloví.

Přidáváme business komponentu

Abychom si mohli ukázat, jak je možné jednoduše předat bezpečnostní kontext do business vrstvy, přesuneme generování přísloví do stateless session bean (generator_ejb). Tedy do bezstavové business komponenty. Přidáme zároveň metodu getAllQuotes(), která bude sloužit administrátorovi pro přehled o příslovích. Použijeme pouze anotaci pro vzdálené rozhraní (@Remote), protože JBoss AS automaticky použije lokální volání, pokud volající i volaný kód běží v rámci jednoho virtuálního stroje (JVM).

@Stateless
@Remote(cz.root.jboss.security.QuoteGenerator.class)
public class QuoteGeneratorBean implements QuoteGenerator {
        private final static String[] quotes = {
                "Tak dlouho se chodí se džbánem pro vodu až se ucho utrhne.",
                "Jak se do lesa volá, tak se z lesa ozývá.",
                "Kdo chce psa bít, hůl si vždycky najde.",
                "Co není v hlavě, musí být v nohou.",
                "Opakování je matka moudrosti.",
                "Není kouře bez ohně.",
                "Dvakrát do stejné řeky nevstoupíš.",
                "Na hrubý pytel hrubá záplata."
        };
        private final static List<String> list = Arrays.asList(quotes);

        public List getAllQuotes() {
                return list;
        }

        public String getRandomQuote() {
                Random r = new Random();
                int x = r.nextInt(list.size());
                return list.get(x);
        }
}

K této komponentě musíme mít také rozhraní.

public interface QuoteGenerator {
    public List getAllQuotes();
    public String getRandomQuote();
}

Business komponenta již nemůže být součástí webové aplikace. Musíme proto vytvořit enterprise aplikaci. Ta je zpravidla zabalena v archívu EAR (Enterprise Application Archive). Součástí tohoto archívu jsou jednotlivé moduly aplikace. Máme zde webový modul (upravená původní webová aplikace) a EJB modul (business komponenta). EJB modul je obyčejný JAR archív, který obsahuje pouze zkompilovanou business komponentu. Kromě uvedených modulů ( quote.war a quotebean.jar) obsahuje EAR archív také popis enterprise aplikace v souboru  META-INF/application.xml.

<application>
    <display-name>Generator Přísloví</display-name>

    <module>
        <web>
            <web-uri>quote.war</web-uri>
            <context-root>/quote</context-root>
        </web>

    </module>
    <module>
        <ejb>quotebean.jar</ejb>
    </module>
</application>

Úprava původní webové aplikace spočívá v tom, že budeme používat EJB místo přímého generování přísloví. Zde musím bohužel zmínit jednu smutnou skutečnost. Pro přístup k EJB komponentě by mělo stačit uvést v JSP stránce následující kód. (Vykřičník zajistí umístění kódu přímo do těla třídy, která vznikne přeložením JSP stránky, nikoliv do těla metody generující stránku. Tam totiž anotace @EJB nemůže být umístěná.)

<%!
  @EJB QuoteGenerator generator;
%>

AS by měl automaticky injektovat danou business komponentu. Nesmíme ale zapomínat, že pracujeme s beta verzí aplikačního serveru. Díky již evidované chybě v AS není možné tento způsob přístupu použít. Ve finální verzi to samozřejmě možné bude, protože se jedná o poměrně zásadní záležitost. Nyní však musíme komponentu ručně vyhledat za použití JNDI.

<%
   InitialContext ic = new InitialContext();
   QuoteGenerator gener = (QuoteGenerator) ic.lookup("quote/QuoteGeneratorBean/remote");
   pageContext.setAttribute("saying", gener.getRandomQuote());
%>

V kódu JSP stránky jsou pro budoucí použití ponechány oba způsoby přístupu ke komponentě.

Aplikaci opět můžete zkusit vystavit a spustit. Na stránce s generovaným příslovím se objeví nový odkaz pro administrátora, který odkazuje na stránku se seznamem všech přísloví.

Definice bezpečnostní domény

Bezpečnostní doména (security domain) nám říká, kde má AS vzít potřebné údaje (uživatele a zařazení do rolí) pro autentizaci a autorizaci uživatele. Bezpečnostní domény jsou definované pro každou serverovou konfiguraci zvlášť a nachází se v souboru server/{vaše konfigurace}/conf/login-config.xml. Jako zdroj potřebných dat je možné použít soubory, databázi, LDAP, nebo třeba naprogramovat vlastní login modul podle vašich potřeb. Jednou ze základních domén v souboru je jmx-console. Používá soubory server/{vaše konfigurace}/conf/props/jmx-console-users.properties a jmx-console-roles.properties na stejné cestě. V konfiguraci default definuje uživatele admin s heslem admin, který má roli JBossAdmin.

JMX konzole, kterou jsme si představili v jednom z předchozích dílů, je v konfiguraci default standardně nezabezpečená. Její konfigurační soubory však obsahují potřebné definice. Stačí je jen odkomentovat. My tyto definice zkopírujeme do naší aplikace a zabezpečíme přístup k seznamu všech přísloví pomocí bezpečnostní domény jmx-console.

Zabezpečení části aplikace

V ukázkové aplikaci (generate_secu­red) upravíme soubor metadata/jboss-web.xml, aby odkazoval na danou bezpečnostní doménu.

<security-domain>>java:/jaas/jmx-console</security-domain>

Potom v souboru metadata/web.xml nastavíme, která část aplikace má být jakým způsobem zabezpečená.

    <security-constraint>
        <web-resource-collection>
            <web-resource-name>HtmlAdaptor</web-resource-name>

            <description>Ukazkova konfigurace, ktera dovoluje pristup jen uzivatelum ve skupine JBossAdmin</description>
            <url-pattern>/admin/*</url-pattern>
            <http-method>GET</http-method>
            <http-method>POST</http-method>

        </web-resource-collection>
        <auth-constraint>
            <role-name>JBossAdmin</role-name>
        </auth-constraint>
    </security-constraint>

    <login-config>
        <auth-method>BASIC</auth-method>
        <realm-name>Quote</realm-name>
    </login-config>

    <security-role>
        <role-name>JBossAdmin</role-name>
    </security-role>

Tag <security-role> importuje role z bezpečnostní domény. <login-config> definuje způsob autentizace uživatele. Kromě metody BASIC je možné použít DIGEST (standardní HTTP autentizace pomocí hlaviček), CLIENT-CERT (uživatelský certifikát X.509) a vlastní formulář v aplikaci (očekávají se pole j_username a j_password). Následně nám <security-constraint> omezí přístup k URL adresám /admin/* (vztaženo ke kořeni aplikace) pouze pro uživatele ve skupině JBossAdmin.

Zabezpečení business komponenty

Požadavek na určitou bezpečnostní doménu, roli a uživatele můžeme umístit i do business komponenty. Při volání se pak údaje o uživateli a skupinách transparentně přenesou z JSP stránky do komponenty. Bezpečnostní doménu je možné definovat pouze na úrovni celé komponenty. Nelze proto k některým metodám přistupovat z nezabezpečené části aplikaci a u jiných vyžadovat určitou roli nebo uživatele. Pomocí tagu <unauthenticated-principal> je možné definovat výchozího uživatele, pokud nedojde k přihlášení. V takovém případě je možné přistupovat ke komponentě i z nezabezpečené části stránek. Musíme ale mít na paměti, že stále máme uživatele jakoby přihlášeného. Může tedy vyvolávat všechny metody v dané bezpečnostní doméně, které nemají omezení rolí nebo konkrétním uživatelem.

V další ukázkové aplikaci (generate_roles) pro jednoduchost zabezpečíme celou aplikaci (úpravou <url-pattern>v souboru web.xml). Do zdrojového kódu EJB doplníme doménu a požadavek na určitou roli u metody getAllQuotes(). Volání druhé metody povolíme všem.

@SecurityDomain("jmx-console")
public class QuoteGeneratorBean implements QuoteGenerator {
    ...
        @RolesAllowed("QuoteLister")
        public List getAllQuotes() {
                return list;
        }

        @PermitAll
        public String getRandomQuote() {
                Random r = new Random();
                int x = r.nextInt(list.size());
                return list.get(x);
        }
}

Schválně jsme použili neexistující roli, abychom viděli, co se stane při neoprávněném přístupu. Správně bychom neautorizovaného uživatele takovou metodu nenechali vyvolat, aby nedošlo k zobrazení výjimky. Takový uživatel by ale mohl odhalit například URL adresu stránky, ke které nemá přístup. Tím, že by byl přihlášen do příslušné domény, by se mu ale zobrazila. Zabezpečení na úrovni business komponent takové volání už zastaví.

Vlastní doména a uživatelé v databázi

V poslední ukázkové aplikaci (generate_db) si vytvoříme vlastní bezpečnostní doménu, která bude číst uživatele z databáze. Použijeme k tomu databázi MySQL. Nejdříve si vytvoříme tabulky pro ukládání dat a vytvoříme testovací data.

CREATE TABLE users(login VARCHAR(64) PRIMARY KEY, passwd VARCHAR(64));
CREATE TABLE user_roles(login VARCHAR(64), role VARCHAR(64));

INSERT INTO users VALUES ("pepa", "abc123");
INSERT INTO users VALUES ("franta", "123456");
INSERT INTO user_roles VALUES ("pepa", "user");
INSERT INTO user_roles VALUES ("pepa", "QuoteLister");
INSERT INTO user_roles VALUES ("franta", "user");

Poté je potřeba nadefinovat datový zdroj (viz soubor  extra/quote-ds.xml).

<datasources>
  <local-tx-datasource>
    <jndi-name>QuoteDS</jndi-name>

    <connection-url>jdbc:mysql://localhost:1521/jmeno_databaze</connection-url>
    <driver-class>com.mysql.jdbc.Driver</driver-class>
    <user-name>uzivatel</user-name>
    <password>heslo</password>

    <transaction-isolation>TRANSACTION_READ_COMMITTED</transaction-isolation>
    <exception-sorter-class-name>org.jboss.resource.adapter.jdbc.vendor.MySQLExceptionSorter</exception-sorter-class-name>
    <min-pool-size>5</min-pool-size>
    <max-pool-size>15</max-pool-size>

    <metadata>
       <type-mapping>mySQL</type-mapping>
    </metadata>
  </local-tx-datasource>
</datasources>

Pro správnou funkci je potřeba nastavit jméno databáze, uživatelské jméno a heslo. Je také potřeba do příslušné konfigurace do adresáře lib nainstalovat JDBC ovladač pro MySQL (naleznete ho v adresáři extra u aplikace). Po okopírování souboru je nutný restart serveru. Samotný soubor s definicí datového zdroje stačí okopírovat do adresáře deploy.

Dalším krokem je definice bezpečnostní domény v souboru conf/login-config.xml příslušné konfigurace.

  <application-policy name="quote">

    <authentication>
      <login-module code="org.jboss.security.auth.spi.DatabaseServerLoginModule"
        flag="required">
        <module-option name="unauthenticatedIdentity">guest</module-option>
        <module-option name="dsJndiName">java:/QuoteDS</module-option>
        <module-option name="principalsQuery">SELECT passwd FROM users WHERE login=?</module-option>

        <module-option name="rolesQuery">SELECT role, 'Roles' FROM user_roles WHERE login=?</module-option>
      </login-module>
    </authentication>
  </application-policy>

Vidíme, že i na tomto místě jsme mohli specifikovat výchozího uživatele (guest). Zadali jsme náš datový zdroj a dotazy pro přečtení hesla a rolí. Po změně v nastavení bezpečnostních domén je opět potřeba server restartovat.

Pokud vše funguje správně, měli bychom nyní mít neautentizovaný přístup k metodě getRandomQuote() jako uživatel guest a uživatel pepa by měl mít přístup k seznamu všech přísloví. Pokud se přihlásíme jako franta, zobrazí se nám zpráva o zakázaném přístupu.

Není úplně ideální mít v databázi nezabezpečená hesla. Je možné použít šifrování. Například MD5. Stačí upravit definici bezpečnostní domény a do databáze uložit hesla šifrovaně.

ict ve školství 24

        <module-option name="hashAlgorithm">MD5</module-option>

        <module-option name="hashEncoding">BASE64</module-option>

Pro vygenerování šifrovaných hesel můžeme na Linuxu použít OpenSSL.

$ echo -n moje_heslo | openssl dgst -md5 -binary | openssl base64

Závěr

V dnešním dílu jsme si ukázali možnosti zabezpečení aplikací na aplikačním serveru JBoss. Předvedli jsme si mechanismus propagace bezpečnostní identity do business komponenty, konfiguraci bezpečnostních domén a jejich napojení na databázi MySQL. Vše se konfiguruje snadno, bez složitého programování. Je ovšem potřeba věnovat velkou pozornost pečlivému nastavení přístupových práv, zařazeni uživatelů do rolí a omezení spouštění metod jen příslušným rolím…

Autor článku