Voy a explicar las dos formas más básicas de poner seguridad en una aplicación web hecha con Spring.

Antes de nada, pongo una imagen de la estructura del proyecto, para que sea más fácil seguirlo. El proyecto se llama users y se puede encontrar completo en GitHub.

Estructura del Proyecto

La idea es que tengamos una serie de webs que no sean accesibles a cualquier usuario, sino que te pida un usuario y contraseña para poder acceder. Y dentro de esta estrategia hay dos caminos:

  • que tengamos guardado el usuario y contraseña en un fichero xml
  • que tengamos guardado el usuario y contraseña en una base de datos

El segundo caso es el normal, claro, pero ambos comparten mucho del trabajo que hay que hacer. Y el primero es el caso fácil, así que empezaremos con él.

Primer caso

Importante: es Spring quien se va a encargar de la seguridad. Nosotros no vamos a tener que programar cuando se pasa por la pantalla de login, ni tendremos que hacer funciones que comprueben los usuarios ni nada así. Se encarga Spring. PERO, para ello, hay que configurarlo todo de una determinada forma. Eso es lo que vamos a explicar. Una vez que esté todo bien configurado, Spring se encargará «mágicamente» de la comprobación de usuarios y demás.

Primero, para incorporar el tema de la seguridad en Spring, hay que añadir las siguientes dependencias en el POM:

<dependency>
  <groupId>org.springframework.security</groupId>
  <artifactId>spring-security-config</artifactId>
  <version>3.1.4.RELEASE</version>
  <exclusions>
  	<exclusion>
  		<groupId>org.springframework</groupId>
  		<artifactId>spring-asm</artifactId>
  	</exclusion>
  </exclusions>
</dependency>
<dependency>
  <groupId>org.springframework.security</groupId>
  	<artifactId>spring-security-web</artifactId>
  	<version>3.1.4.RELEASE</version>
  </dependency>
</dependencies>

Yo he puesto la versión 3.1.4, porque he ido siguiendo un libro que utilizaba esta. Pero podemos escoger la que queramos. Además, en el apartado Resolved Dependencies, he utilizado la opción «Exclude Maven Artifact» para la dependencia spring-asm: 3.0.7.RELEASE[compile]

A continuación hay que modificar el web.xml, añadiendo lo siguiente:

<context-param>
	<param-name>contextConfigLocation</param-name>
	<param-value>/WEB-INF/security-context.xml</param-value>
