16 stycznia 2013

Zmiany w Java EE 6 w javax.servlet.http.HttpServletRequest - metody login oraz logout

Kolejny dzień na StackOverflow i lista do rozpoznania zaczyna niebezpiecznie przybierać na wadze - pojawiają się m.in. zadania związane z rozpoznaniem zmian w Java EE 6. Okazuje się, że przespałem kilka i kiedy trafiłem na How to redirect to request page after basic athentication on websphere nie mogłem uwierzyć własnym oczom - metoda javax.servlet.http.HttpServletRequest.login(java.lang.String username, java.lang.String password) throws ServletException istnieje?! Byłem bliski odpowiedzi, w której niemalże zjechałbym pytającego, jak w ogóle ta metoda mogła mu przyjść do głowy, której nazwa była zaskakująco znajoma - pewnie jedna z bardziej popularnych nazw dla metody, której zadaniem jest...tak dokładnie...uwierzytelnienie użytkownika. Jest również logout i kilka innych oznaczonych Since: Servlet 3.0.

Czy istnieje sposób, aby otrzymać listę wszystkich metod oznaczonych Since: Servlet 3.0 w Javadoc?

Do rozpoznania tematu korzystam bezpośrednio ze specyfikacji JSR-315 Java Servlet 3.0 i zacznę od dwóch nowych metod w javax.servlet.http.HttpServletRequest - login and logout. Specyfikacja poświęca im osobny rozdział 13.10 Login and Logout (strona 162).

Instancja HttpServletRequest jest nam (programistom) dana w trakcie obsługi żądania przez kontener. Zawiera informacje z nagłówka (ang. header) i treści (ang. body) żądania HTTP. Parametry są typu String, a wraz z wartościami tworzą zbiór par (parametr, wartość) - mapę. Jak mi to doskonale wpasowuje się w programowanie z funkcyjnym Clojure, w którym struktury są niezmienne (jak HttpServletRequest) i dostęp do nich jest jakby przyjemniejszy (wybacz, nie mogłem się oprzeć, aby tego tutaj nie wtrącić).

Ciekawostka: Dla danego parametru może istnieć wiele wartości, w postaci tablicy. Dostęp do nich odbywa się za pomocą getParameterValues. Istnieje również getParameter, który zwraca pojedynczą wartość. Pytanie certyfikacyjne mogłoby być: "Który w stosunku do tablicy zwracanej przez getParameterValues?" Pierwszy z pewnością, ale czy pierwszy jest pojęciem trwałym i wielokrotne wykonanie getParameter zawsze zwróci tą samą wartość? Specyfikacja (strona 43) wymusza, aby był to pierwszy element tablicy zwracanej przez getParameterValues i zawsze ten sam. Kolejność wyznaczana jest przez "query string", aby dołączyć do nich te z treści żądania (strona 44).

Ciekawostek cd: Pliki w META-INF/resources w pliku JAR są dostępne jedynie w sytuacji, kiedy kontener rozpakował je przy wywołaniu metody ServletContext.getRealPath() (strona 48). Nie wiedziałem o istnieniu tego specjalnego katalogu (!)

Wracając do HttpServletRequest, login i logout.

Przy obsłudze żądania serwer aplikacyjny określa tożsamość nadawcy. Jest ona niezmienna, aż do momentu poprawnego wywołania metod authenticate, login lub logout na tym żądaniu (strona 162). Metody getUserPrincipal() oraz getRemoteUser() zwrócą null dla sytuacji, w których nadawca żądania nie został uwierzytelniony (strona 163). Później jeszcze pojawia się metoda getAuthType, która również zwróci null.

Serwer (w postaci kontenera servletów/webowego) może stworzyć sesję HTTP na potrzeby utrzymywania stanu uwierzytelnienia użytkownika. Jeśli utworzy ją programista przed uwierzytelnieniem użytkownika, to po poprawnym uwierzytelnieniu sesja musi być taka sama jak przed (strona 163). W ten sposób możliwe jest utrzymanie informacji w pojedynczej sesji przed, w trakcie i po uwierzytelnieniu nadawcy (użytkownika).

W javadoc dla HttpServletRequest.html#login(java.lang.String, java.lang.String) można przeczytać:

