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.