sobota, 23 października 2010

Java Authentication and Authorization Service (JAAS) od podstaw, czyli jak stworzyć własny moduł logowania

Dzisiejszy wpis poświęcony będzie podstawom implementacji szkieletu JAAS odpowiedzialnego w Javie za autentykację do programu i autoryzację dostępu. Ideą JAAS jest swobodne łączenie modułów odpowiedzialnych za bezpieczeństwo - PAM (Pluggable Authentication Modules). Poniższy przykład zaczniemy od utworzenia klasy CustomPrincipal przechowującej nazwę naszego podmiotu (Subject) zgłaszającego prośbę o uwierzytelnienie - dla uproszczenia nazywajmy go dalej użytkownikiem. Musi ona implementować interfejsy: java.security.Principal i java.io.Serializable oraz zawierać metody toString() i equals(). Subject może przechowywać wiele obiektów Principal takich jak np: grupy, jednak na potrzeby niniejszego przykładu wykorzystamy tylko jeden.

package kuba.demo.jaas.module;

import java.io.Serializable;
import java.security.Principal;
import java.util.logging.Logger;

public class CustomPrincipal implements Principal, Serializable {
 
 private static Logger log =Logger.getLogger(CustomPrincipal.class.getName());
 private static final long serialVersionUID = 1L;
 
 private String name;

 
    public CustomPrincipal(String name) {
     
     if (name == null){
      log.severe("Brak principal name");
         throw new NullPointerException("Principal name is null");
     }
     this.name = name;
   }


    @Override
    public String getName() {
 return name;
     }

 
    public String toString() {
     return("CustomPrincipal-" + name);
    }


    public boolean equals(Object o) {
     if (o == null){
         return false;
     }
        if (this == o){
               return true;
        }
        
        if (!(o instanceof CustomPrincipal)){
                return false;
        }
        
        CustomPrincipal that = (CustomPrincipal)o;

     if (this.getName().equals(that.getName())){
         return true;
     }
    
     return false;
    }
     
    
    public int hashCode() {
     return name.hashCode();
    }
}


Kolejną wymaganą klasą jest CustomCallbackHandler - implementuje interfejs javax.security.auth.callback.CallbackHandler. Można powiedzieć, że jest ona pośrednikiem pomiędzy naszą aplikacją, a usługą logowania, przekazując jej nasze dane uwierzytelniające (credentials).

package kuba.demo.jaas.module;

import java.io.IOException;
import java.util.logging.Logger;

import javax.security.auth.callback.Callback;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.NameCallback;
import javax.security.auth.callback.PasswordCallback;
import javax.security.auth.callback.UnsupportedCallbackException;

public class CustomCallbackHandler implements CallbackHandler {
 
 private static Logger log =Logger.getLogger(CustomCallbackHandler.class.getName());
 
 private String user;
 private String password;

 public CustomCallbackHandler(String user, String password) {
  this.user = user;
  this.password = password;
 }

 @Override
 public void handle(Callback[] callbacks) throws IOException,UnsupportedCallbackException {
  
  NameCallback nameCallback  = (NameCallback)callbacks[0];
  PasswordCallback passwordCallback = (PasswordCallback)callbacks[1];
  nameCallback.setName(user);
  passwordCallback.setPassword(password.toCharArray());
  log.info("Inicjalizacja callback handler'a dla uzytkownika: "+user);
 }
}

Pozostała jeszcze klasa odpowiadająca za uwierzytelnienie użytkownika czyli właściwy moduł logowania. Moduł taki - CutomLoginModule.java - musi implementować interfejs javax.security.auth.spi.LoginModule. Cała logika odpowiedzialna za uwierzytelnienie odbywa się w metodzie login(). Możemy w niej sięgnąć do bazy danych lub serwera usług katalogowych. Jeśli autentykacja nie powiedzie się, zwrócony zostanie wyjątek javax.security.auth.login.LoginException. Jeśli logowanie będzie poprawne wywoływana jest metoda commit(). Przesłaniając ją możemy przypisujemy naszemu uwierzytelnionemu obiektowi (Subject) odpowiednie właściwości. W przypadku gdy któraś z powyższych metod zwróci błąd, wywoływana jest metoda abort().

package kuba.demo.jaas.module;

import java.security.Principal;
import java.util.Map;
import java.util.logging.Logger;

import javax.security.auth.Subject;
import javax.security.auth.callback.Callback;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.NameCallback;
import javax.security.auth.callback.PasswordCallback;
import javax.security.auth.login.LoginException;
import javax.security.auth.spi.LoginModule;

public class CustomLoginModule implements LoginModule{
 
 private static Logger log =Logger.getLogger(CustomLoginModule.class.getName());
 
