Pokazywanie postów oznaczonych etykietą autoryzacja. Pokaż wszystkie posty
Pokazywanie postów oznaczonych etykietą autoryzacja. Pokaż wszystkie posty

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>

czwartek, 18 lutego 2010

JAAS - podstawy

Co to jest JAAS?
JAAS czyli Java Authentication and Authorization Service, jest to narzędzie wbudowane do Javy 1.4 i nowszej, które pozwala na łatwą autoryzacje i uwierzytelnianie użytkownika.

Aplikacja
Zaczniemy od zrobienia prostej aplikacje, którą później zabezpieczymy przy pomocy JAAS'a tak, aby osoba przeglądająca naszą witrynę musiała potwierdzić że jest w gronie osób które mogą przeglądać treść danych podstron.
Moja prosta aplikacja wygląda tak:

Do stworzenia mojej aplikacji skorzystałem z popularnego edytora Eclipse. Projekt nazwałem JAASHelloWorld. Widzimy że cała aplikacja składa się z trzech podstron:
  • admin/index.jsp - strona do której będzie miał dostęp tylko administrator
  • user/index.jsp - strona do której będzie miał dostęp zalogowany użytkownik i administartor
  • index.jsp - strona główna do której mają dostęp wszyscy
Aplikacja będzie uruchomiana na popularnym serwerze aplikacji JBoss.

Panel logowania
Bardzo ważną rzeczą jest formularz logowania, dzięki któremu użytkownik będzie mógł potwierdzić swoją tożsamość. Dlatego zaczniemy od stworzenia nowego katalogu login a w nim strony login.jsp. Strona zawiera prosty formularz z dwoma polami i przyciskiem. Formularz musi zawierać trzy bardzo istotne elementy:
  • akcję formularza ustawiamy na j_security_check
  • pole login zawiera atrybut name z wartością j_username
  • pole hasło zawiera atrybut name z wartością j_password
U mnie plik ten wygląda:
<html>
 <head>
  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
 <title>Login</title>
 </head>
 
 <body>
 
  <form method="post" action="j_security_check">
   <div>
    Login:
    <input type="text" name="j_username" />
   </div>
   
   <div>
    Hasło:
    <input type="password" name="j_password"/>
   </div>
   
   <div>
    <input type="submit" value="LOGIN" />
   </div>
  </form>
 
 </body>
</html>

Potrzebujemy jeszcze strony na którą zostaniemy przekierowani w razie błędnego logowania. Do tego celu tworzymy stronę login/login_err.jsp która będzie zawierać komunika o nieudanym logowaniu. U mnie wygląda to tak:
<html>
 <head>
  <title>Login Error</title>
 </head>
 
 <body>
  <div>Logowanie się nie powiodło. Spróbuj jeszcze raz.</div>
 </body>
</html>

Po wykonaniu tego kroku aplikacja wygląda tak:


Baza danych
Następnym krokiem jakim się zajmiemy jest stworzeniem bazy danych. Skrypt który to zrobi wygląda tak:

CREATE TABLE users (
 login VARCHAR(20),
 password VARCHAR(100)
);

CREATE TABLE user_roles (
 login VARCHAR(20),
 rolename VARCHAR(20)
);

Pierwsza tabela users przechowuje nazwy użytkowników (login) i hasła (password) do ich kont.
Druga tabela user_roles przechowuje nazwę użytkownika (login) i nazwę roli (rolename) do której użytkownika należy. Zgodnie z polityką bezpieczeństwa wszystkie hasła przechowywane w bazie danych muszą być szyfrowane. Tak będzie i u nas, dlatego przy pomocy pierwszej strony jaką udało mi się znaleźć w
wyszukiwarce google.com, pod hasłem: "md5 generator" szyfrujemy hasła.
Tak wiec po zaszyfrowaniu haseł admin i user uzyskujemy wyniki:
  • admin = 21232f297a57a5a743894a0e4a801fc3
  • user = ee11cbb19052e40b07aac0ca060c23ee

INSERT INTO users VALUES('admin','21232f297a57a5a743894a0e4a801fc3');
INSERT INTO user_roles VALUES('admin','admin');
INSERT INTO users VALUES('user1','ee11cbb19052e40b07aac0ca060c23ee');
INSERT INTO user_roles VALUES('user1','user');

Konfiguracja JBoss i JAAS
Nadszedł czas na najważniejszą części naszej konfiguracji. Na początek stworzymy DataSource który będzie wskazywał JBoss'owi jakiej bazy należy użyć. JBoss wraz z samym serwerem udostępnia przykładowe pliki konfiguracyjne, które znajdują się w katalogu jboss_home\docs\examples. Zgodnie z zasadą "nie rób więcej niż musisz" wchodzi do tego
jboss_home\docs\examples\jca i kopiujemy plik z nazwą nazwa_bazy-ds, u mnie mysql-ds do katalogu jboss_home\server\default\deploy. Plik ten oczywiście musimy delikatnie zmodyfikować, dlatego otwieramy go i zmieniamy wartości elementów:
  • <jndi-name> - określa nazwę naszego DataSource'a
  • <connection-url> - lokalizacja bazy danych, my używamy MySQl ktory działa pod adresem localhost/nazwa_bazy_danych
  • <user-name> - nazwa użytkownika w bazie danych
  • <password> - hasło do bazy danych
U mnie plik ten po zmodyfikowaniu wygląda tak:
<?xml version="1.0" encoding="UTF-8"?>

<!-- $Id: mysql-ds.xml 41017 2006-02-07 14:26:14Z acoliver $ -->
<!-- Datasource config for MySQL using 3.0.9 available from:
http://www.mysql.com/downloads/api-jdbc-stable.html
-->

<datasources>
<local-tx-datasource>
<jndi-name>MySqlDS</jndi-name>
<connection-url>jdbc:mysql://localhost:3306/testdb</connection-url>
<driver-class>com.mysql.jdbc.Driver</driver-class>
<user-name>root</user-name>
<password>mysql</password>
<exception-sorter-class-name>org.jboss.resource.adapter.jdbc.vendor.MySQLExceptionSorter</exception-sorter-class-name>
<!-- should only be used on drivers after 3.22.1 with "ping" support
<valid-connection-checker-class-name>org.jboss.resource.adapter.jdbc.vendor.MySQLValidConnectionChecker</valid-connection-checker-class-name>
-->
<!-- sql to call when connection is created
<new-connection-sql>some arbitrary sql</new-connection-sql>
-->
<!-- sql to call on an existing pooled connection when it is obtained from pool - MySQLValidConnectionChecker is preferred for newer drivers
<check-valid-connection-sql>some arbitrary sql</check-valid-connection-sql>
-->

<!-- corresponding type-mapping in the standardjbosscmp-jdbc.xml (optional) -->
<metadata>
<type-mapping>mySQL</type-mapping>
</metadata>
</local-tx-datasource>
</datasources>

Używamy bazy danych MySQL, dlatego potrzebujemy jeszcze connector do bazy danych. Plik ten można ściągnąc z ofincjalne strony MySQL , a po rozpakowaniu skopiować do katalogu jboss_home\server\default\lib.

Wrcamy ponownie do katalogu jboss_home\server\default\deploy i tworzymy w nim plik jboss-service.xml. Plik ten będzie zawierał informacje, gdzie JAAS ma szukać informacji na temat loginu, hasła i grupy do jakiej użytkownik należy. U mnie plik ten 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">
     <jaas:authentication>
      <jaas:login-module
       code="org.jboss.security.auth.spi.DatabaseServerLoginModule"
       flag="required">
       <jaas:module-option name="dsJndiName">java:/MySqlDS</jaas:module-option>
       <jaas:module-option name="principalsQuery">
        select u.password from users u where u.login = ?
