Spring Security con JWT para API REST

Publicado: 2022-03-11

Spring se considera un marco confiable en el ecosistema de Java y se usa ampliamente. Ya no es válido referirse a Spring como un marco, ya que es más un término general que cubre varios marcos. Uno de estos marcos es Spring Security, que es un marco de autenticación y autorización potente y personalizable. Se considera el estándar de facto para asegurar aplicaciones basadas en Spring.

A pesar de su popularidad, debo admitir que cuando se trata de aplicaciones de una sola página, no es simple ni directo de configurar. Sospecho que la razón es que comenzó más como un marco orientado a la aplicación MVC, donde la representación de la página web ocurre en el lado del servidor y la comunicación se basa en la sesión.

Si el back-end se basa en Java y Spring, tiene sentido utilizar Spring Security para la autenticación/autorización y configurarlo para la comunicación sin estado. Si bien hay muchos artículos que explican cómo se hace esto, para mí fue frustrante configurarlo por primera vez y tuve que leer y resumir información de múltiples fuentes. Es por eso que decidí escribir este artículo, donde intentaré resumir y cubrir todos los detalles sutiles y debilidades que puede encontrar durante el proceso de configuración.

Definición de terminología

Antes de profundizar en los detalles técnicos, quiero definir explícitamente la terminología utilizada en el contexto de Spring Security solo para asegurarme de que todos hablamos el mismo idioma.

Estos son los términos que debemos abordar:

  • La autenticación se refiere al proceso de verificación de la identidad de un usuario, en función de las credenciales proporcionadas. Un ejemplo común es ingresar un nombre de usuario y una contraseña cuando inicia sesión en un sitio web. Puedes pensar en ello como una respuesta a la pregunta ¿Quién eres? .
  • La autorización se refiere al proceso de determinar si un usuario tiene el permiso adecuado para realizar una acción en particular o leer datos en particular, suponiendo que el usuario se haya autenticado con éxito. Puede considerarlo como una respuesta a la pregunta ¿Puede un usuario hacer/leer esto? .
  • El principio se refiere al usuario actualmente autenticado.
  • La autoridad otorgada se refiere al permiso del usuario autenticado.
  • El rol se refiere a un grupo de permisos del usuario autenticado.

Creación de una aplicación Spring básica

Antes de pasar a la configuración del marco Spring Security, creemos una aplicación web Spring básica. Para esto, podemos usar un Spring Initializr y generar un proyecto de plantilla. Para una aplicación web simple, solo una dependencia de Spring web framework es suficiente:

 <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> </dependencies>

Una vez que hemos creado el proyecto, podemos agregarle un controlador REST simple de la siguiente manera:

 @RestController @RequestMapping("hello") public class HelloRestController { @GetMapping("user") public String helloUser() { return "Hello User"; } @GetMapping("admin") public String helloAdmin() { return "Hello Admin"; } }

Después de esto, si compilamos y ejecutamos el proyecto, podemos acceder a las siguientes URL en el navegador web:

  • http://localhost:8080/hello/user devolverá la cadena Hello User .
  • http://localhost:8080/hello/admin devolverá la cadena Hello Admin .

Ahora, podemos agregar el marco Spring Security a nuestro proyecto, y podemos hacerlo agregando la siguiente dependencia a nuestro archivo pom.xml :

 <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> </dependencies>

Agregar otras dependencias de Spring Framework normalmente no tiene un efecto inmediato en una aplicación hasta que proporcionamos la configuración correspondiente, pero Spring Security es diferente en que tiene un efecto inmediato, y esto generalmente confunde a los nuevos usuarios. Después de agregarlo, si reconstruimos y ejecutamos el proyecto y luego intentamos acceder a una de las URL antes mencionadas en lugar de ver el resultado, seremos redirigidos a http://localhost:8080/login . Este es el comportamiento predeterminado porque el marco Spring Security requiere autenticación lista para usar para todas las URL.

Para pasar la autenticación, podemos usar el nombre de user predeterminado y encontrar una contraseña generada automáticamente en nuestra consola:

 Using generated security password: 1fc15145-dfee-4bec-a009-e32ca21c77ce

