¡No te pierdas ninguna publicación! Suscríbete a The Softtek Blog
En este post vamos a explicar cómo autenticar una API mediante tokens, para poder garantizar que los usuarios que consumen nuestros servicios tienen permisos para hacerlo y son quien dicen ser.
Antes de comenzar, es importante entender algunos conceptos básicos que aparecerán a lo largo de este post:
JSON Based Token (JWT, https://jwt.io/) es un estándar de código abierto basado en JSON para crear tokens de acceso que nos permiten securizar las comunicaciones entre cliente y servidor
Disponemos pues, de hasta tres servidores: el servidor de nuestra API, el servidor de autenticación y el servidor de autorización. Sin embargo, como veremos en este post, podemos implementar las tres funcionalidades en una única aplicación.
Estos token están compuestos por tres partes:
La mejor forma de entenderlo es verlo funcionando, asi que... ¡¡allá vamos!! Lo primero sería crear una aplicación Spring Boot para implementar nuestro API. Esto ya lo hicimos en el webinar "Construyendo un API REST con Spring Boot".
Accedemos a la web de Spring Initializr y generamos un proyecto Maven con Java y Spring Boot 2.1.1, rellenamos el group, el artifact de nuestro proyecto (en este caso "es.softtek" y "jwt-demo") y añadimos dependencias para Web:
Una vez generado, descomprimimos el zip y lo construimos ejecutando el comando "mvn package" sobre la raíz de nuestro proyecto. A continuación, generamos nuestro fichero .project ejecutando el comando "mvn eclipse:eclipse" desde la misma ubicación. Una vez compilado y construido, importamos el proyecto en nuestro IDE.
Vamos a crear un controlador REST para responder a todas las invocaciones del endpoint /hello, que simplemente devuelva un mensaje de bienvenida a todos los clientes que tengan autorización para acceder al servicio (por defecto, todos).
package es.softtek.jwtDemo.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class HelloWorldController {
@RequestMapping("hello")
public String helloWorld(@RequestParam(value="name", defaultValue="World") String name) {
return "Hello "+name+"!!";
}
}
Básicamente hemos usado las siguientes anotaciones:
Si arrancamos la aplicación (ejecutamos comando "mvn spring-boot:run" desde la raíz de nuestra aplicación) podemos testear nuestro servicio. Aún no hemos añadido ninguna configuración de seguridad, por lo que podemos invocar al servicio sin restricciones:
Si invocamos al endpoint de nuestro servicio (http://localhost:8080/hello) nos devolverá el mensaje por defecto:
También podemos indicar un nombre invocando al mismo servicio, pero añadiendo nuestro parámetro (http://localhost:8080/hello?name=Sebas):
A continuación, vamos a añadir configuración de seguridad a nuestra aplicación para proteger el endpoint /hello que acabamos de implementar:
Lo primero que necesitamos es añadir las dependencias para Spring Security y de JWT:
Vamos a crear otro controlador REST para implementar el proceso de autenticación mediante un login usuario/contraseña:
package es.softtek.jwtDemo.controller;
import java.util.Date;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import es.softtek.jwtDemo.dto.User;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
@RestController
public class UserController {
@PostMapping("user")
public User login(@RequestParam("user") String username, @RequestParam("password") String pwd) {
String token = getJWTToken(username);
User user = new User();
user.setUser(username);
user.setToken(token);
return user;
}
private String getJWTToken(String username) {
String secretKey = "mySecretKey";
List<GrantedAuthority> grantedAuthorities = AuthorityUtils
.commaSeparatedStringToAuthorityList("ROLE_USER");
String token = Jwts
.builder()
.setId("softtekJWT")
.setSubject(username)
.claim("authorities",
grantedAuthorities.stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.toList()))
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + 600000))
.signWith(SignatureAlgorithm.HS512,
secretKey.getBytes()).compact();
return "Bearer " + token;
}
}
El método login(...) interceptará las peticiones POST al endpoint /user y recibirá como parámetros el usuario y contraseña. Como se puede observar, para este ejemplo no se realiza ninguna validación de usuario y contraseña, por lo que para cualquier valor de dichos parámetros dejaremos paso. Obviamente, para un proyecto real, en este punto deberíamos autenticar el usuario contra nuestra base de datos o contra cualquier proveedor externo.
Utilizamos el método getJWTToken(...) para construir el token, delegando en la clase de utilidad Jwts que incluye información sobre su expiración y un objeto de GrantedAuthority de Spring que, como veremos más adelante, usaremos para autorizar las peticiones a los recursos protegidos.
Por último, editaremos nuestra clase de arranque JwtDemoApplication para añadir la siguiente configuración:
package es.softtek.jwtDemo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import es.softtek.jwtDemo.security.JWTAuthorizationFilter;
@SpringBootApplication
public class JwtDemoApplication {
public static void main(String[] args) {
SpringApplication.run(JwtDemoApplication.class, args);
}
@EnableWebSecurity
@Configuration
class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.addFilterAfter(new JWTAuthorizationFilter(), UsernamePasswordAuthenticationFilter.class)
.authorizeRequests()
.antMatchers(HttpMethod.POST, "/user").permitAll()
.anyRequest().authenticated();
}
}
}
La clase interna WebSecurityConfig, decorada con @EnableWebSecurity y @Configuration, nos permite especificar la configuración de acceso a los recursos publicados. En este caso se permiten todas las llamadas al controlador /user, pero el resto de las llamadas requieren autenticación.
En este momento, si reiniciamos la aplicación y hacemos una llamada a http://localhost:8080/hello, nos devolverá un error 403 informando al usuario de que no está autorizado para acceder a ese recurso que se encuentra protegido:
Por último, necesitamos implementar el proceso de autorización, que sea capaz de interceptar las invocaciones a recursos protegidos para recuperar el token y determinar si el cliente tiene permisos o no.
Para ello implementaremos el siguiente filtro, JWTAuthorizationFilter:
package es.softtek.jwtDemo.security;
import java.io.IOException;
import java.util.List;
import java.util.stream.Collectors;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.OncePerRequestFilter;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.UnsupportedJwtException;
public class JWTAuthorizationFilter extends OncePerRequestFilter {
private final String HEADER = "Authorization";
private final String PREFIX = "Bearer ";
private final String SECRET = "mySecretKey";
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
try {
if (existeJWTToken(request, response)) {
Claims claims = validateToken(request);
if (claims.get("authorities") != null) {
setUpSpringAuthentication(claims);
} else {
SecurityContextHolder.clearContext();
}
} else {
SecurityContextHolder.clearContext();
}
chain.doFilter(request, response);
} catch (ExpiredJwtException | UnsupportedJwtException | MalformedJwtException e) {
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
((HttpServletResponse) response).sendError(HttpServletResponse.SC_FORBIDDEN, e.getMessage());
return;
}
}
private Claims validateToken(HttpServletRequest request) {
String jwtToken = request.getHeader(HEADER).replace(PREFIX, "");
return Jwts.parser().setSigningKey(SECRET.getBytes()).parseClaimsJws(jwtToken).getBody();
}
/**
* Metodo para autenticarnos dentro del flujo de Spring
*
* @param claims
*/
private void setUpSpringAuthentication(Claims claims) {
@SuppressWarnings("unchecked")
List<String> authorities = (List) claims.get("authorities");
UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(claims.getSubject(), null,
authorities.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList()));
SecurityContextHolder.getContext().setAuthentication(auth);
}
private boolean existeJWTToken(HttpServletRequest request, HttpServletResponse res) {
String authenticationHeader = request.getHeader(HEADER);
if (authenticationHeader == null || !authenticationHeader.startsWith(PREFIX))
return false;
return true;
}
}
Este filtro intercepta todas las invocaciones al servidor (extiende de OncePerRequestFilter) y:
Para este último punto, se hace uso del objeto GrantedAuthority que se incluyó en el token durante el proceso de autenticación.
Una vez que hemos implementado la lógica de autenticación y autorización, vamos a volver a probar nuestro API.
Para ello podemos usar Postman, una sencilla extensión de Chrome que nos permite ejecutar y monitorizar peticiones.
1) Arrancamos nuestro servidor ejecutando el comando "mvn spring-boot:run".
2) Desde Postman, hacemos una petición GET a /hello y comprobamos que nos da un 403, pues el recurso está protegido:
3) Desde Postman, hacemos una petición POST a /user para autenticarnos, incluyendo usuario y contraseña, y obtenemos un token de acceso:
4) Volvemos a hacer la petición GET del paso 2) incluyendo una cabecera Authorization con el token generado en el punto 3)
Hemos visto una manera sencilla de autenticar y autorizar las peticiones a un API REST construido con Java y Spring Boot.
En posteriores publicaciones veremos cómo controlar el ciclo de vida de nuestros tokens, y generar excepciones, e implementaremos la lógica de autenticación para validar nuestro usuario y contraseña contra una base de datos.
El código de la aplicación está publicado en mi Github.
¡¡Muchas gracias por leer este post, espero que haya sido de utilidad!! Para cualquier duda o consulta, podéis usar la sección de comentarios que aparece a continuación.