Kilka słów o samej strukturze
Prosty moduł będzie składał się z 3 klas:
- MyPrincipal
- MyGroup
- MyLoginModule
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.Group i java.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
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>