</jaas:module-option>
       <jaas:module-option name="rolesQuery">
        select r.rolename, 'Roles' from user_roles r, users u
        where u.login = ? and u.login = r.login
</jaas:module-option>
       <jaas:module-option name="hashAlgorithm">MD5</jaas:module-option>
       <jaas:module-option name="hashEncoding">hex</jaas:module-option>
       <jaas:module-option name="hashCharset">UTF-8</jaas:module-option>
      </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>

Krótko powiem co zawierają główne elementy powyższego pliku:
  • <jaas:application-policy> - atrybut name zawiera nazwę dla JAAS'a
  • <jaas:module-option name="dsJndiName"> - wartością tego elementu jest nazwa DataSource'a
  • <jaas:module-option name="principalsQuery"> - wartością tego elementu jest zapytanie select wskazujące na hasło użytkownika
  • <jaas:module-option name="rolesQuery"> - wartością tego elementu jest zapytanie select wskazujące na nazwę roli użytkownika
Wracamy teraz z powrotem do aplikacji. W katalogu WebContent\WEB-INF tworzymy plik jboss-web.xml. U mnie plik ten wygląda tak:
<?xml version="1.0" encoding="UTF-8"?>

<!DOCTYPE jboss-web PUBLIC
"-//JBoss//DTD Web Application 2.4//EN"
"http://www.jboss.org/j2ee/dtd/jboss-web_4_0.dtd">

<jboss-web>
<security-domain>java:/jaas/my-auth</security-domain>
</jboss-web>

Plik ten zawiera element <jboss-web> w którym znajduje się element <security-domain> zawierający nazwę dla JAAS'a którą ustalilismy w pliku jboss-service.xml.
Ostatnim plikiem jaki musimy zmodyfikować jak plik WebContent\WEB-INF\web.xml. W tym pliku ustawiamy:
  • jakie katalogi (strony) są zablokowane dla niezalogowanych użytkoników
  • jakie role obsługiwane są w naszej aplikacji (u mnie jest to user i admin)
  • gdzie znajduje się formularz do logowania

