Cuando se trata de seguridad en la aplicación existen dos posibles caminos:
- Controlar la seguridad por nuestros propios medios ("a mano")
- Utilizar las funcionalidades provistas por el conenedor de aplicaciones para tal fin.
La seguridad en una aplicación web se divide en la autentificación (proceso por el cual se verifica la identidad de un usuario) y la autorización (proceso que determina si un usuario tiene acceso a un recurso o tarea particular cuando el usuario ya se encuentra autenticado).
Asumiremos que en nuestra aplicación tendremos dos tipos de usuarios: los usuarios regulares (que podrán acceder a la parte /users/ del sitio) y los administradores (quienes además de poder acceder a la parte de usuarios también podrán acceder a la parte de /admin/). Los usuarios que no se hayan autentificado solo podrán acceder a la parte pública de la aplicación.
Lo que queremos hacer es que nuestra aplicación utilice el recurso JDBC para acceder a la información de los usuarios y grupos.
Los "security realms" son colecciones de usuarios y grupos relacionados. Un usuario puede pertenecer a uno o más grupos de seguridad y son éstos grupos los que definen qué acciones pueden llevar a cabo los usuarios. Para el contenedor de aplicaciones el "realm" es interpretado como un string para identificar y obtener un nombre de usuario y una contraseña.
Para comenzar crearemos una base de datos con la siguiente estructura:
Tenemos entonces 3 tablas y una vista, el script para crearlas es el siguiente:
CREATE TABLE Grupos ( Id_Grupo INT(10) PRIMARY KEY, Nombre_Grupo VARCHAR(30) NOT NULL, Descripcion_Grupo VARCHAR(255) ); CREATE TABLE Usuarios( Id_Usuario INT(10) PRIMARY KEY, LogIn VARCHAR(30) NOT NULL UNIQUE, Nombre_Completo VARCHAR(60) NOT NULL, password CHAR(64) NOT NULL ); CREATE TABLE Usuarios_X_Grupos ( Id_Usuario INT(10) NOT NULL FOREIGN KEY (Id_Usuario) REFERENCES Usuarios(Id_Usuario) ON UPDATE NO ACTION ON DELETE NO ACTION, Id_Grupo INT(10) NOT NULL FOREIGN KEY (Id_Grupo) REFERENCES Grupos(Id_Grupo) ON UPDATE NO ACTION ON DELETE NO ACTION, CONSTRAINT Usuarios_X_Grupos_PK PRIMARY KEY (Id_Usuarios,Id_Grupo) ); CREATE VIEW V_Roles_Usuarios AS SELECT u.LogIn, u.password, g.Nombre_Grupo FROM Usuarios_X_Grupos ug INNER JOIN Usuarios u ON u.Id_Usuario = ug.Id_Usuario INNER JOIN Grupos g ON g.ID_Grupo = ug.Id_Grupo; INSERT INTO Grupos (Id_Grupo,Nombre_Grupo,Descripcion_Grupo) VALUES (1,'USER','Usuarios Regulares'), (2,'ADMIN','Usuarios de administración'); INSERT INTO Usuarios(ID_Usuario,LogIn,Nombre_Completo,password) VALUES (1,'john','John','6e0b7076126a29d5dfcbd54835387b7b'), /*john123*/ (2,'admin','ADMIN','21232f297a57a5a743894a0e4a801fc3'); /*admin*/ INSERT INTO Usuarios_X_Grupos(Id_Usuario,ID_Grupo) VALUES (1,1),(2,1),(2,2);
Ya que tenemos la base de datos que va a almacenar nuestros datos de usuarios estamos listos para crear una conexión a nuestra base de datos a través de la consola de administración de glassfish.
Para ingresar en la consola de administración de GlassFish se debe acceder a http://localhost:4848
Una vez dentro se debe ir a Resources/Connection Pools y luego hacer clic en New. Ingresamos el nombre y hacemos clic en next.
En el segundo paso dejamos todos los valores por defecto y hacemos clic en Finish.
Luego seleccionamos el pool de y hacemos clic en Additional Properties y en add properties, como en la figura:
Completamos los datos para poder conectarnos a la base de datos en los campos. Cuando hayamos terminado podemos hacer clic en el botón Ping para ver si la conexión fue configurada correctamente.
Si la configuración está OK hacemos clic en JDBC Resources y luego en New.
El nombre JNDI va a ser el que se utilice en el "jdbc security realm" para obtener la conexión a la base de datos. Colocamos el nombre que queramos y seleccionamos el pool de conexiones que habíamos creado.
Una vez que hayamos configurado todo lo anterior podremos configurar el JDBC realm. Vamos a Configuration/Security/Realms y hacemos clic en New.
Colocamos el nombre para este jdbc realm (este nombre será usado en el web.xml) y seleccionamos JDBCRealm en la lista de selección. Colocamos las propiedades como se muestran en la imagen.
El valor de JNDI debe ser igual al del JDBC Resource que creamos.
Lo interesante en este caso es que para "User Table" y para "Group Table" usé la vista "V_Roles_Usuarios" como valor de la propiedad. En esta vista se tienen los datos del usuario y del grupo juntos. La razón por la cual no se usaron los nombres de las tablas correspondientes es porque glassfish asume que tanto en la tabla de usuarios como en la de grupos hay una columna que contiene el nombre de usuario y eso resultaría en datos duplicados.
Se pueden colocar las propiedades como se ve en la imagen y todas las demás pueden ser dejadas en blanco.
Una vez definido el JDBC Realm necesitamos configurar nuestra aplicación. Toda la lógica de la autentificación será llevada a cabo por el servidor de aplicación, de esta forma solo deberemos hacer algunas modificaciones en los descriptores de despliegue para asegurar la aplicación, o sea, en web.xml y en sun-web.xml.
<login-config> <auth-method>FORM</auth-method> <realm-name>jdbc-realm</realm-name> <form-login-config> <form-login-page>/faces/login.xhtml</form-login-page> <form-error-page>/faces/loginError.xhtml</form-error-page> </form-login-config> </login-config>
El elemento "login-config" define al método de autorización para la aplicación (en este caso el form será mostrado al usuario para que se autentique) y el realm que será utilizado para la autorización. Definimos también las páginas para el login y para el login incorrecto. Si el usuario intenta dirigirse a una url sin haberse autenticado antes, será redirigido a la pantalla de LogIn primero. Si la autenticación falla, se redireccionará a la página loginError.
<security-constraint> <web-resource-collection> <web-resource-name>Admin user</web-resource-name> <url-pattern>/faces/admin/*</url-pattern> <http-method>GET</http-method> <http-method>POST</http-method> </web-resource-collection> <auth-constraint> <role-name>ADMIN</role-name> </auth-constraint> </security-constraint> <security-constraint> <web-resource-collection> <web-resource-name>Admin user</web-resource-name> <url-pattern>/faces/users/*</url-pattern> <http-method>GET</http-method> <http-method>POST</http-method> </web-resource-collection> <auth-constraint> <role-name>ADMIN</role-name> <role-name>USER</role-name> </auth-constraint> </security-constraint>
El elemento "security-constraint" define quién tiene acceso a las páginas que concuerdan con cierto patrón de URL. LOs roles admitidos para el acceso son definidos en este elemento.
Queremos que los usuarios administradores tengan acceso a los recursos dentro de /admin/*. Los usuarios regulares y administradores tienen acceso a los recursos dentro de /users/*.
Antes de que podamos autenticar satisfactoriamente a nuestros usuarios, debemos enlazar los roles de usuario definidos en el archivo web.xml con los grupos definidos en el realm. Hacemos esto mediante el archivo sun-web.xml.
<security-role-mapping> <role-name>ADMIN</role-name> <group-name>ADMIN</group-name> </security-role-mapping> <security-role-mapping> <role-name>USER</role-name> <group-name>USER</group-name> </security-role-mapping>
El archivo sun-web.xml puede tener uno o más elementos. Cada uno de estos elementos por cada rol definido en el archivo web.xml
De esta manera, si un usuario ingresa una URL como la siguiente: "http://host/showcase/faces/users/users.xhtml" será redireccionado a la pantalla de login. El código para dicha pantalla se encuentra a continuación:
Es importante destacar que los atributos j_security_check, j_username and j_password son requeridos por la autentificación manejada por el contenedor y no se deben renombrar.<h:body> <p>Login to access secure pages:</p> <form method="post" action="j_security_check"> <h:panelGrid columns="2"> <h:outputLabel for="j_username" value="Username" /> <input type="text" name="j_username" /> <h:outputLabel for="j_password" value="Password" /> <input type="password" name="j_password" /> <h:outputText value="" /> <h:panelGrid columns="2"> <input type="submit" name="submit" value="Login" /> <h:button outcome="index" value="Cancel" /> </h:panelGrid> </h:panelGrid> </form> </h:body>
Luego de que se haya autenticado el usuario se redireccionará a la url requerida. La página users tiene un simple enlace para desloguearse.
<h:body> <p>Welcome to user pages</p> <h:form> <h:commandButton action="#{authBackingBean.logout}" value="Logout" /> </h:form> </h:body>
Y el bean que ejecuta el logout es:
@ManagedBean @RequestScoped public class AuthBackingBean { private static Logger log = Logger.getLogger(AuthBackingBean.class.getName()); public String logout() { String result="/index?faces-redirect=true"; FacesContext context = FacesContext.getCurrentInstance(); HttpServletRequest request = (HttpServletRequest)context.getExternalContext().getRequest(); try { request.logout(); } catch (ServletException e) { log.log(Level.SEVERE, "Failed to logout user!", e); result = "/loginError?faces-redirect=true"; } return result; } }
Como pueden ver en el Servlet 3.0 se ha agregado el método logout que hace que el contenedor se "olvide" de las credenciales de usuario.
Nota: Para el caso de las contraseñas se colocó un string hasheado mediante el algoritmo MD5, cuando se configure el Realm se debe especificar que algoritmo se utiliza para las contraseñas (MD5, SHA256,etc)
Eso fue todo por este día, espero que les sea de suma utilidad este tutorial para cuando tengan que desarrollar alguna aplicación con Java EE.
Saludos y hasta la próxima! :)