 private Subject subject;
 private CallbackHandler callbackHandler;
 private Map sharedState;
 private Map options;
 private String userName;
 private String userPassword;
 
 
 @Override
 public void initialize(Subject subject, CallbackHandler callbackHandler,
         Map sharedState, Map options) {
  
  this.subject = subject;
  this.callbackHandler = callbackHandler;
  this.sharedState = sharedState;
  this.options = options;
 }

 
 @Override
 public boolean login() throws LoginException {
  
  NameCallback nameCallback = new NameCallback("Username");
  PasswordCallback passwordCallback = new PasswordCallback("Password",false);
  
  Callback[] callbacks = new Callback[]{ nameCallback, passwordCallback };
  
  try {
    callbackHandler.handle(callbacks);
    
  } catch (Exception e) { 
   e.printStackTrace();
  }
  
  userName = nameCallback.getName();
  userPassword = String.valueOf(passwordCallback.getPassword());
  
  if("jakub.pawlowski".equals(userName) && "my_password".equals(userPassword)){
   
   log.info("Poprawnie zalogowano uzytkownika [ "+userName+" ]");
   return true;
   
  }else{
   
   log.info("Logowanie nie powiodlo sie");
   return false;
  }
 }

 
 @Override
 public boolean commit() throws LoginException {
  
  Principal principal = new CustomPrincipal(userName);
  subject.getPrincipals().add(principal);
  userPassword = null;
  log.info("Dodano obiekt CustomPrincipal");
  
  return true;
 }

 
 @Override
 public boolean abort() throws LoginException {
  
  log.info("Wywolanie abort()");
  userName = null;
  userPassword = null;
  return true;
 }

 
 @Override
 public boolean logout() throws LoginException {
  
  log.info("Uzytkownik [ "+userName+" ] zostal wylogowany");
  userName = null;
  userPassword = null;
  return true;
 }

}

Ostatnim krokiem jest utworzenie pliku konfiguracyjnego wskazującego na mój moduł logowania. Ja nazwałem go custom_jaas.config

MyLoginModule
{
 kuba.demo.jaas.module.CustomLoginModule REQUIRED;
};

Czas na sprawdzenie jak to wszystko działa. Poniżej prosty klient, należy go uruchomić z opcją wskazującą na plik konfiguracyjny: -Djava.security.auth.login.config=="e:/custom_jaas.config"

package kuba.demo.jaas.client;

import java.security.Principal;
import java.util.Iterator;
import java.util.Set;
import java.util.logging.Logger;

import javax.security.auth.Subject;
import javax.security.auth.login.LoginContext;
import javax.security.auth.login.LoginException;

import kuba.demo.jaas.module.CustomCallbackHandler;
import kuba.demo.jaas.module.CustomPrincipal;

public class CustomLoginModuleClient {

 private static Logger log =Logger.getLogger(CustomLoginModuleClient.class.getName());
 
 public static void main(String[] args) {
  
  CustomCallbackHandler callbackHandler = new CustomCallbackHandler("jakub.pawlowski", "my_password");
  
  try {
   LoginContext loginContext = new LoginContext("MyLoginModule",callbackHandler);
   loginContext.login();
   
   Subject s = loginContext.getSubject();
   Set ps = s.getPrincipals();
   Iterator iter = ps.iterator();
    
    while(iter.hasNext()){
     
     Principal principal = (Principal)iter.next();
     
     if(principal instanceof CustomPrincipal){
      
      log.info("Principal [ "+principal.getName()+" ]");
     }
    }
    
    loginContext.logout();
   
  } catch (LoginException e) {
   e.printStackTrace();
  }

 }
}


Jak widać poniżej autoryzacja przebiegła poprawnie

INFO: Poprawnie zalogowano uzytkownika [ jakub.pawlowski ]
2010-10-23 19:28:09 kuba.demo.jaas.module.CustomLoginModule commit
INFO: Dodano obiekt CustomPrincipal
2010-10-23 19:28:09 kuba.demo.jaas.client.CustomLoginModuleClient main
INFO: Principal [ jakub.pawlowski ]
2010-10-23 19:28:09 kuba.demo.jaas.module.CustomLoginModule logout
INFO: Uzytkownik [ jakub.pawlowski ] zostal wylogowany

niedziela, 17 października 2010

JSF 2.0 - Pierwsze starcie

Wczoraj po kilku godzinach prób, z przerwami na jedzenie, wymianę opon na zimowe, i z wieloma rzucanymi pod nosem niecenzuralnymi wyrazami, udało mi się doprowadzić do działania na serwerze Tomcat 7 aplikację JSF w wersji 2.0. Problemem było nie samo JSF ale obsługa Expression Language. Po przeczytaniu połowy internetu - tej większej:) - okazało się że do poprawnej obsługi EL API w ww. środowisku wymagana jest biblioteka el-ri-1.0.jar ( http://download.java.net/maven/2/com/sun/el/el-ri/1.0/ ). Pozostałe 2, czyli jsf-impl. jar i jsf-api.jar w wersji 2.0.3 pobrałem oczywiście stąd: https://javaserverfaces.dev.java.net/.

Cała pozostała konfiguracja to plik web.xml


Nie wiem czy to normalne ale JBoss Tools w wersji 3.2.0 M2 nie podpowiada na stronie nazw i składowych managed bean'ów, które nie są zarejestrowane w pliku faces-config.xml - co w JSF 2.0 nie jest wymagane ze względu na możliwość użycia adnotacji. Czyli coś takiego:

@ManagedBean(name = "myFirstBean")
@SessionScoped
public class TestBean implements Serializable{ 

 private static final long serialVersionUID = 1L;
 
 public TestBean() {
 }
}

nie jest widoczne na stronie .xhtml