U mnie plik ten po modyfikacji wygląda tak:
<?xml version="1.0" encoding="UTF-8"?>
<web-app id="WebApp_ID" version="2.4" xmlns="http://java.sun.com/xml/ns/j2ee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">
 <display-name>JAASHelloWorld</display-name>
 <welcome-file-list>
  <welcome-file>index.jsp</welcome-file>
 </welcome-file-list>
 
 <security-constraint>
  <web-resource-collection>
   <web-resource-name>Restricted to user</web-resource-name>
   <url-pattern>/user/*</url-pattern>
  </web-resource-collection>
  <auth-constraint>
   <role-name>user</role-name>
   <role-name>admin</role-name>
  </auth-constraint>
 </security-constraint>

 <security-constraint>
  <web-resource-collection>
   <web-resource-name>Restricted to Admin</web-resource-name>
   <url-pattern>/admin/*</url-pattern>
  </web-resource-collection>
  <auth-constraint>
   <role-name>admin</role-name>
  </auth-constraint>
 </security-constraint>

 <login-config>
  <auth-method>FORM</auth-method>
  <realm-name>my-auth</realm-name>
  <form-login-config>
   <form-login-page>/login/login.jsp</form-login-page>
   <form-error-page>/login/login_err.jsp</form-error-page>
  </form-login-config>
 </login-config>

 <security-role>
  <description>Typowy uzytkownik systemu</description>
  <role-name>user</role-name>
 </security-role>

 <security-role>
  <description>Administrator systemu</description>
  <role-name>admin</role-name>
 </security-role>
 
</web-app>

To teraz krótki opis, co znajduje się w powyższym pliku.
Jak można zauważyć, w pliku wydzielone są trzy główne elementy: <security-constraint>, <login-config> i <security-role>. Element <security-constraint> zawiera informacje o blokowanych zasobach, oraz o tym jakie grupy użytkowników mają dostęp do informacje zawartych na tych stronach. Element <login-config> zawiera informacje o rodzaju uwierzytelniania (<auth-method>) oraz o formularzu który ma się wyświetlić w razie odniesienia się do zablokowanych zasobów, w celu sprawdzenia czy użytkownik ma wystarczające prawa do oglądania treści zawartych na stronie. Przy konfiguracji JAAS'a ustawiamy również pod element <realm-name>, który zawiera nazwę realm (wartość ustawiana była w pliku jboss-service.xml). Element <security-role> przechowuje nazwę grupy, nasza mini aplikacja posiada dwie grupy, user - czyli zwykli użytkonicy, oraz admin - czyli administrator strony.

Możemy teraz przetestować to co stworzyliśmy. Uruchamiamy serwer JBoss i depoloy'ujemy na nim aplikację. Strona będzie dostępna pod adresem http://localhost:8080/JAASHelloWorld/ lub http://127.0.0.1:8080/JAASHelloWorld/.

Wylogowanie
Kiedy logowanie już działa, potrzebujemy jeszcze mechanizmu wylogowania. Jest na to bardzo szybki sposób. W katalogu WebContent/login tworzymy plik logout.jsp i umieszczamy w nim treść:
<%
session.invalidate();
response.sendRedirect("../index.jsp"); 
%>

Teraz gdy mamy już logowanie i wylogowywanie gotowe, struktura naszej aplikacji powinna wyglądać tak:


Tomcat 6, proste uwierzytelnianie - część 2

W pierwsze części artykułu mówiliśmy o tym jak prosto zabezpieczyć naszą aplikację przed niewłaściwymi osobami. Ale jak już zauważyliśmy ma ona jedną bardzo ważną wadę. Hasła które zapisywane są w bazie danych nie są szyfrowane. Czyli każdy kto ma wgląd do naszej bazy danych, ma dostęp do haseł i loginów użytkowników, co jest niedopuszczalne.
Dlatego omówimy teraz jak w szybki sposób szyfrować hasła w bazie danych bardzo popularnym algorytmem md5.
Cały artykuł jest oparty o aplikacje którą stworzyliśmy w pierwszej części artykułu.

Baza danych
Baza danych jest dokładnie taka sama jak w pierwszej części artykułu, czyli:

CREATE TABLE users (
 login VARCHAR(20),
 password VARCHAR(100)
);

CREATE TABLE user_roles (
 login VARCHAR(20),
 rolename VARCHAR(20)
);
Jedyne co wymaga zmiany to zaszyfrowanie hasła. Robimy to poprzez wywołanie polecenia INSERT, z tą różnica że hasło będzie zaszyfrowane. Aby zaszyfrować hasło użyłem przykładowej strony, która wyskoczył jako jedna z pierwszych w wyszukiwarce google.com, pod hasłem: md5 generator.
Tak wiec po zaszyfrowaniu haseł admin i user uzyskujemy wyniki:
  • admin = 21232f297a57a5a743894a0e4a801fc3
  • user = ee11cbb19052e40b07aac0ca060c23ee

INSERT INTO users VALUES('admin','21232f297a57a5a743894a0e4a801fc3');
INSERT INTO user_roles VALUES('admin','admin');
INSERT INTO users VALUES('user1','ee11cbb19052e40b07aac0ca060c23ee');
INSERT INTO user_roles VALUES('user1','user');

Pliki konfiguracyjne
Musimy jeszcze zmienić nasz plikach konfiguracyjny WebContent/META-INF/context.xml, w którym ustawialiśmy parametry konfiguracyjne bazy danych. Do elementu <Realm> dodajemy atrybut digest="nazwa_algorytmu_szyfrującego", w naszym wypadku będzie to MD5.
Plik po modyfukacji powinien wyglądać tak:

<?xml version="1.0" encoding="ISO-8859-1"?>
<Context antiResourceLocking="false" privileged="true">
<Realm className="org.apache.catalina.realm.JDBCRealm" debug="0"
 driverName="org.gjt.mm.mysql.Driver" connectionURL="jdbc:mysql://localhost/testdb"
 connectionName="root" connectionPassword="mysql" userTable="users"
 userNameCol="login" userCredCol="password" userRoleTable="user_roles"
 roleNameCol="rolename" digest="MD5"/>
</Context>
Po tych zmianach wystarczy ponownie zdeployować aplikacje na serwerze. Strona działa dokładnie tak samo jak wersja bez użycia algorytmu szyfrującego md5, ale jest o wiele bardzije bezpieczna.

środa, 17 lutego 2010

Tomcat 6, proste uwierzytelnianie - część 1

W tym prostym tutorialu przedstawię jak w bardzo prosty sposób zabezpieczyć zasoby naszej aplikacji działającej na bardzo popularnym serwerze aplikacji jakim jest Apache Tomcat 6.

Baza Danych
Na początek musimy przygotować strukturę bazy danych, gdzie będziemy trzymać nazwy użytkowników i przydzielone do nich nazwy ról. Skrypt który to zrobi wygląda tak:
CREATE TABLE users (
 login VARCHAR(20),
 password VARCHAR(100)
);

CREATE TABLE user_roles (
 login VARCHAR(20),
 rolename VARCHAR(20)
);
Pierwsza tabela users przechowuje nazwy użytkowników (login) i hasła (password) do ich kont. Druga tabela user_roles przechowuje nazwę użytkownika (login) i nazwę roli (rolename) do której użytkownika należy. Teraz wystarczy uzupełnić nasze tabele danymi:
INSERT INTO users VALUES('admin','admin');
INSERT INTO user_roles VALUES('admin','admin');
INSERT INTO users VALUES('user1','user');
INSERT INTO user_roles VALUES('user1','user');
Ja będę używał bazy MySQL, dlatego musimy jeszcze ściągnąć connector, który należy rozpakować i skopiować do katalogu tomcat_home/lib.

Aplikacja
Teraz możemy zająć się tworzeniem aplikacji. Ja używam Eclipse. Tworzymy Dynamic Web project, eclipse zadba o to aby stworzyć odpowiedni układ katalogów. U mnie wygląda to tak:



Kiedy mamy już szkielet naszej aplikacji, dodamy kilka stron, aby mieć na czym testować nasze uwierzytelnianie. Dodajemy do WebContent trzy katalogi: user, admin i login. Katalog admin będzie zawierał podstrony do których ma dostęp tylko administrator, natomiast katalog user będą mogli przeglądać tylko użytkownicy zalogowani. Katalog login będzie zawierał podstrony które będą potrzebne do zalogowania i wylogowania użytkownika z naszej aplikacji. Potrzebujemy także strony startowej, dlatego bezpośrednio w katalogu WebContent tworzymy strone index.jsp. Na razie zostawimy ją pustą, późnie wypełnimy ją jakąś treścią.

W katalogu login tworzymy trzy pliki:
  • login.jsp - zawiera formularz logowania
  • login_err.jsp - zawiera komunikat o nieudanej próbie logowania
  • logout.jsp - zawiera krótki kawałek kodu który odpowiada za wylogowanie użytkownika
Strona login/login.jsp zawiera prosty formularz dzięki któremu użytkownik będzie mógł się zalogować. Należy zwrócić tutaj uwagą na akcje jaką wykona formularz po kliknięciu przycisku, oraz na wartości atrybutu name w polach gdzie wprowadzamy login i hasło.
<html>
 <head>
  <title>Login</title>
 </head>
 
 <body>
  <form method="post" action="j_security_check">
   <div>
    Login:
    <input type="text" name="j_username" />
   </div>
   <div>
    Hasło:
    <input type="password" name="j_password"/>
   </div>
   <div>
    <input type="submit" value="LOGIN" />
   </div>
  </form>
 </body>
</html>

Strona login/login_err.jsp zawiera prosty komunikat o błędnym zalogowaniu:
<html>
 <head>
  <title>Login Error</title>
 </head>
 
 <body>
  <div>Logowanie się nie powiodło. Sprobuj jeszcze raz.</div>
 </body>
</html>
Strona login/logout.jsp wylogowywuje użytkownika i przekierowuje nas na stronę index.jsp.
<%
session.invalidate();
response.sendRedirect("../index.jsp"); 
%>

W katalogu user tworzymy plik user/index.jsp, plik będzie zawierał krótki komunika "Witaj użytkowniku" i dwa linki, jeden do strony głównej (index.jsp), drugi do strony login/logout.jsp.


<html>
 <head>
  <title>Uzytkonik</title>
 </head>
 
 <body>
  Witaj uzytkowniku
  <br />
  <a href="../index.jsp">cofnij</a>
  <br />
  <a href="../login/logout.jsp">logout</a>
 </body>
</html>
W katalogu admin tworzymy plik admin/index.jsp, który podobnie jak plik user/index.jsp będzie zawierał komunikat powitalny, tym razem jednak administratora systemu, oraz dwa linki.

<html>
 <head>
  <title>Administrator</title>
 </head>
 
 <body>
  Witaj administratorze
  <br />
  <a href="../index.jsp">cofnij</a>
  <br />
  <a href="../login/logout.jsp">logout</a>
 </body>
</html>
Wracamy teraz do naszej głównej strony index.jsp, dodamy do niej krótki komunikat powitalny oraz linki do naszych stron użytkownika (user/index.jsp) oraz administratora (user/index.jsp). Dodamy też możliwość wylogowania się, dostępną tylko dla zalogowanych użytkowników.
<html>
 <head>
  <title>Uzytkownik</title>
 </head>
 
 <body>
  Witaj na stronie testowej.
  <br />
  <a href="admin/index.jsp">Administrator</a>
  <br />
  <a href="user/index.jsp">Uzytkownik</a>
  <br />
  <%
  if(request.getUserPrincipal() != null){
  %>
  <a href="login/logout.jsp">logout</a>
  <%
  }
  %>
 </body>
</html>
Po stworzeniu wszystkich stron naszej małej aplikacji układ katalogów powinien wygląda tak:


Pliki konfiguracyjne, czyli to co najważniejsze
Teraz kiedy nasza mini aplikacja jest już gotowa, możemy zablokować dostęp do niektórych katalogów, w celu zabezpieczenia danych które są tam przechowywane przed niewłaściwymi osobami.
W katalogu WEB-INF znajduje się plik web.xml, w którym umieścimy informacje o tym, jakie podstrony naszej aplikacje będą niedostępne dla niezalogowanych użytkowników.
Plik web.xml po zmodyfikowaniu powinie wyglądać tak:
<?xml version="1.0" encoding="UTF-8"?>
<web-app id="WebApp_ID" version="2.4" xmlns="http://java.sun.com/xml/ns/j2ee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">

 <display-name>HelloWorld</display-name>
 <welcome-file-list>
  <welcome-file>index.jsp</welcome-file>
 </welcome-file-list>

 <security-constraint>
  <web-resource-collection>
   <web-resource-name>AdminPages</web-resource-name>
   <description> accessible by authorised users </description>
   <url-pattern>/admin/*</url-pattern>
   <http-method>DELETE</http-method>
   <http-method>GET</http-method>
   <http-method>POST</http-method>
   <http-method>PUT</http-method>
  </web-resource-collection>
  <auth-constraint>
   <role-name>admin</role-name>
  </auth-constraint>
  <user-data-constraint>
   <transport-guarantee>NONE</transport-guarantee>
  </user-data-constraint>
 </security-constraint>

 <security-constraint>
  <web-resource-collection>
   <web-resource-name>UserPages</web-resource-name>
   <description> accessible by authorised users </description>
   <url-pattern>/user/*</url-pattern>
   <http-method>DELETE</http-method>
   <http-method>GET</http-method>
   <http-method>POST</http-method>
   <http-method>PUT</http-method>
  </web-resource-collection>
  <auth-constraint>
   <role-name>user</role-name>
   <role-name>admin</role-name>
  </auth-constraint>
  <user-data-constraint>
   <transport-guarantee>NONE</transport-guarantee>
  </user-data-constraint>
 </security-constraint>

 <login-config>
  <auth-method>FORM</auth-method>
  <form-login-config>
   <form-login-page>/login/login.jsp</form-login-page>
   <form-error-page>/login/login_err.jsp</form-error-page>
  </form-login-config>
 </login-config>

 <security-role>
  <role-name>admin</role-name>
 </security-role>
 <security-role>
  <role-name>user</role-name>
 </security-role>
</web-app>
To teraz krótki opis, co znajduje się w powyższym pliku.
Jak można zauważyć, w pliku wydzielone są trzy główne elementy: <security-constraint>, <login-config> i <security-role>. Element <security-constraint> zawiera informacje o blokowanych zasobach, oraz o tym jakie grupy użytkowników mają dostęp do informacje zawartych na tych stronach. Element <login-config> zawiera informacje o formularzu który ma się wyświetlić w razie odniesienia się do zablokowanych zasobów, w celu sprawdzenia czy użytkownik ma wystarczające prawa do oglądania treści zawartych na stronie. Element <security-role> przechowuje nazwę grupy, nasza mini aplikacja posiada dwie grupy, user - czyli zwykli użytkonicy, oraz admin - czyli administrator strony.

Nasza aplikacja do weryfikacji użytkownika wykorzystuje bazę danych, dlatego musimy stworzyć plik który będzie informował Tomcat'a o tym gdzie ma szukać informacji o loginach, hasłach i przynależności do grup. W tym celu w katalogu META-INF tworzymy plik context.xml.
<?xml version="1.0" encoding="ISO-8859-1"?>
<Context antiResourceLocking="false" privileged="true">
<Realm className="org.apache.catalina.realm.JDBCRealm" debug="0"
 driverName="org.gjt.mm.mysql.Driver" connectionURL="jdbc:mysql://localhost/testdb"
 connectionName="root" connectionPassword="mysql" userTable="users"
 userNameCol="login" userCredCol="password" userRoleTable="user_roles"
 roleNameCol="rolename" />
</Context>
Plik zawiera główny element <Realm> który posiada kilka ważnych atrybutów:
  • driverName - informacje o sterowniku z jakiego używamy
  • connectionURL - lokalizacja bazy danych, my używamy MySQl ktory działa pod adresem localhost/nazwa_bazy_danych
  • connectionName - nazwa użytkownika w bazie danych
  • connectionPassword - hasło do bazy danych
  • userTable - nazwa tabeli która przechowuje informacje o loginach i hasłach
  • userNameCol - nazwa kolumny która przechowuje login użytkownika
  • userCredCol - nazwa kolumny która przechowuje hasło użytkownika
  • userRoleTable - nazwa tabeli która przechowuje login i nazwą roli użytkownika
  • roleNameCol - nazwa kolumny która przechowuje nazwę roli
Teraz wystarczy stworzyć paczkę war z naszej aplikacji z zdeployować ją na serwerze Apache Tomcat, jeśli używamy eclipse wystarczy dodać serwer w zakładce Servers i uruchomić wraz z nasza aplikacją.

Nasza aplikacja co prawda działa, ale hasła które są przechowywane w bazie danych nie są szyfrowane. Dlatego w części 2 artykułu naprawimy ten błąd.