Recuerde que la contraseña cambia cada vez que volvemos a ejecutar la aplicación. Si queremos cambiar este comportamiento y hacer que la contraseña sea estática, podemos agregar la siguiente configuración a nuestro archivo application.properties :

 spring.security.user.password=Test12345_

Ahora, si ingresamos las credenciales en el formulario de inicio de sesión, seremos redirigidos a nuestra URL y veremos el resultado correcto. Tenga en cuenta que el proceso de autenticación listo para usar se basa en sesiones, y si queremos cerrar sesión, podemos acceder a la siguiente URL: http://localhost:8080/logout

Este comportamiento listo para usar puede ser útil para las aplicaciones web clásicas de MVC donde tenemos autenticación basada en sesión, pero en el caso de aplicaciones de una sola página, generalmente no es útil porque en la mayoría de los casos de uso, tenemos autenticación del lado del cliente. representación y autenticación sin estado basada en JWT. En este caso, tendremos que personalizar en gran medida el marco Spring Security, lo que haremos en el resto del artículo.

Como ejemplo, implementaremos una aplicación web de librería clásica y crearemos un back-end que proporcionará API CRUD para crear autores y libros, además de API para la administración y autenticación de usuarios.

Descripción general de la arquitectura de seguridad de Spring

Antes de comenzar a personalizar la configuración, analicemos primero cómo funciona la autenticación de Spring Security detrás de escena.

El siguiente diagrama presenta el flujo y muestra cómo se procesan las solicitudes de autenticación:

Arquitectura de seguridad de primavera

Arquitectura de seguridad de primavera

Ahora, dividamos este diagrama en componentes y discutamos cada uno de ellos por separado.

Cadena de filtros de seguridad de resorte

Cuando agrega el marco Spring Security a su aplicación, registra automáticamente una cadena de filtros que intercepta todas las solicitudes entrantes. Esta cadena consta de varios filtros, y cada uno de ellos maneja un caso de uso particular.

Por ejemplo:

  • Compruebe si la URL solicitada es de acceso público, según la configuración.
  • En caso de autenticación basada en sesión, verifique si el usuario ya está autenticado en la sesión actual.
  • Comprobar si el usuario está autorizado para realizar la acción solicitada, y así sucesivamente.

Un detalle importante que quiero mencionar es que los filtros Spring Security se registran con el orden más bajo y son los primeros filtros invocados. Para algunos casos de uso, si desea colocar su filtro personalizado frente a ellos, deberá agregar relleno a su pedido. Esto se puede hacer con la siguiente configuración:

 spring.security.filter.order=10

Una vez que agreguemos esta configuración a nuestro archivo application.properties , tendremos espacio para 10 filtros personalizados frente a los filtros Spring Security.

Administrador de autenticación

Puede pensar en AuthenticationManager como un coordinador donde puede registrar varios proveedores y, según el tipo de solicitud, entregará una solicitud de autenticación al proveedor correcto.

Proveedor de autenticación

AuthenticationProvider procesa tipos específicos de autenticación. Su interfaz expone solo dos funciones:

  • authenticate realiza la autenticación con la solicitud.
  • supports comprobaciones si este proveedor admite el tipo de autenticación indicado.

Una implementación importante de la interfaz que usamos en nuestro proyecto de muestra es DaoAuthenticationProvider , que recupera los detalles del usuario de UserDetailsService .

Servicio de detalles de usuario

UserDetailsService se describe como una interfaz central que carga datos específicos del usuario en la documentación de Spring.

En la mayoría de los casos de uso, los proveedores de autenticación extraen la información de identidad del usuario en función de las credenciales de una base de datos y luego realizan la validación. Debido a que este caso de uso es tan común, los desarrolladores de Spring decidieron extraerlo como una interfaz separada, lo que expone la función única:

  • loadUserByUsername acepta el nombre de usuario como parámetro y devuelve el objeto de identidad del usuario.

Autenticación usando JWT con Spring Security

Después de analizar los aspectos internos del marco Spring Security, configurémoslo para la autenticación sin estado con un token JWT.

