1. Introducción
Dentro de la cantidad de "sobreinformación" que existe actualmente, voy a intentar dar una píldoras resumidas de los pasos a seguir para securizar vuestra API creada con SringBoot.Vamos a usar JSON Web Tokens (JWT), activaremos Spring Security con la configuración adecuada, y vamos a securizarlo usando una Base de datos H2 como ejemplo de uso de JPA (A modo de ejemplo debería bastar)
Las configuraciones en detalle de cada uno de las piezas, las dejo a discreción del lector en profundizar en ellas.
Os dejo el código fuente de este tutorial en github para vuestra consulta.
2. JWT
Cogiendo la definición de la wikipedia, no vamos a repetirla, destacar solamente:La mayor ventaja de los tokens sobre otros sistemas es el hecho de que no tenga estado (stateless). El backend no necesita mantener un registro de los tokens. Cada token es compacto y auto contenido. Contiene todos los datos necesarios para comprobar su validez, así como la información del usuario para las diferentes peticiones.
3. Vamos a por el código
La estructura del proyecto lo he dividido en la parte que pega al modelo (la cual está asociada con JPA a un ejemplo con H2 y se debería cambiar por el sistema que tenga cada uno) y la otra parte de Security que es la que sería común a cualquier microservicio, incluídos aquellos que tengan que hacer uso del JWT cuando quedan expuestos.En la parte de Security no vamos a hacer uso de ningún controller, realmente no nos hace falta, si no que a través de los Filters que nos proporciona el propio framework de Spring y de Security podemos ser capaces de tener las piezas necesarias para devolver y analizar un JWT.
Por ultimo tenemos una serie de ficheros de configuración (appication.properties, SecurityConfig y SecurityConstants)
4. A programar!
4.1 Autenticación
Lo primero que vamos a analizar es la Authenticación (Respondemos a la pregunta de ¿quién? ).public class JWTAuthenticationFilter extends UsernamePasswordAuthenticationFilter { private AuthenticationManager authenticationManager; public JWTAuthenticationFilter(AuthenticationManager authenticationManager) { this.authenticationManager = authenticationManager; // Sin esta parte la url que atendería sería la de /login setFilterProcessesUrl(SecurityConstants.SIGNIN_URL); } @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { try { Usuario credenciales = new ObjectMapper().readValue(request.getInputStream(), Usuario.class); return authenticationManager.authenticate(new UsernamePasswordAuthenticationToken( credenciales.getUsername(), credenciales.getPassword(), new ArrayList<>())); } catch (IOException e) { throw new RuntimeException(e); } } @Override protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication auth) throws IOException, ServletException { String token = Jwts.builder().setIssuedAt(new Date()).setIssuer(SecurityConstants.ISSUER_INFO) .setSubject(((User) auth.getPrincipal()).getUsername()).claim("roles", "user") .setExpiration(new Date(System.currentTimeMillis() + SecurityConstants.TOKEN_EXPIRATION_TIME)) .signWith(SignatureAlgorithm.HS512, SecurityConstants.SUPER_SECRET_KEY).compact(); response.addHeader(SecurityConstants.HEADER_AUTHORIZACION_KEY, SecurityConstants.TOKEN_BEARER_PREFIX + " " + token); response.setContentType("application/json"); response.setCharacterEncoding("UTF-8"); response.getWriter().write("{\"" + SecurityConstants.TOKEN_PREFIX + "\":\"" + token + "\"}"); } }
Vamos a resaltar las siguientes partes:
// Sin esta parte la url que atendería sería la de /login
setFilterProcessesUrl(SecurityConstants.SIGNIN_URL);
Por otro lado nos fijamos que en el proceso de authenticate le pasamos como queremos hacer la autenticación:
new UsernamePasswordAuthenticationToken()
Y en caso de tener éxito generamos el token. Este se devolverá en la cabecera y además en el body (no es necesario pero es más cómodo).
4.2 Autorización
Ahora vamos a responder ¿Para qué tengo permiso?.public class JWTAuthorizationFilter extends BasicAuthenticationFilter { public JWTAuthorizationFilter(AuthenticationManager authManager) { super(authManager); } @Override protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain chain) throws IOException, ServletException { String header = req.getHeader(SecurityConstants.HEADER_AUTHORIZACION_KEY); if (header == null || !header.startsWith(SecurityConstants.TOKEN_BEARER_PREFIX)) { chain.doFilter(req, res); return; } UsernamePasswordAuthenticationToken authentication = getAuthentication(req); SecurityContextHolder.getContext().setAuthentication(authentication); chain.doFilter(req, res); } private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) { String token = request.getHeader(SecurityConstants.HEADER_AUTHORIZACION_KEY); if (token != null) { // Se procesa el token y se recupera el usuario. String user = Jwts.parser().setSigningKey(SecurityConstants.SUPER_SECRET_KEY) .parseClaimsJws(token.replace(SecurityConstants.TOKEN_BEARER_PREFIX, "")).getBody().getSubject(); if (user != null) { return new UsernamePasswordAuthenticationToken(user, null, new ArrayList<>()); } return null; } return null; } }
*Este filtro podría cambiarse por OnePerRequest, o por un Generic, etc ...
Como vemos no es muy complejo de entender, simplemente buscamos en la cabecera el token. Si lo encontramos lo validamos, y si todo es correcto devolvemos UsernamePasswordAuthenticationToken(), pero en este caso sin la PASS porque ya no nos hace falta.
4.3 La "magia" de Spring
Una de las bondades de Spring es que muchas cosas ya las da intrínsecamente el propio framework. Es algo que está muy bien, aunque en mi caso me suele poner de los nervios y prefiero tener el control, pero que en la mayoría de los caso nos ahorra mucho trabajo.@Service public class UsuarioDetailsServiceImpl implements UserDetailsService { private UsuarioRespository usuarioRepository; public UsuarioDetailsServiceImpl(UsuarioRespository usuarioRepository) { this.usuarioRepository = usuarioRepository; } @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { Usuario usuario = usuarioRepository.findByUsername(username); if (usuario == null) { throw new UsernameNotFoundException(username); } return new User(usuario.getUsername(), usuario.getPassword(), emptyList()); } }
El mero hecho de implementar esta Interfaz, le da a Security la capacidad de saber cómo debe hacer para recuperar un usuario de la Base de Datos.
4.4 Configuración
El último paso es la parte de configuración de los filtros y de security. La que aquí pongo es muy elemental y se debe se bastante más exquisito en las cuestiones a añadir.
@Configuration // @EnableGlobalMethodSecurity(securedEnabled = true) // Este es el método 1 de // securización a usar: @Secured @EnableGlobalMethodSecurity(prePostEnabled = true) // Este es el método 2 de securización a usar: @PreAuthorize public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired protected UserDetailsService userDetailsService; @Bean public BCryptPasswordEncoder bCryptPasswordEncoder() { return new BCryptPasswordEncoder(); } @Override protected void configure(HttpSecurity http) throws Exception { http.cors().and().csrf().disable().authorizeRequests().antMatchers("/h2-console/**").permitAll().anyRequest() .authenticated().and().addFilter(new JWTAuthenticationFilter(authenticationManager())) .addFilter(new JWTAuthorizationFilter(authenticationManager())).sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.STATELESS); http.headers().frameOptions().disable(); } @Override public void configure(AuthenticationManagerBuilder auth) throws Exception { // Se define la clase que recupera los usuarios y el algoritmo para procesar las // passwords // Se podría usar un método custom en vez de bCryptPasswordEncoder() auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder()); } @Bean CorsConfigurationSource corsConfigurationSource() { final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", new CorsConfiguration().applyPermitDefaultValues()); return source; } }
Como podemos ver, se ha usado un método para desencriptar la contraseña (cada implementación debería sobreescribir este método y adaptarlo a sus necesidades.
Y por ultimo enlazamos "la magia":
auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder());
5. Resumiendo
No he pretendido ser muy exquisito con todo el código aquí explicado, pero sí dejar "subrayado" aquellas partes que en otros lenguajes o frameworks (como Node) es más fácil de ver y quizá en Spring está algo más oculto.
No hay comentarios:
Publicar un comentario
Nota: solo los miembros de este blog pueden publicar comentarios.