· Jose Antonio López · 6 min lectura
Spring Security con WebSecurityConfig - Autenticación y Autorización con Roles
Seguridad robusta para aplicaciones Spring Boot con WebSecurityConfig y SecurityFilterChain. Guía completa con autenticación, autorización, integración JWT y ejemplos de código para proteger APIs.

Objetivo
Este contenido forma parte del artículo autenticación en Spring Security con JWT.
Esta parte creo que es tan importante, que necesita que la implementación en Spring Security 6.4.2 se explique en un post aparte.
Dependencias
Las dependencias con las que se va trabajar en el post:
ApiConfig
Primero, es bueno definir la clase ApiConfig. La clase contiene toda aquella información relacionada con la raíz de las rutas de la API.
package com.hey.fincas.common.infrastructure.config;
public class ApiConfig {
private static final String COMMON_PATH = "/hey-fincas-api";
private static final String API_VERSION = "/v1";
public static final String API_BASE_PATH = COMMON_PATH + API_VERSION;
private ApiConfig() {
throw new UnsupportedOperationException("This class should never be instantiated");
}
}Versionado de la API
En este caso, la API se versiona con /v1. Si se desea cambiar la versión, se puede hacer en esta clase. Este punto pasa muy desapercibido en programadores que no están acostumbrados a trabajar con APIs.
Las APIs se versionan para que los clientes puedan seguir usando la versión anterior mientras se actualiza a la nueva versión. Puede ser tan fácil como tener dos versiones de la API en producción. Llegado el momento, se puede desactivar la versión anterior.
Si no se hace este punto en etapas primeras del desarrollo, puede ser un dolor de cabeza en el futuro.
Me gusta dejar como privadas las variables COMMON_PATH y API_VERSION para que no se puedan modificar desde fuera de la clase. Si por cualquier motivo se necesita exponerlas cómo públicas, se puede modificar el modificador de acceso a public.
Roles
En la aplicación, se definen dos roles: END_USER y BACK_OFFICE_ADMIN.
package com.hey.fincas.auth.domain;
import org.springframework.security.core.GrantedAuthority;
public enum Role implements GrantedAuthority {
BACK_OFFICE_ADMIN,
SALES_MANAGER,
END_USER;
@Override
public String getAuthority() {
return "ROLE_" + name();
}
}GrantedAuthority
GrantedAuthority es una interfaz que representa una autoridad. La autoridad se concede a un usuario y tiene permisos específicos como READ, WRITE, DELETE, etc.
Por simplificar la implementación aconsejo usar GrantedAuthority con roles.
En la implementación, se usa ROLE_ como prefijo para los roles. Es una convención de Spring Security que puede ser personalizada y opino que hacerlo es complicarse.
Tal y como está ahora el valor del enum se serializa como BACK_OFFICE_ADMIN y END_USER. Dentro de la aplicación se usará tal y como Spring Security necesita. En la base de datos y en los bodys de las peticiones y respuestas se verá BACK_OFFICE_ADMIN y END_USER.
SecurityConfig
package com.hey.fincas.auth.infrastructure.config;
@Configuration
@EnableWebSecurity(debug = true)
@EnableMethodSecurity
public class WebSecurityConfig {
private final AuthService authService;
private final UserDetailsService userDetailsService;
private final PasswordEncoder passwordEncoder;
public WebSecurityConfig(AuthService authService, UserDetailsService userDetailsService, PasswordEncoder passwordEncoder) {
this.authService = authService;
this.userDetailsService = userDetailsService;
this.passwordEncoder = passwordEncoder;
}
public final static String LOGIN_URL_MATCHER = ApiConfig.API_BASE_PATH + "/auth/login";
public final static String LOG_OUT_URL_MATCHER = ApiConfig.API_BASE_PATH + "/auth/logout";
final String BASE_URL_MATCHER = ApiConfig.API_BASE_PATH + "/**";
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
final Filter jwtFilter = jwtAuthenticationFilter();
http
.formLogin(AbstractHttpConfigurer::disable)
.authorizeHttpRequests((requests) -> requests
.requestMatchers(HttpMethod.POST, LOGIN_URL_MATCHER).permitAll()
.requestMatchers(BASE_URL_MATCHER).authenticated()
.anyRequest().denyAll()
)
.logout(logout -> {
logout
.logoutRequestMatcher(new AntPathRequestMatcher(LOG_OUT_URL_MATCHER, HttpMethod.POST.name()))
.logoutSuccessHandler((request, response, authentication) -> {
response.setStatus(HttpStatus.NO_CONTENT.value());
final Cookie cookie = new Cookie(AuthCookieConstants.TOKEN_COOKIE_NAME, null);
cookie.setMaxAge(0);
response.addCookie(cookie);
})
;
})
.addFilterBefore(jwtFilter, LogoutFilter.class)
.csrf((csrf) -> {
try {
csrf.disable()
.sessionManagement((sessionManagement) -> sessionManagement
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
).oauth2ResourceServer((oauth2) -> oauth2.jwt(Customizer.withDefaults()));
} catch (Exception e) {
throw new AuthenticationException("Spring Security Config Issue",e) {
};
}
}
)
.authenticationManager(authenticationManager())
.exceptionHandling(handler -> handler
.authenticationEntryPoint((request, response, authException) -> {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
})
)
;
return http.build();
}
@Bean
public AuthenticationManager authenticationManager() {
DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
authenticationProvider.setUserDetailsService(userDetailsService);
authenticationProvider.setPasswordEncoder(passwordEncoder);
ProviderManager providerManager = new ProviderManager(authenticationProvider);
providerManager.setEraseCredentialsAfterAuthentication(true);
return providerManager;
}
@Bean
public MethodSecurityExpressionHandler methodSecurityExpressionHandler() {
DefaultMethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler();
expressionHandler.setRoleHierarchy(roleHierarchy());
return expressionHandler;
}
@Bean
public RoleHierarchy roleHierarchy() {
return RoleHierarchyImpl.withDefaultRolePrefix()
.role(Role.BACK_OFFICE_ADMIN.name())
.implies(Role.SALES_MANAGER.name())
.implies(Role.END_USER.name())
.build();
}
private JwtAuthenticationFilter jwtAuthenticationFilter() {
return new JwtAuthenticationFilter(authService, userDetailsService);
}
}Anotaciones
SecurityFilterChain
El método securityFilterChain es el encargado de configurar la seguridad de la aplicación. El método recibe una instancia de HttpSecurity que se encarga de configurar la seguridad de la aplicación.
FormLogin
El formulario de login por defecto de Spring Security es una página web que se muestra al usuario para que introduzca sus credenciales. Se deshabilita el formulario de login por defecto de Spring Security. Al ser una API, no se necesita un formulario de login.
authorizeHttpRequests
- La ruta
/hey-fincas-api/v1/auth/loginsea accesible sin autenticación y sólo mediante el método POST. - Se aceptan todas las rutas que empiecen por
/hey-fincas-api/v1y que estén autenticadas. - Todas las demás rutas están denegadas.
La configuración se puede personalizar según la aplicación pero es un buen punto de partida. Asegura sobretodo peticiones fuera de la raíz sean denegadas y nadie pueda acceder a ellas para ver que hay detrás.
logout
- La ruta
/hey-fincas-api/v1/auth/logoutes accesible sólo mediante el método POST. - Al hacer logout, se elimina la cookie de autenticación.
Hay aplicaciones que implementan el logout en un controlador. Implementarlo en el controlador no es una mala práctica, pero es más seguro hacerlo en la configuración de Spring Security.
De esta forma, se asegura que la petición no pase por todos los filtros de la aplicación y se evita llegar al servlet.
addFilterBefore
Se añade el filtro jwtFilter antes del filtro LogoutFilter. El filtro jwtFilter se encarga de validar el token JWT y autenticar al usuario. Para determinar dónde añadir el filtro, se puede usar el método addFilterBefore o addFilterAfter.
Para más detalles sobre dónde añadir el filtro personalizado, visita la documentación oficial de Spring Security .
csrf
- Se deshabilita la protección CSRF.
- Se establece la política de creación de sesiones como
STATELESS. - Se configura el servidor de recursos OAuth2 con la configuración por defecto.
CSRF es un ataque que se produce cuando un atacante engaña a un usuario para que realice una acción no deseada en una aplicación en la que el usuario está autenticado. Al deshabilitar CSRF, se evita este tipo de ataque. Más información sobre CSRF.
La política de creación de sesiones STATELESS indica que no se creará una sesión para el usuario. Cada petición se considera independiente y no se almacena información del usuario en la sesión. Esta es una de las partes que asegura que la API sea RESTful.
exceptionHandling
- Se establece el código de estado de la respuesta a
401 Unauthorizedcuando el usuario no está autenticado. - Si nececistas más personalización hay que crear un
AuthenticationEntryPointpersonalizado.
authenticationManager
AuthenticationManager es el componente principal de Spring Security que gestiona la autenticación. Verifica las credenciales del usuario y lo autentica. La implementación más común es DaoAuthenticationProvider y puedes ver más información en la documentación oficial.
methodSecurityExpressionHandler
MethodSecurityExpressionHandler es una interfaz para manejar las expresiones de seguridad en los métodos. Permite definir reglas de seguridad a nivel de método usando anotaciones como @PreAuthorize y @PostAuthorize.
Recomiendo usar DefaultMethodSecurityExpressionHandler que es la implementación por defecto y poco más.
roleHierarchy
RoleHierarchy permite definir una jerarquía de roles en tu aplicación. Es útil cuando tienes roles que heredan permisos de otros roles.
Por ejemplo, tienes un rol ADMIN que tiene sus permisos más los de un SALES_MANAGER. Spring Security usará esa jerarquía.
Volver al artículo principal
Puedes volver a la autenticación en Spring Security con JWT.
- Spring Security
- Spring Boot