Para personalizar Spring Security, necesitamos una clase de configuración anotada con la anotación @EnableWebSecurity en nuestro classpath. Además, para simplificar el proceso de personalización, el marco expone una clase WebSecurityConfigurerAdapter . Extenderemos este adaptador y anularemos sus dos funciones para:

  1. Configure el administrador de autenticación con el proveedor correcto
  2. Configurar la seguridad web (URL públicas, URL privadas, autorización, etc.)
 @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { // TODO configure authentication manager } @Override protected void configure(HttpSecurity http) throws Exception { // TODO configure web security } }

En nuestra aplicación de muestra, almacenamos las identidades de los usuarios en una base de datos MongoDB, en la colección de users . Estas identidades son mapeadas por la entidad User , y sus operaciones CRUD son definidas por el repositorio UserRepo Spring Data.

Ahora, cuando aceptamos la solicitud de autenticación, debemos recuperar la identidad correcta de la base de datos utilizando las credenciales proporcionadas y luego verificarla. Para ello, necesitamos la implementación de la interfaz UserDetailsService , que se define de la siguiente manera:

 public interface UserDetailsService { UserDetails loadUserByUsername(String username) throws UsernameNotFoundException; }

Aquí, podemos ver que se requiere devolver el objeto que implementa la interfaz UserDetails , y nuestra entidad User lo implementa (para obtener detalles de implementación, consulte el repositorio del proyecto de muestra). Teniendo en cuenta el hecho de que expone solo el prototipo de una sola función, podemos tratarlo como una interfaz funcional y proporcionar la implementación como una expresión lambda.

 @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { private final UserRepo userRepo; public SecurityConfig(UserRepo userRepo) { this.userRepo = userRepo; } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(username -> userRepo .findByUsername(username) .orElseThrow( () -> new UsernameNotFoundException( format("User: %s, not found", username) ) )); } // Details omitted for brevity }

Aquí, la llamada a la función auth.userDetailsService iniciará la instancia de DaoAuthenticationProvider utilizando nuestra implementación de la interfaz UserDetailsService y la registrará en el administrador de autenticación.

Junto con el proveedor de autenticación, debemos configurar un administrador de autenticación con el esquema de codificación de contraseña correcto que se usará para la verificación de credenciales. Para esto, necesitamos exponer la implementación preferida de la interfaz PasswordEncoder como un bean.

En nuestro proyecto de muestra, utilizaremos el algoritmo de hash de contraseña bcrypt.

 @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { private final UserRepo userRepo; public SecurityConfig(UserRepo userRepo) { this.userRepo = userRepo; } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(username -> userRepo .findByUsername(username) .orElseThrow( () -> new UsernameNotFoundException( format("User: %s, not found", username) ) )); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } // Details omitted for brevity }

Habiendo configurado el administrador de autenticación, ahora necesitamos configurar la seguridad web. Estamos implementando una API REST y necesitamos autenticación sin estado con un token JWT; por lo tanto, necesitamos configurar las siguientes opciones:

  • Habilite CORS y deshabilite CSRF.
  • Establezca la gestión de sesiones en sin estado.
  • Establecer el controlador de excepciones de solicitudes no autorizadas.
  • Establecer permisos en puntos finales.
  • Agregar filtro de token JWT.

Esta configuración se implementa de la siguiente manera:

 @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { private final UserRepo userRepo; private final JwtTokenFilter jwtTokenFilter; public SecurityConfig(UserRepo userRepo, JwtTokenFilter jwtTokenFilter) { this.userRepo = userRepo; this.jwtTokenFilter = jwtTokenFilter; } // Details omitted for brevity @Override protected void configure(HttpSecurity http) throws Exception { // Enable CORS and disable CSRF http = http.cors().and().csrf().disable(); // Set session management to stateless http = http .sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and(); // Set unauthorized requests exception handler http = http .exceptionHandling() .authenticationEntryPoint( (request, response, ex) -> { response.sendError( HttpServletResponse.SC_UNAUTHORIZED, ex.getMessage() ); } ) .and(); // Set permissions on endpoints http.authorizeRequests() // Our public endpoints .antMatchers("/api/public/**").permitAll() .antMatchers(HttpMethod.GET, "/api/author/**").permitAll() .antMatchers(HttpMethod.POST, "/api/author/search").permitAll() .antMatchers(HttpMethod.GET, "/api/book/**").permitAll() .antMatchers(HttpMethod.POST, "/api/book/search").permitAll() // Our private endpoints .anyRequest().authenticated(); // Add JWT token filter http.addFilterBefore( jwtTokenFilter, UsernamePasswordAuthenticationFilter.class ); } // Used by spring security if CORS is enabled. @Bean public CorsFilter corsFilter() { UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); CorsConfiguration config = new CorsConfiguration(); config.setAllowCredentials(true); config.addAllowedOrigin("*"); config.addAllowedHeader("*"); config.addAllowedMethod("*"); source.registerCorsConfiguration("/**", config); return new CorsFilter(source); } }

Tenga en cuenta que agregamos el JwtTokenFilter antes del UsernamePasswordAuthenticationFilter interno de Spring Security. Estamos haciendo esto porque necesitamos acceso a la identidad del usuario en este punto para realizar la autenticación/autorización, y su extracción ocurre dentro del filtro de token JWT según el token JWT proporcionado. Esto se implementa de la siguiente manera:

 @Component public class JwtTokenFilter extends OncePerRequestFilter { private final JwtTokenUtil jwtTokenUtil; private final UserRepo userRepo; public JwtTokenFilter(JwtTokenUtil jwtTokenUtil, UserRepo userRepo) { this.jwtTokenUtil = jwtTokenUtil; this.userRepo = userRepo; } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { // Get authorization header and validate final String header = request.getHeader(HttpHeaders.AUTHORIZATION); if (isEmpty(header) || !header.startsWith("Bearer ")) { chain.doFilter(request, response); return; } // Get jwt token and validate final String token = header.split(" ")[1].trim(); if (!jwtTokenUtil.validate(token)) { chain.doFilter(request, response); return; } // Get user identity and set it on the spring security context UserDetails userDetails = userRepo .findByUsername(jwtTokenUtil.getUsername(token)) .orElse(null); UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken( userDetails, null, userDetails == null ? List.of() : userDetails.getAuthorities() ); authentication.setDetails( new WebAuthenticationDetailsSource().buildDetails(request) ); SecurityContextHolder.getContext().setAuthentication(authentication); chain.doFilter(request, response); } }

Antes de implementar nuestra función API de inicio de sesión, debemos realizar un paso más: necesitamos acceso al administrador de autenticación. De forma predeterminada, no es de acceso público y debemos exponerlo explícitamente como un bean en nuestra clase de configuración.

Esto puede hacerse de la siguiente manera:

 @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { // Details omitted for brevity @Override @Bean public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } }

Y ahora, estamos listos para implementar nuestra función API de inicio de sesión:

 @Api(tags = "Authentication") @RestController @RequestMapping(path = "api/public") public class AuthApi { private final AuthenticationManager authenticationManager; private final JwtTokenUtil jwtTokenUtil; private final UserViewMapper userViewMapper; public AuthApi(AuthenticationManager authenticationManager, JwtTokenUtil jwtTokenUtil, UserViewMapper userViewMapper) { this.authenticationManager = authenticationManager; this.jwtTokenUtil = jwtTokenUtil; this.userViewMapper = userViewMapper; } @PostMapping("login") public ResponseEntity<UserView> login(@RequestBody @Valid AuthRequest request) { try { Authentication authenticate = authenticationManager .authenticate( new UsernamePasswordAuthenticationToken( request.getUsername(), request.getPassword() ) ); User user = (User) authenticate.getPrincipal(); return ResponseEntity.ok() .header( HttpHeaders.AUTHORIZATION, jwtTokenUtil.generateAccessToken(user) ) .body(userViewMapper.toUserView(user)); } catch (BadCredentialsException ex) { return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); } } }

Aquí, verificamos las credenciales proporcionadas mediante el administrador de autenticación y, en caso de éxito, generamos el token JWT y lo devolvemos como un encabezado de respuesta junto con la información de identidad del usuario en el cuerpo de la respuesta.

Autorización con Spring Security

En la sección anterior, configuramos un proceso de autenticación y configuramos URL públicas/privadas. Esto puede ser suficiente para aplicaciones simples, pero para la mayoría de los casos de uso del mundo real, siempre necesitamos políticas de acceso basadas en roles para nuestros usuarios. En este capítulo, abordaremos este problema y configuraremos un esquema de autorización basado en roles utilizando el marco Spring Security.

En nuestra aplicación de muestra, hemos definido los siguientes tres roles:

  • USER_ADMIN nos permite administrar los usuarios de la aplicación.
  • AUTHOR_ADMIN nos permite gestionar autores.
  • BOOK_ADMIN nos permite gestionar libros.

Ahora, debemos aplicarlos a las URL correspondientes:

  • api/public es de acceso público.
  • api/admin/user puede acceder a los usuarios con el rol USER_ADMIN .
  • api/author puede acceder a los usuarios con el rol AUTHOR_ADMIN .
  • api/book puede acceder a los usuarios con el rol BOOK_ADMIN .

El marco Spring Security nos proporciona dos opciones para configurar el esquema de autorización:

  • Configuración basada en URL
  • Configuración basada en anotaciones

Primero, veamos cómo funciona la configuración basada en URL. Se puede aplicar a la configuración de seguridad web de la siguiente manera:

 @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { // Details omitted for brevity @Override protected void configure(HttpSecurity http) throws Exception { // Enable CORS and disable CSRF http = http.cors().and().csrf().disable(); // Set session management to stateless http = http .sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and(); // Set unauthorized requests exception handler http = http .exceptionHandling() .authenticationEntryPoint( (request, response, ex) -> { response.sendError( HttpServletResponse.SC_UNAUTHORIZED, ex.getMessage() ); } ) .and(); // Set permissions on endpoints http.authorizeRequests() // Our public endpoints .antMatchers("/api/public/**").permitAll() .antMatchers(HttpMethod.GET, "/api/author/**").permitAll() .antMatchers(HttpMethod.POST, "/api/author/search").permitAll() .antMatchers(HttpMethod.GET, "/api/book/**").permitAll() .antMatchers(HttpMethod.POST, "/api/book/search").permitAll() // Our private endpoints .antMatchers("/api/admin/user/**").hasRole(Role.USER_ADMIN) .antMatchers("/api/author/**").hasRole(Role.AUTHOR_ADMIN) .antMatchers("/api/book/**").hasRole(Role.BOOK_ADMIN) .anyRequest().authenticated(); // Add JWT token filter http.addFilterBefore( jwtTokenFilter, UsernamePasswordAuthenticationFilter.class ); } // Details omitted for brevity }

Como puede ver, este enfoque es simple y directo, pero tiene una desventaja. El esquema de autorización en nuestra aplicación puede ser complejo, y si definimos todas las reglas en un solo lugar, se volverá muy grande, complejo y difícil de leer. Debido a esto, normalmente prefiero usar una configuración basada en anotaciones.

El marco Spring Security define las siguientes anotaciones para la seguridad web:

  • @PreAuthorize es compatible con Spring Expression Language y se usa para proporcionar un control de acceso basado en expresiones antes de ejecutar el método.
  • @PostAuthorize es compatible con Spring Expression Language y se utiliza para proporcionar un control de acceso basado en expresiones después de ejecutar el método (proporciona la capacidad de acceder al resultado del método).
  • @PreFilter es compatible con Spring Expression Language y se usa para filtrar la colección o las matrices antes de ejecutar el método en función de las reglas de seguridad personalizadas que definimos.
  • @PostFilter es compatible con Spring Expression Language y se usa para filtrar la colección o las matrices devueltas después de ejecutar el método en función de las reglas de seguridad personalizadas que definimos (proporciona la capacidad de acceder al resultado del método).
  • @Secured no es compatible con Spring Expression Language y se usa para especificar una lista de roles en un método.
  • @RolesAllowed no es compatible con Spring Expression Language y es la anotación equivalente de JSR 250 de la anotación @Secured .

Estas anotaciones están deshabilitadas de forma predeterminada y se pueden habilitar en nuestra aplicación de la siguiente manera:

 @EnableWebSecurity @EnableGlobalMethodSecurity( securedEnabled = true, jsr250Enabled = true, prePostEnabled = true ) public class SecurityConfig extends WebSecurityConfigurerAdapter { // Details omitted for brevity }


securedEnabled = true habilita la anotación @Secured .
jsr250Enabled = true habilita la anotación @RolesAllowed .
prePostEnabled = true habilita @PreAuthorize , @PostAuthorize , @PreFilter , @PostFilter .

Después de habilitarlos, podemos aplicar políticas de acceso basadas en roles en nuestros puntos finales de API como esta:

 @Api(tags = "UserAdmin") @RestController @RequestMapping(path = "api/admin/user") @RolesAllowed(Role.USER_ADMIN) public class UserAdminApi { // Details omitted for brevity } @Api(tags = "Author") @RestController @RequestMapping(path = "api/author") public class AuthorApi { // Details omitted for brevity @RolesAllowed(Role.AUTHOR_ADMIN) @PostMapping public void create() { } @RolesAllowed(Role.AUTHOR_ADMIN) @PutMapping("{id}") public void edit() { } @RolesAllowed(Role.AUTHOR_ADMIN) @DeleteMapping("{id}") public void delete() { } @GetMapping("{id}") public void get() { } @GetMapping("{id}/book") public void getBooks() { } @PostMapping("search") public void search() { } } @Api(tags = "Book") @RestController @RequestMapping(path = "api/book") public class BookApi { // Details omitted for brevity @RolesAllowed(Role.BOOK_ADMIN) @PostMapping public BookView create() { } @RolesAllowed(Role.BOOK_ADMIN) @PutMapping("{id}") public void edit() { } @RolesAllowed(Role.BOOK_ADMIN) @DeleteMapping("{id}") public void delete() { } @GetMapping("{id}") public void get() { } @GetMapping("{id}/author") public void getAuthors() { } @PostMapping("search") public void search() { } }

Tenga en cuenta que las anotaciones de seguridad se pueden proporcionar tanto a nivel de clase como a nivel de método.

Los ejemplos demostrados son simples y no representan escenarios del mundo real, pero Spring Security proporciona un amplio conjunto de anotaciones y puede manejar un esquema de autorización complejo si elige usarlos.

Prefijo predeterminado del nombre del rol

En esta subsección separada, quiero enfatizar un detalle más sutil que confunde a muchos usuarios nuevos.

El marco Spring Security diferencia dos términos:

  • Authority representa un permiso individual.
  • Role representa un grupo de permisos.

Ambos pueden representarse con una sola interfaz llamada GrantedAuthority y luego verificarse con Spring Expression Language dentro de las anotaciones de Spring Security de la siguiente manera:

  • Authority : @PreAuthorize(“tieneAutoridad('EDITAR_LIBRO')”)
  • Role : @PreAuthorize(“hasRole('BOOK_ADMIN')”)

Para que la diferencia entre estos dos términos sea más explícita, el marco Spring Security agrega un prefijo ROLE_ al nombre del rol de forma predeterminada. Entonces, en lugar de buscar un rol llamado BOOK_ADMIN , buscará ROLE_BOOK_ADMIN .

Personalmente, encuentro este comportamiento confuso y prefiero deshabilitarlo en mis aplicaciones. Se puede deshabilitar dentro de la configuración de Spring Security de la siguiente manera:

 @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { // Details omitted for brevity @Bean GrantedAuthorityDefaults grantedAuthorityDefaults() { return new GrantedAuthorityDefaults(""); // Remove the ROLE_ prefix } }

Probando con Spring Security

Para probar nuestros puntos finales con pruebas unitarias o de integración cuando usamos el marco Spring Security, debemos agregar la dependencia spring-security-test junto con spring-boot-starter-test . Nuestro archivo de compilación pom.xml se verá así:

 <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> <exclusions> <exclusion> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-test</artifactId> <scope>test</scope> </dependency>

Esta dependencia nos da acceso a algunas anotaciones que se pueden usar para agregar contexto de seguridad a nuestras funciones de prueba.

Estas anotaciones son:

  • @WithMockUser se puede agregar a un método de prueba para emular la ejecución con un usuario simulado.
  • @WithUserDetails se puede agregar a un método de prueba para emular la ejecución con UserDetails devueltos por UserDetailsService .
  • @WithAnonymousUser se puede agregar a un método de prueba para emular la ejecución con un usuario anónimo. Esto es útil cuando un usuario desea ejecutar la mayoría de las pruebas como un usuario específico y anular algunos métodos para que sean anónimos.
  • @WithSecurityContext determina qué SecurityContext usar y las tres anotaciones descritas anteriormente se basan en él. Si tenemos un caso de uso específico, podemos crear nuestra propia anotación que usa @WithSecurityContext para crear cualquier SecurityContext que queramos. Su discusión está fuera del alcance de nuestro artículo, y consulte la documentación de Spring Security para obtener más detalles.

La forma más fácil de ejecutar las pruebas con un usuario específico es usar la anotación @WithMockUser . Podemos crear un usuario simulado con él y ejecutar la prueba de la siguiente manera:

 @Test @WithMockUser(username="[email protected]", roles={"USER_ADMIN"}) public void test() { // Details omitted for brevity }

Sin embargo, este enfoque tiene un par de inconvenientes. Primero, el usuario simulado no existe, y si ejecuta la prueba de integración, que luego consulta la información del usuario de la base de datos, la prueba fallará. En segundo lugar, el usuario simulado es la instancia de la clase org.springframework.security.core.userdetails.User , que es la implementación interna de Spring Framework de la interfaz UserDetails , y si tenemos nuestra propia implementación, esto puede causar conflictos más adelante, durante ejecución de pruebas.

Si los inconvenientes anteriores son bloqueadores para nuestra aplicación, entonces la anotación @WithUserDetails es el camino a seguir. Se usa cuando tenemos implementaciones personalizadas UserDetails y UserDetailsService . Se supone que el usuario existe, por lo que debemos crear la fila real en la base de datos o proporcionar la instancia simulada de UserDetailsService antes de ejecutar las pruebas.

Así es como podemos usar esta anotación:

 @Test @WithUserDetails("[email protected]") public void test() { // Details omitted for brevity }

Esta es una anotación preferida en las pruebas de integración de nuestro proyecto de muestra porque tenemos implementaciones personalizadas de las interfaces antes mencionadas.

El uso @WithAnonymousUser permite ejecutar como un usuario anónimo. Esto es especialmente conveniente cuando desea ejecutar la mayoría de las pruebas con un usuario específico, pero algunas pruebas como usuario anónimo. Por ejemplo, lo siguiente ejecutará los casos de prueba test1 y test2 con un usuario simulado y test3 con un usuario anónimo:

 @SpringBootTest @AutoConfigureMockMvc @WithMockUser public class WithUserClassLevelAuthenticationTests { @Test public void test1() { // Details omitted for brevity } @Test public void test2() { // Details omitted for brevity } @Test @WithAnonymousUser public void test3() throws Exception { // Details omitted for brevity } }

Terminando

Al final, me gustaría mencionar que el marco Spring Security probablemente no ganará ningún concurso de belleza y definitivamente tiene una curva de aprendizaje pronunciada. Me he encontrado con muchas situaciones en las que se reemplazó con alguna solución propia debido a la complejidad de su configuración inicial. Pero una vez que los desarrolladores entienden su funcionamiento interno y logran establecer la configuración inicial, se vuelve relativamente fácil de usar.

En este artículo, traté de demostrar todos los detalles sutiles de la configuración, y espero que encuentre útiles los ejemplos. Para obtener ejemplos de código completos, consulte el repositorio de Git de mi proyecto Spring Security de muestra.