czwartek, 25 marca 2010

JAAS - prosty moduł

W jednym z poprzednim wpisie, pokazywałem jak wykorzystać JAAS'a w aplikacji webowej. Teraz chciałbym się skupić na tym, jak napisać własny moduł, dzięki któremu będziemy mogli wprowadzić własne zasady uwierzytelnianie i autoryzacji. Na końcu pokażę jak użyć tego modułu w aplikacji webowej, uruchomionej na serwerze aplikacji JBoss.


Kilka słów o samej strukturze
Prosty moduł będzie składał się z 3 klas:

  • MyPrincipal
  • MyGroup
  • MyLoginModule
Klasy należy umieścić gdzieś w naszej aplikacji, u mnie będzie to pakiet com.blogspot.mkorwel.jaas




MyPrincipal
MyPrincipal jest prostą klasą implementującą interfejs java.security.Principal. Klasa przechowuje identyfikator (login, publiczne id, email) który pozwala jednoznacznie określić użytkowania lub rolę.
package com.blogspot.mkorwel.jaas;


import java.io.Serializable;
import java.security.Principal;


/**
 * 
 * @author mkorwel
 *
 */
public class MyPrincipal implements Principal, Serializable {


 /**
  * 
  */
 private static final long serialVersionUID = 1933073722894960007L;
 private final String name;


 public MyPrincipal(String name) {
  this.name = name;
 }


 public String getName() {
  return name;
 }


 public boolean equals(Object o) {
  if (o == null)
   return false;


  if (this == o)
   return true;


  if (!(o instanceof MyPrincipal))
   return false;
  MyPrincipal that = (MyPrincipal) o;


  if (this.getName().equals(that.getName()))
   return true;
  return false;
 }


 public int hashCode() {
  return name.hashCode();
 }
}
MyGroup
MyGroup jest prostą klasą implementującą interfejsy java.security.acl.Groupjava.io.Serializable. Klasa przechowuje kolekcję ról.
package com.blogspot.mkorwel.jaas;


import java.io.Serializable;
import java.security.Principal;
import java.security.acl.Group;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Set;


/**
 * 
 * @author mkorwel
 *
 */
public class MyGroup implements Group, Serializable {


 /**
  * 
  */
 private static final long serialVersionUID = -3978735590793434379L;
 private final String name;
 private final Set users = new HashSet();


 public MyGroup(String name) {
  this.name = name;
 }


 public boolean addMember(Principal user) {
  return users.add(user);
 }


 public boolean removeMember(Principal user) {
  return users.remove(user);
 }


 public boolean isMember(Principal member) {
  return users.contains(member);
 }


 public Enumeration members() {
  return Collections.enumeration(users);
 }


 public String getName() {
  return name;
 }


 public boolean equals(Object o) {
  if (o == null)
   return false;


  if (this == o)
   return true;


  if (!(o instanceof MyGroup))
   return false;
  MyGroup that = (MyGroup) o;


  if (this.getName().equals(that.getName()))
   return true;
  return false;
 }


 public int hashCode() {
  return name.hashCode();
 }


}

MyLoginModule
Klasa MyLoginModule jest miejscem gdzie implementujemy całą logikę modułu. Klasa implementuje interfejs LoginModule, czyli 5 metod:
  • initialize
  • login
  • commit
  • abort
  • logout
Initialize 
Metoda initialize wywoływana jest przy każdej próbie logowania. Zajmuje się wczytaniem wszystkich niezbędnych parametrów (takich jak miejsce zapisu loginów, haseł, itd.)

Login
Metoda odpowiada za poprawną uwierzytelnianie. W tradycyjnym modelu uwierzytelniania metoda sprawdza login i hasło.

Commit
Metoda wykonuje się jeżeli uwierzytelnianie powiodło się. Zapisuje osobę i role do obiektu Subject.

Abort
Metoda wykonuje się jeżeli uwierzytelnianie się nie powiodło.

Logout
Metoda wywoływana podczas wylogowania użytkownika.

package com.blogspot.mkorwel.jaas;


import java.util.HashMap;
import java.util.Map;


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.callback.UnsupportedCallbackException;
import javax.security.auth.login.LoginException;
import javax.security.auth.spi.LoginModule;


/**
 * 
 * @author mkorwel
 * 
 */
public class MyLoginModule implements LoginModule {


 private Map usernameAndPassword = new HashMap() {


  private static final long serialVersionUID = -2695491152140640122L;


  {
   put("user1", "user");
   put("admin", "admin");
  }
 };
 private Map usernameAndRoles = new HashMap() {


  private static final long serialVersionUID = 1136203053244663646L;


  {
   put("user1", "user");
   put("admin", "admin");
  }
 };


 // initial state
 private Subject subject;
 private CallbackHandler callbackHandler;
 @SuppressWarnings("unused")
 private Map sharedState;
 @SuppressWarnings("unused")
 private Map options;


 // username and password
 private String username;
 private String password;


 private MyPrincipal userPrincipal;


