Este API está orientado a conceder permisos en función de quién ejecuta el código. Se basa en PAMs (Pluggable Authentication Modules) que proporcionan mecanismos de autentificación. El mecanismo por defecto es el de usuario-password: cuando un usuario ejecuta código que require privilegios se pide el nombre de usuario y el password, y si éstos son correctos se conceden los permisos. Así, JAAS abarca tanto la autentificación, o verificación de identidad del usuario, como la autorización, es decir la concesión de permisos de acceso a los recursos. Como veremos más adelante, este es el modelo implementado por BEA WebLogic.
Supongamos que desde el código de una aplicación (p.e. EjemploJAAS.java) se intenta autentificar a un usuario a través de su nombre y password. El primer paso es crear lo que se denomina un contexto de Login o LoginContext al que pasamos un nombre (p.e. "Ejemplo") seguido de un manejador llamado UserPasswordCallbackHandler que parametrizado por el nombre de usuario y su password:
// 1. LoginContext String usuario = args[0]; char[] password = args[1].toCharArray(); LoginContext loginContext = new LoginContext( "Ejemplo", new UserPasswordCallbackHandler(usuario, password));
Para que la autentificación funcione, debemos tener especificado que al contexto Ejemplo se le asocia un determinado módulo de login o LoginModule con una determinada configuración. Por ello, la clase abstracta Configuration define como deben usarse LoginContext y LoginModule. Más concretamente, determina que módulos de login deben ser invocados y cómo su éxito o fracaso afecta al proceso de login. Se establecen cuatro posibilidades:
Required | El módulo de login debe tener éxito para que el login completo tenga éxito. Si falla, se consulta a otros módulos de login. |
Requisite | El módulo de login debe tener éxito para que el login completo tenga éxito. Si falla, el proceso de login es corto-circuitado y no se llama a ningún otro módulo de login. |
Sufficient | Si este módulo tiene éxito y ningún módulo requerido o de requisito falla, el login completo tiene éxito. |
Optional | El éxito de este módulo no influye en el resto del proceso. Si ningún módulo de los tres tipos anteriores falla, el login completo tiene éxito independientemente de que un módulo opcional tenga éxito. |
La asociación del contexto Ejemplo a un determinado módulo de login, llamado PasswordLoginModule, con la configuración required pueden alojarse, p.e., en el fichero jaas.config:
Ejemplo { PasswordLoginModule required; };
El código del manejador se encuentra en UserPasswordCallbackHandler.java. Dicho código implementa la interfaz CallbackHander. Para ello hay que definir un constructor, que simplemente recibirá el nombre de usuario y el password que se le pasa por parámetro desde el código que inició la autentificación:
public UserPasswordCallbackHandler(String usuario, char[] password) { mUsuario = usuario; mPassword = password; }
A continuación hay que implementar el método handle() que recibe como entrada un array de objetos Callback. Sun proporciona diversas implementaciones como PasswordCallback y NameCallback con métodos como setName() y setPassword() que inicializan el nombre y password del usuario en los respectivos callbacks a los valores definidos por el constructor y que en su momento le pasó la aplicación que realiza la autentificación:
public void handle(Callback[] callbacks) throws UnsupportedCallbackException { // Iterar los callbacks for(int i=0;i<callbacks.length;i++) { Callback callback = callbacks[i]; // Manejar callback según su tipo. if (callback instanceof NameCallback) { NameCallback nameCallback = (NameCallback)callback; nameCallback.setName(mUsuario); } else if (callback instanceof PasswordCallback) { PasswordCallback passwordCallback = (PasswordCallback)callback; passwordCallback.setPassword(mPassword); } else { throw new UnsupportedCallbackException(callback, "Tipo de callback no soportado"); } } }
Volviendo a la aplicación EjemploJAAS.java, entra en juego la interfaz LoginModule que se encuentra implementada en el código PasswordLoginModule.java. En esta interfaz tenemos los siguientes métodos:
initialize() | Dado un objeto inicializa el LoginModule para un intento de login. |
login() | Comprueba las credenciales del sujeto anterior. Esto es dependiente de la implementación: Puede consistir en un acceso a BD, leer un fichero de passwords, etc. |
commit() | Se invoca solo si login() tiene éxito. Este método añade las identidades y credenciales necesarias para el sujeto. El módulo login debe limpiar su estado durante el commit. Una vez que el commit tenga éxito, JAAS añadira el sujeto al contexto actual. |
abort() | Si el login() falló, se invoca este método y se limpia el estado del login. |
logout() | Hace logout borrando las identidades y credenciales que sea necesario. |
Desde la aplicación llamaremos a login() de la siguiente forma:
// 2. login() logincontext.login();
Concretamente el método login() define el array de callbacks y llama al método handle() del manejador para darle valor al nombre de usuario y al password. A continuación se accede a los callbacks devueltos mediante los métodos getName() y getPassword(). Estos valores son los que en su momento se pasarón desde la aplicación que realiza la autentificación.
// Crear dos callbacks: uno para usuario y el otro para password. Callback[] callbacks = new Callback[2]; callbacks[0] = new NameCallback("Usuario"); callbacks[1] = new PasswordCallback("Password", false); try { // Llamar al callbackhandler para rellenar informacion mCallbackHandler.handle(callbacks); mUsuario = ((NameCallback)callbacks[0]).getName(); char[] tempPassword = ((PasswordCallback)callbacks[1]).getPassword(); mPassword = new char[tempPassword.length]; System.arraycopy(tempPassword, 0, mPassword, 0, tempPassword.length); // Borrar password en el callback ((PasswordCallback)callbacks[1]).clearPassword(); } catch (IOException ioe) { throw new LoginException(ioe.toString()); } catch (UnsupportedCallbackException uce) { throw new LoginException(uce.toString()); }
A continuación se borra el password llamando al método clearPassword() y se produce la validación del nombre de usuario y del password. Asumiendo que el usuario es sco y el password es scosco, el código de validación podría ser el siguiente:
// Validar usuario y password if ( "sco".equals(mUsuario) && mPassword.length == 6 && mPassword[0] == 's' && mPassword[1] == 'c' && mPassword[2] == 'o' && mPassword[3] == 's' && mPassword[4] == 'c' && mPassword[5] == 'o' ) { // Usuario y password son correctos mLoginExito = true; return true; } else { // Fallo de autentificación. Borrar estado y lanzar excepción mLoginExito = false; mUsuario = null; clearPassword(); throw new FailedLoginException("Password Incorrecto"); }
de tal manera que si se detecta un fallo de autentificación se borra el password y se lanza una excepción. Esto hace que JAAS automáticamente lance el abort() que a su vez puede desencadenar un logout().Si por el contrario todo va bien, JAAS ejecutará el método commit(). Este método va a crear un objeto de la clase Subject.
Un Subject es una entidad (persona o empresa) o sujeto que está utilizando el sistema. Una entidad puede poseer una o más identidades o instancias de java.security.Principal. Por ejemplo podemos tener dos logins cada uno de los cuales está asociado a una aplicación distinta. Para obtener un Set con todas las entidades llamaremos al método getPrincipals().
Una sujeto contiene una lista de credenciales o instancias de Credentials, objetos tales como passwords y certificados, que pueden ser públicos o privados y puede accederse a ellos con los métodos getPublicCredentials() y getPrivateCredentials() respectivamente.
Los sujetos representan quién está ejecutando el código y por lo tanto el acceso a determinados recursos depende de quién es la entidad activa. Para obtener dicha entidad llamaremos a getSubject(). JAAS se encarga, en coordinación con la interfaz LoginModule, de asignar entidades. Concretamente, en el método commit() del LoginModule de nuestro ejemplo, se crea un Principal llamando a la implementación de esta interfaz que se encuentra en el código ImplPrincipal.java. A continuación a dicho principal se le añade el nombre de usuario, y el principal se añade al sujeto.
public boolean commit() throws LoginException { if (mLoginExito == false) { return false; } // Login con éxito: crear Principal y añadirlo al Subject mPrincipal = new ImplPrincipal(mUsuario); if (!(mSujeto.getPrincipals().contains(mPrincipal))) { mSujeto.getPrincipals().add(mPrincipal); } // Si queremos que el Subject contenga credenciales // este es el momento para añadirlas. // Borrar usuario y password. mUsuario = null; clearPassword(); mCommitExito = true; return true; }
Una vez realizado el commit(), desde la aplicación leeremos el contexto y lo imprimiremos:
// 3. getSubject() e imprimir Subject subject = loginContext.getSubject(); System.out.println(subject);
Para ilustrar el funcionamiento de las clases e interfaces anteriores necesitamos compilar los *.java: la aplicación EjemploJAAS.java, el manejador UserPasswordCallbakHandler.java, el módulo de login PasswordLoginModule.java, y el principal ImplPrincipal.java, y debe estar en el mismo directorio el fichero de configuración jaas.config.
A continuación llamaremos a la aplicación haciendo que la máquina virtual de java tome como fichero de configuración el fichero jaas.config. Si ponemos el nombre de usuario y el password correcto
java -Djava.security.auth.login.config==jaas.config EjemploJAAS sco scosco
el resultado será mostrar el nombre de usuario del sujeto autentificado. Si por el contrario se suministra un nombre de usuario o un password incorrectos el login fallará al dispararse una excepción.
En ocasiones es conveniente determinar si quien está ejecutando un determinado código está autorizado para ello. Supongamos que desde el main() de la aplicación EjemploJAAS2.java definimos un contexto de login para el usuario sco con password scosco. Supongamos que, independientemente de que el login tenga éxito o fracase intentamos ejecutar un determinado fragmento de código. Para ello extraemos el sujeto del login y llamamos al método doAs()de la clase Subject:
sujeto.doAs(sujeto, new AccionEjemplo());
donde AccionEjemplo es una clase que implementa la interfaz java.security.PrivilegedAction. Esta interfaz contiene únicamente el método run(). Supongamos que al ejecutarlo llamamos al método getSecretText() definido en la aplicación:
class AccionEjemplo implements PrivilegedAction { public AccionEjemplo() {} public Object run() { System.out.println("Texto secreto: " + EjemploJAAS2.getSecretText()); return null; } }
Hasta el momento hemos permitido el acceso independientemente de que el login haya tenido o no éxito. Sin embargo getSecretText() está diseñado para producir una salida distinta según el caso. ¿Cómo nos enteramos de si el usuario realmente está autorizado o no, y en función de ello emitimos una salida u otra? Necesitamos definir una instancia de la clase java.security.AccessControlContext, que obtendremos mediante llamando al método getContext() de la clase java.security.AccessController y una ver obtenida pasársela como argumento al método getSubject() de la clase Subject:
AccessControlContext contexto = AccessController.getContext(); Subject sujeto = Subject.getSubject(contexto);
Si el sujeto obtenido es null entonces está claro que se trata de un acceso incorrecto, y en ese caso se emite el texto esto lo puede ver cualquiera. En caso contrario se obtienen todas sus identidades o principales (principals). Si una de esas entidades corresponde al usuario sco entonces se emite el texto solo para tus ojos.
if (sujeto == null) { System.out.println("Sujeto null"); return TEXTO_GENERICO; } // Obtener todos los principales: instancias de ImplPrincipal. // Devolver el texto secreto si el usuario "sco" Set principales = sujeto.getPrincipals(); Iterator iterador = principales.iterator(); while (iterador.hasNext()) { ImplPrincipal principal = (ImplPrincipal)iterador.next(); if (principal.getName().equals("sco")) { return TEXTO_PARTICULAR; } } return TEXTO_GENERICO;