Wywołanie metody login uruchamia proces uwierzytelnienia zgodnie z konfiguracją ServletContext (cóż za magia, a pewnie chodzi o konfigurację aplikacji webowej przez web.xml czy adnotacje). Metoda nie spowoduje rzucenia wyjątkiem ServletException, jeśli mechanizm uwierzytelniający wspiera sprawdzenie pary (login, hasło), nadawca nie został jeszcze określony - wszystkie metody getUserPrincipal, getRemoteUser oraz getAuthType zwracają null oraz para (login, hasło) jest poprawna.
Poprawne wykonanie metody sprawia, że metody getUserPrincipal, getRemoteUser oraz getAuthType zwracają wartość nie-null, tj. dane nadawcy.

Proste i przyjemne, co? Za wyjątkiem konfiguracji bezpieczeństwa, tj. uwierzytelnienia i autoryzacji przez kontener, co wcale nie musi być zadaniem trywialnym. Mimo uproszczeń w konfiguracji w serwerach aplikacyjnych, to wciąż niezbyt popularny aspekt zarządzania nimi.

Ja skorzystam z przykładu uruchomionego na IBM WebSphere Application Server 8.5.0.1.
package pl.japila.javaee6;

import java.io.IOException;
import java.io.PrintWriter;
import java.security.Principal;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@WebServlet("/login")
public class LoginServlet extends HttpServlet {
    private static final long serialVersionUID = 1L;
    private final String HTML_BR = "<b r>"; // for Blogger only
    