</context-param>
<listener>
	<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<filter>
	<filter-name>springSecurityFilterChain</filter-name>
	<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
	<filter-name>springSecurityFilterChain</filter-name>
	<url-pattern>/*</url-pattern>
</filter-mapping>

Con esto le decimos a Spring dos cosas:

  • que tenemos un fichero en /WEB-INF/ que se llama security-context.xml, y que es donde estamos definiendo los parámetros
    de seguridad
  • que todas las pantallas (/*) tienen que ser examinadas para la autenticación del usuario

Como hemos dicho que tendremos un fichero security-context.xml que va a definir los parámetros de seguridad, pues tenemos que
crear dicho fichero.

Este fichero es como el DefaultServlet-servlet.xml que tengamos, pero lo importante que tiene que tener este fichero son estas dos cosas:

<security:authentication-manager>
	<security:authentication-provider>
		<security:user-service>
			<security:user name="Admin" password="Admin123" authorities="ROLE_ADMIN" />
		</security:user-service>
	</security:authentication-provider>
</security:authentication-manager>

Aquí le estamos diciendo a Spring que hay un usuario de nombre Admin, con contraseña Admin123, que tiene el rol de ROLE_ADMIN.

<security:http auto-config="true">
	<security:intercept-url pattern="/oculta" access="ROLE_ADMIN"/>
	<security:form-login login-page="/login" default-target-url="/oculta" authentication-failure-url="/loginfailed"/>
	<security:logout logout-success-url="/logout" />
</security:http>

Y aquí le decimos:

  • que para acceder a la web /oculta, se necesita el ROLE_ADMIN
  • que hay una pantalla de login que es /login (que será la que muestre para solicitar un usuario)
  • Que al pasar con éxito la pantalla de login, te lleve a la pantalla /oculta
  • Que si falla la autenticación (si el usuario no existe o lo que sea), te lleve a la pantalla de /loginfailed
  • Y que si sales con un logout (que luego le pondremos) te lleve a la pantalla de /logout

Ojo, no tienen por qué existir todas esas páginas; los enlaces son los que le llegan al controller, y luego el controller puede que los redirija todos a la pantalla de login, por ejemplo.

Todo eso es lo que necesitamos para el tema de la configuración.

Aparte de esto, en la parte de front, necesitaremos una pantalla de login (login.jsp) que sea un formulario típico con los campos usuario y contraseña. En esta pantalla lo importante es que la etiqueta form lleve en el action el valor j_spring_security_check.

<form id="signin" action="<c:url value= "/j_spring_security_check"></c:url>" method="post">

Y que los campos de usuario y contraseña lleven como name j_username y j_password.

<input name='j_username' placeholder="User" autofocus />
<input name='j_password' type="password" placeholder="Password" />

También necesitamos un Controller para las peticiones de la pantalla de login, ya que eso funciona como cualquier otra vista. Yo lo que he hecho en ese Controller es que la pantalla de /logout y /loginfailed llamen a la vista /login, así solo tengo que crear una vista.

¿Como funciona todo esto?

Bueno, cuando ponemos la dirección de una web para la que hemos definido que necesita un rol específico para poder acceder a ella (por ejemplo la web /oculta –> ) Spring intercepta esa llamada y sabe que tiene que llamar primero a la pantalla de /login para que el usuario se autentifique.

El usuario mete sus credenciales y Spring las valida contra el usuario que tiene. Si se trata de ese usuario, que tiene un rol que le permite ver la web /oculta, lo manda a ella. En caso contrario, lo manda a la pantalla de /loginfailed (que, en nuestro caso, es de nuevo la pantalla de /login pero con un mensaje de error).

Segundo caso

El segundo caso es el realmente interesante; tener guardados los usuarios en una base de datos. Para ello, solo hay que hacer un par de cosas sobre lo explicado anteriormente. El principio es el mismo; si tenemos todo bien definido, Spring se encarga de hacer todas las comprobaciones y llamadas a la base de datos, sin que nosotros tengamos que intervenir.

Lo primero; crear las tablas en BD.
Evidentemente necesitamos una Base de Datos. Yo la he creado en MySql, y se llama users_example (se puede poner el nombre que se quiera, pero lo necesitaremos luego). En esta base de datos, hay que crear dos tablas muy concretas:

CREATE TABLE USERS(
    USERNAME VARCHAR(50) NOT NULL PRIMARY KEY,
    PASSWORD VARCHAR(61) NOT NULL,
    ENABLED BOOLEAN NOT NULL
);

CREATE TABLE AUTHORITIES(
    USERNAME VARCHAR(50) NOT NULL,
    AUTHORITY VARCHAR(50) NOT NULL,
    CONSTRAINT FK_AUTHORITIES_USERS
    FOREIGN KEY(USERNAME) REFERENCES USERS(USERNAME)
);

Las tablas se tienen que llamar así y los campos también. Spring espera que se llamen así.

Además, puesto que vamos a leer de una base de datos, hay que bajarse las dependencias necesarias para que Spring pueda leer bases de datos. En este caso una base de datos mysql. Si utilizáis otra, tendréis que bajaros las dependencias correspondientes a esa. Así que añadimos al POM las siguientes dependencias:

<dependency>
	<groupId>org.springframework</groupId>
	<artifactId>spring-jdbc</artifactId>
	<version>4.3.6.RELEASE</version>
</dependency>
<dependency>
	<groupId>mysql</groupId>
	<artifactId>mysql-connector-java</artifactId>
	<version>5.1.41</version>
</dependency>

A continuación hay que indicarle a Spring que vamos a mirar los usuarios en una base de datos. Esto se hace en el fichero de configuración de seguridad security-context.xml. En este fichero teníamos esto:

<security:user-service>
	<security:user name="Admin" password="Admin123" authorities="ROLE_ADMIN" />
</security:user-service>

donde le indicábamos a Spring que el usuario había que comprobarlo contra esas credenciales. Pues esas 3 líneas las vamos a quitar y las vamos a sustituir por la siguiente:

<security:jdbc-user-service data-source-ref="dataSource"/>

Con esta línea le indicamos que los usuarios los vamos a comprobar contra una base de datos (un datasource). Y en ese «datasource», Spring espera encontrar los datos de acceso a la base de datos, así que tendremos que definir ese datasource con un bean, dentro del security-context.xml. Los datos que pondremos en este datasource son los de nuestra base de datos.

<bean id="dataSource" class= "org.springframework.jdbc.datasource.DriverManagerDataSource">      
  <property name="driverClassName" value="com.mysql.jdbc.Driver"></property>
  <property name="url" value="jdbc:mysql://localhost:3306/users_example"></property>
  <property name="username" value="root"></property>
  <property name="password" value=""></property>
</bean>

Y, con todo definido así, Spring hace su magia y ya podemos autenticarnos contra la base de datos. Eso sí, necesitaremos tener un usuario en la base de datos, claro, así que habrá que insertar uno:

INSERT INTO USERS VALUES('user', '123', true);
INSERT INTO AUTHORITIES VALUES('user', 'ROLE_ADMIN');

Y ya está listo. Con esto, una vez que lancemos la aplicación, cuando intentemos acceder a la web /oculta, nos pedirá un usuario. Metemos user/123 y entraremos.

Una cosa importante, el rol del usuario: en la BD tendremos que ponerle al usuario el rol que necesita para ver la web /oculta que hemos indicado, si no no nos dejará verla (en nuestro caso ROLE_ADMIN).

Detalles

Los detalles son importantes. Y aquí hay dos que hay que tener en cuenta.
Primero: es mejor no tener los datos de la base de datos directamente en el fichero security-context.xml. Lo bueno es tenerlos en un fichero de propiedades. Así, si se cambia la base de datos, solo hay que tocar este fichero de propiedades.

Así que nos creamos un fichero de propiedades en la ruta src/main/resources y lo llamaremos application.properties. Dentro de este fichero ponemos, en formato clave-valor, los datos de acceso a nuestra base de datos:

 db.jdbcUrl=jdbc:mysql://localhost:3306/users_example
 db.username=root
 db.password=
 db.driver=com.mysql.jdbc.Driver

A continuación modificamos el security-context.xml cambiando los datos de acceso por un «${clave}», donde indicamos los datos que hemos puesto en el fichero properties, como se ve a continuación:

<bean id="dataSource" class= "org.springframework.jdbc.datasource.DriverManagerDataSource">      
  <property name="driverClassName" value="${db.driver}"></property>
  <property name="url" value="${db.jdbcUrl}"></property>
  <property name="username" value="${db.username}"></property>
  <property name="password" value="${db.password}"></property>
</bean>

Y, finalmente, le indicamos a Spring donde está ese fichero de propiedades, para que pueda ir ahí a leer los datos. Para ello añadimos, también en el security-context.xml, la línea:

<context:property-placeholder location="classpath:application.properties"/> 

poniendo el nombre de nuestro fichero de propiedades.

Con esto hecho, todo tiene que seguir funcionando como siempre, pero los datos de la base de datos ya están en un fichero aparte.

Como segundo detalle, habría que encriptar la contraseña en la base de datos, porque tenerla en texto plano no es nada seguro. Esto es bastante fácil. En el security-context.xml hay que declarar un bean como sigue:

<bean id="bCryptPasswordEncoder" class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder">
</bean>

Y, donde le decimos a Spring que utilizamos un datasource, le indicamos también que hay un bean para la password:

<security:authentication-manager  alias="authenticationManager">
	<security:authentication-provider>
		<security:password-encoder ref="bCryptPasswordEncoder" />
		<security:jdbc-user-service data-source-ref="dataSource"/>
	</security:authentication-provider>
</security:authentication-manager>

Además de esto, hay otra cosa que hay que tener en cuenta; nuestra password en la base de datos, tiene que estar encriptada. Para ello yo lo que he hecho es que en el controller, he puesto una llamada para que codifique la password y la muestre en la consola. La he copiado de ahí y la he insertado en la BD.

String password = "admin123";
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
System.out.println("Encoded password is " + passwordEncoder.encode(password));

Una vez hecho esto, esas líneas ya se pueden borrar.

Puedes encontrar el proyecto completo en GitHub

Como implementar seguridad en Spring
Etiquetado en: