En el 2011 escribí un post muy parecido a éste, sobre cómo manejar autenticación y autorización a través filtros; ahora vamos a mejorar la situación y vamos a manejar la autenticación a través de jdbcRealms con el servidor web glassfish, aunque el esquema puede ser utilizado en otros servidores web, y la autorización con security-constraints en el web.xml
El objetivo? pues lo mismo de siempre; proteger los recursos web a través de restricciones de seguridad; viendolo a gran escala:
- Se tienen las tablas de rol, usuario y roles por usuario en una base de datos relacional (a los roles se los conoce como grupos en éste ámbito)
- Se define en el archivo web.xml los recursos protegidos (security-constraint) y se indica que roles tienen acceso a estos recursos (authorization-constraint), los roles que se definen aquí no necesariamente deben coincidir con el nombre del grupo en la base de datos.
- En el archivo sun-web.xml se hace el mapeo entre rol definido en el archivo web.xml y el grupo (rol) en la base de datos
- Se crea en el servidor web un Security Realm donde configuramos la base de datos, y las tablas que contienen los datos de usuario y rol
- Creamos un formulario web, e invocamos al metodo login de la clase HttpServletRequest que por debajo intenta autenticarse contra las tablas de usuario y rol configuradas en el realm.
El modelo entidad relación que espera la configuración es el siguiente:
Pero lo mas común es tener un esquema así:
Para hacerlo transparente simplemente hacemos una vista de las tablas de la siguiente manera:
SELECT dbo.[user].username, dbo.[user].password, dbo.role.role FROM dbo.[user] INNER JOIN dbo.user_role ON dbo.user_role.username = dbo.[user].username INNER JOIN dbo.role ON dbo.user_role.role_id = dbo.role.id
Al hacer un select sobre la vista debe devolver los usuarios con sus roles asignados:
Ahora vamos a configurar el glassfish; lo primero es considerar tener el driver jdbc en el classpath del servidor, lo recomendable es colocarlos en la carpeta: [ruta_instalación_glassfish]/glassfish/domains/tu_domain/lib/ext
Ahora accedemos a la consola de administración, que por defecto se accede por http://localhost:4848 , comenzamos creando un pool de conexiones, dentro de Resources-JDBC-JDBC Connection Pools creamos un pool, que consta de 2 pasos:
En el paso 1 le damos un nombre al pool y definimos a que tipo de base de datos se va a conectar
En el paso 2 colocamos los datos para la conexión:
Una vez creado el pool, comprobamos que todo esta correcto validando con un ping desde la consola de glassfish:
Una vez definido el pool creamos un recurso jdbc dentro de Resources-JDBC-JDBC Resources, simplemente le asignamos un nombre y como pool seleccionamos al pool de conexiones creado anteriormente:
Finalmente nos falta crear el realm de seguridad dentro de Configurations-server_config-Security-Realms, lo primero es darle un nombre, el cual vamos a utilizar luego en la configuración del web-xml de la aplicación web, la configuración va de la siguiente manera:
Lo que se debe tomar en cuenta: En classname debemos escoger com.sun.enterprise.security.auth.realm.jdbc.JDBCRealm, en el JAASContext debemos colocar jdbcRealm, en el JNDI colocamos el nombre del JDBCResource creado anteriormente, en los campos User Table y Group Table colocamos el nombre de la vista que hicimos de las tablas de autenticación, así mismo con los nombres de los campos de user, password y group (rol); y los datos de user y password para la conexión a la base de datos.
Ahora creamos un proyecto web en eclipse configurado para hacer deployment en glassfish, la estructura del sitio consta de la sección para administradores (admin) y la sección para editores (editor), en el web.xml debemos configurar todo lo referente a autenticación y autorización:
<login-config> <auth-method>FORM</auth-method> <realm-name>demoSecurityRealm</realm-name> <form-login-config> <form-login-page>/login.jspx?faces-redirect=true</form-login-page> <form-error-page>/loginerror.jspx</form-error-page> </form-login-config> </login-config> <security-role> <role-name>webAdmin</role-name> </security-role> <security-role> <role-name>webEditor</role-name> </security-role> <security-constraint> <web-resource-collection> <web-resource-name>admin-area</web-resource-name> <url-pattern>/admin/*</url-pattern> </web-resource-collection> <auth-constraint> <description>admins should be allowed to access this resources</description> <role-name>webAdmin</role-name> </auth-constraint> </security-constraint> <security-constraint> <web-resource-collection> <web-resource-name>editor-area</web-resource-name> <url-pattern>/editor/*</url-pattern> </web-resource-collection> <auth-constraint> <description>editors should be allowed to access this resources</description> <role-name>webEditor</role-name> </auth-constraint> </security-constraint>
El detalle de las configuraciones:
- El modo de autenticación configurado es FORM, el realm-name es el realm que creamos dentro de glassfish
- Creamos 2 tags de security-role, estos roles pueden ser o no los mismos nombres que los que se encuentran en la base de datos, luego vemos donde realizamos el mapeo, por ahora están definidos 2 roles: webAdmin y webEditor
- Creamos 2 tags de security-constraint para las areas de administradores y editores, en el auth-constraint colocamos los nombres de los roles definidos en los tags de security-role, respectivamente para cada sección
Finalmente en el archivo sun-web.xml debemos mapear los nombres de los roles definidos en el archivos web.xml con los roles de la base de datos:
<sun-web-app error-url=""> <context-root>/J2EESecurityRealm</context-root> <security-role-mapping> <role-name>webAdmin</role-name> <group-name>Administrator</group-name> </security-role-mapping> <security-role-mapping> <role-name>webEditor</role-name> <group-name>Editor</group-name> </security-role-mapping> <class-loader delegate="true" /> <jsp-config> <property name="keepgenerated" value="true"> <description>Keep a copy of the generated servlet class java code.</description> </property> </jsp-config> </sun-web-app>
El sun-web.xml es muy sensible incluso en el orden de los tags, así que tomenlo en cuenta. Finalmente queda hacer la página para autenticarnos y probar que funciona todo lo realizado previamente.
<h:body style="height: 100%;"> <f:view> <h:form id="form"> <h:panelGrid columns="2"> <h:outputLabel id="user">User:</h:outputLabel> <h:inputText value="#{loginBean.username}"></h:inputText> <h:outputLabel id="password">Password:</h:outputLabel> <h:inputSecret value="#{loginBean.password}"></h:inputSecret> <h:commandButton action="#{loginBean.login}" value="Login"></h:commandButton> </h:panelGrid> <h:messages id="errorMessages"></h:messages> <h:panelGrid rendered="#{request.getUserPrincipal() != null}" columns="1"> <a href="admin/adminIndex.jspx">Admin Zone</a> <a href="editor/editorIndex.jspx">Editor Zone</a> </h:panelGrid> </h:form> </f:view> </h:body>
Detras tenemos un managed bean que se va a encargar de autenticarnos:
</pre> @ManagedBean(name="loginBean") @RequestScoped public class LoginBean { private String username; private String password; public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public void login(){ HttpServletRequest request = (HttpServletRequest) FacesContext.getCurrentInstance().getExternalContext().getRequest(); try { request.login(username, password); } catch (Exception e) { FacesContext.getCurrentInstance().addMessage("", new FacesMessage(FacesMessage.SEVERITY_ERROR, "Invalid credentials, try again", "")); } } public void logout() throws ServletException, IOException{ HttpServletRequest request = (HttpServletRequest) FacesContext.getCurrentInstance().getExternalContext().getRequest(); request.getSession().invalidate(); request.logout(); FacesContext.getCurrentInstance().getExternalContext().redirect("login.jspx"); } public String getLoggedUser(){ HttpServletRequest request = (HttpServletRequest) FacesContext.getCurrentInstance().getExternalContext().getRequest(); if(request.getUserPrincipal() != null) return request.getUserPrincipal().getName(); return ""; } }
El método principal del bean es el login donde como vemos de manera transparente ejecutamos request.login y esto automagicamente nos trata de autenticar utilizando el security realm definido en el glassfish, si tratamos de acceder a las secciones de admin o editor nos hace una especie de redirect a la pagina de login, pero como vemos mantiene la url:
Si nos autenticamos con credenciales correctas la página de login nos presenta el menú para acceder a las distintas secciones, ya que estamos utilizando la propiedad rendered del panel para indicarle cuando mostrarse:
Si el usuario con el que nos autenticamos tiene un rol que tiene acceso a las secciones definidas con los security-constraints internamente el servidor le dara acceso sin problemas, automáticamente hace la validacion entre los roles asignados en la base de datos con la autorización que tienen los mismos en el web.xml.
En este caso el usuario gishac tiene ambos roles, si se autentican con un usuario que tiene un rol que no tiene acceso a la sección el servidor le niega el acceso al recurso, para el ejemplo me autentico con el usuario editor que solo tiene el rol Editor.
En asp.net hay una característica que la maneja de modo automática la plataforma, cuando un usuario que no esta autenticado trata de acceder a una zona protegida y es enviado a la página de login y se autentica, automáticamente es redirigido a la página que le fue negado el acceso, en jsf hay que usar un pequeno artificio, el cual consiste en crear en el bean una propiedad que contiene el valor de la página a redirigir, y en el metodo login validamos si tiene algun valor para redirigirlo; finalmente el código del bean queda así:
@ManagedBean(name="loginBean") @RequestScoped public class LoginBean { private String username; private String password; @ManagedProperty(value="#{param.from}") private String from; public String getFrom() { return from; } public void setFrom(String from) { this.from = from; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public void login(){ HttpServletRequest request = (HttpServletRequest) FacesContext.getCurrentInstance().getExternalContext().getRequest(); try { request.login(username, password); if(from != null){ FacesContext.getCurrentInstance().getExternalContext().redirect(from); } } catch (Exception e) { FacesContext.getCurrentInstance().addMessage("", new FacesMessage(FacesMessage.SEVERITY_ERROR, "Invalid credentials, try again", "")); } } public void logout() throws ServletException, IOException{ HttpServletRequest request = (HttpServletRequest) FacesContext.getCurrentInstance().getExternalContext().getRequest(); request.getSession().invalidate(); request.logout(); FacesContext.getCurrentInstance().getExternalContext().redirect("login.jspx"); } public String getLoggedUser(){ HttpServletRequest request = (HttpServletRequest) FacesContext.getCurrentInstance().getExternalContext().getRequest(); if(request.getUserPrincipal() != null) return request.getUserPrincipal().getName(); return ""; } }
Y en el xhtml de la página de login colocamos un input hidden:
<input type="hidden" name="from" value="#{requestScope['javax.servlet.forward.request_uri']}" />
El ejemplo puede ser portado a otro servidor web (servlet container, etc etc), por ejemplo tomcat, sólo tienen que crear el jdbcRealm en el mismo.
Saludos,
gish@c