    private void displayCredentials(PrintWriter out, HttpServletRequest request) {
        Principal principal = request.getUserPrincipal();
        String user = request.getRemoteUser();
        out.printf("Principal: %s%s", principal, HTML_BR);
        out.printf("User: %s%s", user, HTML_BR);
        out.printf("AuthType: %s%s", request.getAuthType(), HTML_BR);
    }

    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        response.setContentType("text/html; charset=UTF-8");
        PrintWriter out = response.getWriter();
        out.printf("

Step 1. nulls only (no user authenticated)

"); displayCredentials(out, request); out.printf("

Step 2. Exception for jacek/password (no user 'jacek' available)

"); try { request.login("jacek", "password"); } catch (ServletException expected) { out.printf("Exception (expected): %s%s", expected, HTML_BR); } displayCredentials(out, request); out.printf("

Step 3. Authentication of admin/admin..."); try { request.login("admin", "admin"); } catch (ServletException unexpected) { out.printf("FAILED

"); out.printf("Exception (UNexpected): %s%s", unexpected, HTML_BR); } out.printf("SUCCESS"); displayCredentials(out, request); out.printf("

Step 4. Logging out..."); try { request.logout(); } catch (ServletException unexpected) { out.printf("FAILED

"); out.printf("Exception (UNexpected): %s%s", unexpected, HTML_BR); } out.printf("SUCCESS"); displayCredentials(out, request); } }
Wykonanie powyższego servletu, jakkolwiek poprawne od strony użytkownika, to w dzienniku można zauważyć wyjątek SECJ0369E: Authentication failed when using LTPA. The exception is com.ibm.websphere.wim.exception.PasswordCheckFailedException: CWWIM4537E No principal is found from the 'jacek' principal name. Stąd opakowanie wywołania request.login("jacek", "password") dla nieistniejącego użytkownika w blok try-catch.

8 komentarzy:

  1. Ja te metody znalazłem dopiero przy changelogu do Spring Security 3.2 M1 (http://blog.springsource.org/2012/12/17/spring-security-3-2-m1-highlights-servlet-3-api-support/).

    Coś słabo z naszą znajomością JEE :)

    OdpowiedzUsuń
    Odpowiedzi
    1. I coś mi mówi, że to taki trend teraz, kiedy ludziska rozproszyli się na nowe języki, technologie, podejścia, a stare dobre Java EE poszło w odstawkę mimo, że wciąż w użyciu i znajomość na średnim poziomie jest zakładana (to na pewno mój przypadek).

      Mam wrażenie, że wielu zakłada znajomość Java EE u siebie i u współtowarzyszy, ale dopiero, kiedy jest wpadka/obsuwa, szuka się biedaka, który dałby radę rozkminić temat, dlaczego tak to działa, a nie inaczej (to również mój przypadek, co widać po wczorajszym wpisie).

      Nie wspomnę już częstego pisania własnych rozwiązań, kiedy takowe istnieją w Java EE - to już uważam za totalną masakrę! (to przypadek z grupy WJUG).

      Pora to zmienić i stąd ten wpis. Się przypomni to i owo, i będzie znowu git.

      Usuń
    2. No właśnie nie jestem pewien, czy pora to zmienić.

      Znam jako tako kilka specyfikacji z których korzystam, a cała resztę głównie tylko kojarzę, bo jest mi średnio potrzebna. Nie lubię full-blown kontenerów JEE, bo gdy coś się w nich pieprzy, mam związane ręce. Wolę Springa (i podobne rozwiązania) w którym mogę dowolnie czytać/modyfikować/pachować źródła, i prosty kontener Servletów.

      Te 'nowe' metody, wymienione powyżej, nic w moim życiu nie zmieniają. Nie zamierzam z nich korzystać, bo:
      a) nie życzę sobie zależności w domenie od HttpServletRequest (a security bywa częśćią domeny) - zdecydowanie głupi pomysł
      b) tylko by mi to utrudniało testowanie i security w backendzie (nie ma tam zależności do servletów)
      c) mam Spring Security, który jest jest genialny w działaniu, budowie i źródłach
      d) nie mam zaufania do specyfikacji JEE, bo zwykle są ograniczone ("będzież programował po naszemu, albo wcale")

      Mało piszę "własnych rozwiązań", bo za dużo dobrych bibliotek OS jest na świecie, ale do specyfikacji JEE nie mam zaufania. Od JEE6 w końcu da się sensownie programować zgodnie z czystymi specyfikacjami, tylko... po co? JEE z natury jest spóźnione, w stosunku do najlepszych rozwiązań dostępnych w Open Source'owych libach.

      Usuń
    3. @Jakub - zgadzam się w 100%! Ostatnio ewangeliści "Vanilla JavaEE" przyhamowali i JavaEE 7 nie jest tak reklamowany jak JavaEE 6. Nie spodziewam się już takich artykułów jak *ponad 3 lata* temu po wydaniu JavaEE 6. JavaEE 7 to tylko batch (patrz: Spring Batch), JSON (patrz: Jackson) i jakaś tam drobnica od dawna dostępna w "szarej strefie" (czyli poza błogosławieństwem Oracle i JCP).

      A co do login()/logout() - racja - Security to nie element jeden warstwy (w tym przypadku webowej) - to raczej "aspekt" przenikający całą aplikację od widoków WWW aż do DAO, zatem wprowadzenie tych metod NIC NIE ZMIENIA - nie wiem naprawdę kto zamierza z tego korzystać. A w zasadzie nie mam pojęcia kto jeszcze (poza testowymi projekcikami) pisze klasy dziedziczące po javax.servlet.http.HttpServlet.

      pozdrawiam
      Grzegorz Grzybek

      Usuń
    4. Przyjęcie istniejącego (w naszej rozmowie to jest Spring Framework i przyległości) to w/g mnie zaakceptowanie stagnacji. Spring się rozwija, ale to wciąż to samo poletko (chciałem napisać "bagienko", ale to ma negatywne konotacje i się wstrzymałem).

      Dzięki takim akcjom "naprawczym" w Java EE jest szansa, że Spring* będzie jednym z wielu możliwych rozwiązań, a nie jedynym. W/g mnie to jakby zaprzeczyć tworzeniu konkurencji dla Spring*, gdyby założyć, że Spring jest "de best" i inni mogą jedynie próbować podążać jego śladami. Dla mnie jest to sytuacja nie do zaakceptowania i nawet z czystej przekory będę promował rozwiązania "czyste" na Java EE. Dzięki temu wielu dowie się, że Java EE nie śpi, a wręcz jest liderem w promowaniu pewnych podejść architektonicznych.

      Gdybym mógł zerknąć na kod, aby dowiedzieć się, co miałeś na myśli pisząc "A co do login()/logout() - racja - Security to nie element jeden warstwy (w tym przypadku webowej) - to raczej "aspekt" przenikający całą aplikację od widoków WWW aż do DAO, zatem wprowadzenie tych metod NIC NIE ZMIENIA - nie wiem naprawdę kto zamierza z tego korzystać." byłoby cacy. Odnoszę wrażenie, że zasłaniamy się jedynie pojęciami - aspekt - które w tym momencie nic nie wnosi. Z Twoim przykładem możnaby było zastanowić się nad rozwiązaniem alternatywnym w Java EE i to mogłoby nas rozwinąć technicznie. A o to chodzi, nieprawdaż?

      Usuń
    5. @Jacek - pisząc "aspekt" miałem na myśli "pionowy prostokąt" znajdujący się "obok" ułożonych "poziomych prostokątów", jeden na drugim, gdzie najwyższy to WIDOK, a zaraz pod nim mamy warstwę "web" (kontrolery, serwlety, akcje, co tam bądź). "Aspekt" to krótsze słowo :)

      "Aspektowość" Spring Security to dla mnie:
      - istnienie tagów JSP/dialektów Thymeleaf dla widoku
      - konfiguracja URL Security dla warstwy kontrolerów (+ takie rzeczy jak remember-me, session-security, itp.)
      - Integracja z CAS, LDAP, ...
      - adnotacje takie jak @PreAuthorize, czy listy ACL dla niższych warstw

      Ja nie jestem przeciwny JavaEE, można powiedzieć "wręcz przeciwnie". Ja również mam "napady czystości", gdzie chciałbym zrobić coś "najczyściej" jak się da. JavaEE API obiecuje coś takiego i starałem się - czyste JPA, czyste EJB itp. Ale w praktyce (10 lat) nie udało się. Zawsze było coś nie tak - a to serwer miał jakieś problemy z bibliotekami, a to trzeba było coś dokonfigurować w ramach "implementation specific configuration", a to działało na Tomcacie, a nie działało na WebSphere.

      Co do rozwiązań alternatywnych w JavaEE, to znając (zawsze poświęcam sporo czasu po wydaniu konkretnej wersji JavaEE, aby pobrać wszystkie PDFy, TCK (te, które można), źródła i API wszystkich specyfikacji wchodzących w skład JavaEE) odpowiednie JSRy zawsze nad tym się zastanawiam wprowadzając do nowego projektu po raz kolejny dopracowaną od lat (i weryfikowaną przy każdej nowej wersji Spring Security!) konfigurację "context.security.xml" Spring-Security, czy nie dałoby się tego zrobić w JavaEE? Odpowiedź niestety zawsze brzmi - nie. Teraz mamy: integrację z CAS, obsługę certyfikatów X.509 do przekazywania części danych uwierzytelniających, widoki oparte o Thymeleaf.

      W innym projekcie były dwa serwery LDAP (AD + OpenLDAP) i uwierzytelnianie trzeba było zrobić w oparciu o oba LDAPy. Pewnie da się napisać jakieś rozszerzenie do WebSphere'a (oj pisałem pisałem, żeby np. EJB na WebSphere wywołać z poziomu WARa na Tomcacie), ale przekonałem się, że to zbyt wielki nakład pracy w stosunku do prostszego (ale też elastyczniejszego i potężniejszego!) rozwiązania jakim jest Spring-Security.

      Spring* jest jednym z możliwych rozwiązań - takie postacie jak Reza Rahman, Adam Bien, Bill Burke, czy Gawin King (zakopany gdzieś teraz na/w Ceylonie) polecają zawsze (jeśli już mają ochotę wyjść poza "Vanilla JavaEE") Seama. Nie mówię, że to gorsze rozwiązanie, to po prostu rozwiązanie o innej historii i filozofii, ale stanowi pewnie godną alternatywę dla Spring*.

      Sam przyznałeś nawet, że jesteś przykładem osoby, która o nowościach w JavaEE dowiaduje się z changeloga Springa :) Jasne, że JavaEE nie śpi, ale moim zdaniem musi się konkretniej określić - czy jest alternatywą dla Springa (a nie jest, bo Spring to nie "alternatywa" dla JavaEE, tylko uproszczenie, upragmatyzowanie i rozwinięcie jego koncepcji!) czy jest organem standaryzującym (w takim razie z wydania na wydanie spada stosunek liczby implementacji do liczby specyfikacji) - sama standaryzacja jest dobra, ale należy unikać patologii polegających na wybieraniu jakiejś dostępnej od lat technologii i zmianie nazwy pakietów z np. "com.ibm.*" na "javax.cośtam.*". Mam wrażenie, że w przypadku JavaEE taka sytuacja dotyczy przetwarzania JSON. Od lat używam Codehausowego Jacksona i nie przeszkadza mi, że klasy są w pakietach "org.codehaus", a nie "javax.json"...

      pozdrawiam
      Grzegorz Grzybek

      Usuń
  2. s/w przypadku JavaEE taka sytuacja dotyczy przetwarzania JSON/w przypadku JavaEE 7 taka sytuacja dotyczy przetwarzania JSON/

    nie mogę niestety edytować...

    OdpowiedzUsuń
  3. @Jacek - Sam przyznałeś nawet - przepraszam - z rozpędu wpisałem...

    OdpowiedzUsuń