 @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 {


  Callback[] callbacks = new Callback[2];
  callbacks[0] = new NameCallback("user name: ");
  callbacks[1] = new PasswordCallback("password: ", false);


  try {
   // pobranie loginu
   callbackHandler.handle(callbacks);
   username = ((NameCallback) callbacks[0]).getName();


   // pobranie hasla
   char[] tmpPassword = ((PasswordCallback) callbacks[1])
     .getPassword();
   if (tmpPassword == null) {
    tmpPassword = new char[0];
   }
   password = new String(tmpPassword);


   ((PasswordCallback) callbacks[1]).clearPassword();


  } catch (java.io.IOException ioe) {
   throw new LoginException(ioe.toString());
  } catch (UnsupportedCallbackException uce) {
   throw new LoginException("Error: " + uce.getCallback().toString()
     + " not available to garner authentication information "
     + "from the user");
  }


  // weryfikacje loginu i hasla
  return (usernameAndPassword.containsKey(username) && password
    .equals(usernameAndPassword.get(username)));
 }


 @Override
 public boolean commit() throws LoginException {


  userPrincipal = new MyPrincipal(username);


  if (!subject.getPrincipals().contains(userPrincipal)) {
   subject.getPrincipals().add(userPrincipal);
   MyGroup role = new MyGroup("Roles");
   role.addMember(new MyPrincipal(usernameAndRoles.get(username)));
   subject.getPrincipals().add(role);
  }


  return true;
 }


 @Override
 public boolean abort() throws LoginException {


  username = null;
  password = null;
  userPrincipal = null;


  return true;
 }


 @Override
 public boolean logout() throws LoginException {


  subject.getPrincipals().remove(userPrincipal);


  username = null;
  password = null;
  userPrincipal = null;


  return true;
 }
}
Nasz prosty moduł nie posiada żadnych parametrów przekazywanych z zewnątrz. Korzysta z bardzo prostego mechanizmu uwierzytelniania login/hasło które są zapisane w dwóch mapach na początku klasy.
Ważna uwaga!!!
Jeśli używamy serwera aplikacyjnego JBoss, tak jak ja w przykładzie, musimy pamiętać o tym  że wszystkie role muszę być wewnątrz obiektu "Roles".

Własny moduł pod JBoss'em
Tak jak wspominałem na początku, pokaże jak szybko wykorzystać nasz moduł w aplikacji uruchomionej na serwerze aplikacji JBoss.
Jeśli korzystamy ze standardowego modułu uwierzytelniania i autoryzacji jaki przedstawiłem w poprzednim wpisie, to musimy podmienić tylko plik jboss-service.xml. Zmiany dotyczą tylko wskazania klasy modułu i usunięciu wszystkich dodatkowych parametrów takich jak: wskazanie zapytania SELECT dzięki któremu pobieramy login/hasło/role, itd.
Aktualny jboss-service.xml powinien wyglądać tak:
<?xml version="1.0" encoding="UTF-8"?>
<server>
 <mbean code="org.jboss.security.auth.login.DynamicLoginConfig"
  name="jboss.seminarium:name=seminarium.login,service=DynamicLoginConfig">


  <attribute name="PolicyConfig" serialDataType="jbxb">
   <jaas:policy
    xsi:schemaLocation="urn:jboss:security-config:4.1 resource:security-config_4_1.xsd"
    xmlns:jaas="urn:jboss:security-config:4.1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
    <jaas:application-policy name="my-auth-module">
     <jaas:authentication>
      <jaas:login-module code="com.blogspot.mkorwel.jaas.MyLoginModule"
       flag="required">
      </jaas:login-module>
     </jaas:authentication>
    </jaas:application-policy>
   </jaas:policy>
  </attribute>
  <depends optional-attribute-name="LoginConfigService">
   jboss.security:service=XMLLoginConfig
        </depends>
  <depends optional-attribute-name="SecurityManagerService">
   jboss.security:service=JaasSecurityManager
        </depends>
 </mbean>
</server>

niedziela, 7 marca 2010

Użycie recaptcha w javie

Recaptcha
Zaczynamy od stworzenia sobie konta na recaptcha i dodajemy domenę na której nasz kod będzie używany. Jeśli nie zamierzamy udostępniać naszej aplikacji w internecie, to możemy wymyślić sobie tymczasową domeną, np. global-key.pl. Musimy tylko pamiętać aby zaznaczyć checkbox (Enable this key on all domains (global key)) dzięki któremu będziemy mogli korzystać z naczego klucza także na serwerach produkcyjnych czy testowych (takich jak localhost czy 127.0.0.1). Następnie klikamy Create Key.
Wszystko powinno wyglądać mniej więcej tak:


Do implementacji recaptcha będziemy potrzebowali biblioteki recaptcha4j-0.0.7.jar. Plik jar należy umieścić w katalogi WebContent\WEB-INF\lib lub w innym katalogu gdzie przechowujemy biblioteki dla naszej aplikacji. 

