Hoy traigo un articulo practico donde vamos a ver como montar un servidor JWT con Spring Boot en 5 minutos. Podeis descargar el codigo fuente de Github
Que es JWT
JWT o Json Web Token es un estandar abierto para crear tokens de acceso que cercioren que quien lo esta usando tiene ciertas propiedades. Por ejemplo, podemos crear un token de acceso que nos asegure que el que lo usa es un administrador del sistema
La estructura de un token JWT se divide en 3 partes:
- Header: Indica el algoritmo con el que se ha encriptado y el tipo. Un ejemplo seria:
{"alg": "HS256","typ": "JWT"}
- Payload: Esta es la parte donde se definen las propiedades tanto del token como del que lo esta usando. Por ejemplo:
{"userid": "1234", "admin": true}
- Signature: La ultima parte es la firma. Se concatena el header con el payload separados por un punto previamente codificados en Base 64. El resultado se encripta usando el algoritmo que hemos definido en la cabecera, en este caso HMAC-SHA256
Una vez que tenemos las 3 partes se codifican en Base64 y cada parte se concatena con un punto. Lo que nos quedaria seria algo asi:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyaWQiWV9.PjmN-JugVug
Montando JWT en Spring Boot
A estas alturas no creo que haya alguien que no conozca Spring Boot. Por si no lo conoces puedes encontrar toda la informacion sobre este proyecto en su pagina web
Gracias a Spring Boot podemos crear aplicaciones web muy rapidamente y aqui vamos a ver como con muy pocas lineas podemos tener una aplicacion con una API REST protegida mediante JWT
Lo primero que tenemos que crear es la clase que arranca nuestro servicio
@ServletComponentScan @SpringBootApplication public class JWTServerApplication { public static void main(String[] args) { SpringApplication.run(JWTServerApplication.class, args); } @Bean public FilterRegistrationBean corsFilter() { final CorsConfiguration config = new CorsConfiguration(); config.setAllowCredentials(true); config.addAllowedOrigin("*"); config.addAllowedHeader("*"); config.addAllowedMethod("*"); final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", config); final FilterRegistrationBean bean = new FilterRegistrationBean(new CorsFilter(source)); bean.setOrder(0); return bean; } }
Lo mas importate aqui es el bean del metodo corsFilter
. Al ser un servicio REST queremos que se pueda acceder desde cualquier host y para eso hay que saltarse las restricciones CORS
Lo siguiente es usar un filtro para interceptar las peticiones a la API y comprobar el token
@WebFilter(urlPatterns = "/api/*") public class JwtFilter implements Filter { @Value("${jwt.secret}") private String secret; @Override public void init(final FilterConfig filterConfig) throws ServletException { } @Override public void doFilter(final ServletRequest req, final ServletResponse res, final FilterChain chain) throws IOException, ServletException { final HttpServletRequest request = (HttpServletRequest) req; final HttpServletResponse response = (HttpServletResponse) res; final String authHeader = request.getHeader("authorization"); if (HttpMethod.OPTIONS.toString().equals(request.getMethod())) { response.setStatus(HttpServletResponse.SC_OK); chain.doFilter(req, res); } else { if (authHeader == null || !authHeader.startsWith("Bearer ")) { response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); return; } final String token = authHeader.substring(7); try { final Claims claims = Jwts.parser() .setSigningKey(TextCodec.BASE64.encode(secret)) .parseClaimsJws(token) .getBody(); request.setAttribute("claims", claims); } catch (final JwtException e) { response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); return; } chain.doFilter(req, res); } } @Override public void destroy() { } }
Todas las peticiones que vayan al path /api
son las que vamos a capturar siempre y cuando no sean de tipo OPTIONS. Estas peticiones no se capturan porque son las que hacen las llamadas AJAX antes de hacer la peticion real y en ellas no se envia el token. Si las capturasemos no tendriamos token y siempre serian erroneas
El token viene en la cabecera authorization
con el formato Bearer token
. Lo cogemos y lo desencriptamos usando nuestro secret
. Este secret se configura en el fichero application.yml de la aplicacion Spring Boot
Si todo es correcto añadimos los claims (propiedades) como un atributo de la request que se podran coger mas tarde en el metodo del RestController. Veamos un ejemplo:
@RestController @RequestMapping("/api/me") public class MeController { @Autowired private UserService userService; @RequestMapping(method = RequestMethod.GET) public UserJson getUser(@RequestAttribute("claims") final Claims claims) throws NotFoundException { return userService.getUserByEmail(claims.getSubject()); } }
Como se puede ver cogemos un @RequestAttribute("claims")
donde nos vendran las propiedades, en este caso en la propiedad subject tenemos el email del usuario. Podemos definir las propiedades que queramos a la hora de crear el token. En este proyecto se hace a la hora de hacer login de la siguiente manera
@RestController public class LoginController { @Value("${jwt.secret}") private String secret; @Autowired private UserService userService; @RequestMapping(value = "/login", method = RequestMethod.POST, produces = MediaType.TEXT_PLAIN_VALUE) public ResponseEntity<String> login(@RequestBody @Valid final LoginRequest login) throws ServletException { final boolean existUser = userService.existUser(login.getEmail(), login.getPassword()); if (!existUser) { return new ResponseEntity<>(HttpStatus.UNAUTHORIZED); } final Instant now = Instant.now(); final String jwt = Jwts.builder() .setSubject(login.getEmail()) .setIssuedAt(Date.from(now)) .setExpiration(Date.from(now.plus(1, ChronoUnit.DAYS))) .signWith(SignatureAlgorithm.HS256, TextCodec.BASE64.encode(secret)) .compact(); return new ResponseEntity<>(jwt, HttpStatus.OK); } }
Creamos el token usando como subject el email del usuario y para mayor seguridad ponemos la hora de creacion (issuedAt) y una fecha de expiracion (expiration) y lo devolvemos al hacer login
En el ejemplo del repositorio estan incluidos los casos de uso de login y registro asi como la persistencia de los usuarios en base de datos MongoDB. Ademas, estan todos los tests basados en BDD con Cucumber asi como el fichero Dockerfile y docker-compose para ejecutarlo como servicio independiente