Przygotowanie formularza
Kiedy mamy już wygenerowany prywatny i publiczny klucz, czas wstawić do formularza pola które będą przechowywać wartości recaptcha. Do tego celu użyjemy tego skryptu napisanego w JavaScript:
<script type="text/javascript"
   src="http://api.recaptcha.net/challenge?k=<your_public_key>">
</script>


<noscript>
   <iframe src="http://api.recaptcha.net/noscript?k=<your_public_key>"
       height="300" width="500" frameborder="0"></iframe><br>
   <textarea name="recaptcha_challenge_field" rows="3" cols="40">
   </textarea>
   <input type="hidden" name="recaptcha_response_field" 
       value="manual_challenge">
</noscript>

W powyższym skrypcie wstawiamy nasz Public Key w miejsce <your_public_key> i tak przygotowany fragment kodu wklejamy do naszego formularza. U mnie wygląda to tak:

<?xml version="1.0" encoding="ISO-8859-1" ?>
<%@ page language="java" contentType="text/html; charset=ISO-8859-1"
    pageEncoding="ISO-8859-1"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1" />
<title>Login</title>
</head>
<body>
 <form action="success" method="post">


  <div>
   Value1: <input type="text" name="value1"/>
  </div>
  
  <div>
   Value2: <input type="text" name="value2"/>
  </div>
  
  <div>
   <script type="text/javascript"
      src="http://api.recaptcha.net/challenge?k=<publicKey>">
   </script>
   <noscript>
       <iframe src="http://api.recaptcha.net/noscript?k=<publicKey>"
           height="300" width="500" frameborder="0"></iframe><br />
       <textarea name="recaptcha_challenge_field" rows="3" cols="40">
       </textarea>
       <input type="hidden" name="recaptcha_response_field" 
           value="manual_challenge" />
   </noscript>
  </div>
  
  <div>
   <input type="submit" value="Wyslij" />
  </div>
 
 </form>
</body>
</html>





Możemy teraz uruchomić naszą aplikację i sprawdzić czy recaptcha działa, na tym etapie powinna się tylko wyświetlić na stronie.
Nadszedł czas na stworzenie logiki biznesowej która odbierze formularz z recaptchą. Ja do tego celu użyję serwletu. 


package com.blogspot.mkorwel.servlet;


import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;


import net.tanesha.recaptcha.ReCaptchaImpl;
import net.tanesha.recaptcha.ReCaptchaResponse;
/**
 * 
 * @author mkorwel
 *
 */
public class Success extends HttpServlet {
  private static final long serialVersionUID = 1L;


  protected void doGet(HttpServletRequest request,
    HttpServletResponse response) throws ServletException, IOException {


  }


  protected void doPost(HttpServletRequest request,
    HttpServletResponse response) throws ServletException, IOException {


   //Pobranie i wypisanie naszych danych z formularza
   System.out.println(request.getParameter("value1"));
   System.out.println(request.getParameter("value2"));
   
   //Pobranie danych dla recaptcha
   String challengeReCaptcha = request.getParameter("recaptcha_challenge_field");
   String responseReCaptcha = request.getParameter("recaptcha_response_field");
   String remoteAddr = request.getRemoteAddr(); //Address użytkonika
      
   //Wypisanie danych dla recaptcha
   System.out.println(challengeReCaptcha);
   System.out.println(responseReCaptcha);
   System.out.println(remoteAddr);
   
   ReCaptchaImpl reCaptcha = new ReCaptchaImpl();
   //ustawiamy nasz klucz prywatny
      reCaptcha.setPrivateKey("<privateKey>");
      
      //sprawdzamy czy użytkownik wpisał pobrany wyraz w recaptcha
      ReCaptchaResponse reCaptchaResponse =
          reCaptcha.checkAnswer(remoteAddr, challengeReCaptcha, responseReCaptcha);
      
      if (!reCaptchaResponse.isValid()) {
       response.sendRedirect("/index.jsp");
      } else {
       getServletConfig().getServletContext().getRequestDispatcher(
    "/result.jsp").forward(request, response);
      }
   
  }
}

Krótkie wyjaśnienie co dzieje się w servlecie.
W metodzie doPost na początku pobieramy nasze dane z formularza aby wypisać je na ekran. Następnie pobieramy dane dla recaptcha, tj. dwa pola które umieściliśmy na stronie: recaptcha_challenge_field i recaptcha_response_field oraz adres użytkownika który wpisywał recaptcha. Po wczytaniu potrzebnych danych tworzymy obiekt ReCaptchaImpl i ustawiamy nasz klucz prywatny. W tym momencie mamy już wszystkie dany które potrzebne nam są do zweryfikowania poprawnoście recaptcha, dlatego tworzymy obiekt ReCaptchaResponse na którym wywołujemy metode checkAnswer z klasy ReCaptchaImpl którą kilka linijek temu stworzyliśmy. Do sprawdzenia czy użytkownik wpisał pobrany wyraz dla recaptcha używamy metody isValid. W Serwlecie w razie błędnego wpisania kodu, użytkownik zostaje przekierowany do strony z formularzem, a jeśli kod jest poprawny, idzie do strony która wyświetla ekran powitalny.