· Jose Antonio López  · 6 min lectura

Spring Security amb WebSecurityConfig - Autenticació i Autorització amb Rols

Seguretat robusta per a aplicacions Spring Boot amb WebSecurityConfig i SecurityFilterChain. Guia completa amb autenticació, autorització, integració JWT i exemples de codi per a protegir APIs.

Seguretat robusta per a aplicacions Spring Boot amb WebSecurityConfig i SecurityFilterChain. Guia completa amb autenticació, autorització, integració JWT i exemples de codi per a protegir APIs.

Objectiu

Aquest contingut forma part de l’article autenticació a Spring Security amb JWT.

Aquesta part crec que és tan important, que necessita que la implementació a Spring Security 6.4.2 s’expliqui en un post a part.

Dependències

Les dependències amb les que es treballarà en el post:

DependènciaDescripció
spring-boot-starter-securitySuport de seguretat per a l’aplicació. Més a la documentació oficial.
spring-boot-starter-oauth2-authorization-serverProporciona un servidor d’autorització OAuth2 per a gestionar tokens d’accés. Més a la documentació oficial.

ApiConfig

Primer, és bo definir la classe ApiConfig. La classe conté tota aquella informació relacionada amb l’arrel de les rutes de l’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");
    }
}

Versionat de l’API

En aquest cas, l’API es versiona amb /v1. Si es desitja canviar la versió, es pot fer en aquesta classe. Aquest punt passa molt desapercebut en programadors que no estan acostumats a treballar amb APIs.

Les APIs es versionen perquè els clients puguin seguir fent servir la versió anterior mentre s’actualitza a la nova versió. Pot ser tan fàcil com tenir dues versions de l’API en producció. Arribat el moment, es pot desactivar la versió anterior.

Si no es fa aquest punt en etapes primeres del desenvolupament, pot ser un mal de cap en el futur.

M’agrada deixar com a privades les variables COMMON_PATH i API_VERSION perquè no es puguin modificar des de fora de la classe. Si per qualsevol motiu es necessita exposar-les com a públiques, es pot modificar el modificador d’accés a public.

Rols

A l’aplicació, es defineixen dos rols: END_USER i 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 és una interfície que representa una autoritat. L’autoritat es concedeix a un usuari i té permisos específics com READ, WRITE, DELETE, etc.

Per simplificar la implementació aconsello fer servir GrantedAuthority amb rols.

A la implementació, es fa servir ROLE_ com a prefix per als rols. És una convenció de Spring Security que pot ser personalitzada i opino que fer-ho és complicar-se.

Tal com està ara el valor de l’enum es serialitza com BACK_OFFICE_ADMIN i END_USER. Dins de l’aplicació es farà servir tal com Spring Security necessita. A la base de dades i als bodys de les peticions i respostes es veurà BACK_OFFICE_ADMIN i END_USER.

SecurityConfig

SecurityConfig.java
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);
    }

}

Anotacions

AnotacióDescripció
@ConfigurationIndica que la classe és una classe de configuració.
@EnableWebSecurity(debug = true)Habilita la seguretat web a l’aplicació. La forma més fàcil d’implementar la seguretat és exposant un @Bean que retorni un objecte del tipus SecurityFilterChain.
@EnableMethodSecurityHabilita la seguretat a nivell de mètodes, permetent l’ús d’anotacions de seguretat als mètodes dels controladors.

SecurityFilterChain

El mètode securityFilterChain és l’encarregat de configurar la seguretat de l’aplicació. El mètode rep una instància de HttpSecurity que s’encarrega de configurar la seguretat de l’aplicació.

FormLogin

El formulari de login per defecte de Spring Security és una pàgina web que es mostra a l’usuari perquè introdueixi les seves credencials. Es deshabilita el formulari de login per defecte de Spring Security. En ser una API, no es necessita un formulari de login.

authorizeHttpRequests

  • La ruta /hey-fincas-api/v1/auth/login sigui accessible sense autenticació i només mitjançant el mètode POST.
  • S’accepten totes les rutes que comencin per /hey-fincas-api/v1 i que estiguin autenticades.
  • Totes les altres rutes estan denegades.

La configuració es pot personalitzar segons l’aplicació però és un bon punt de partida. Assegura sobretot peticions fora de l’arrel siguin denegades i ningú pugui accedir a elles per a veure què hi ha darrere.

logout

  • La ruta /hey-fincas-api/v1/auth/logout és accessible només mitjançant el mètode POST.
  • En fer logout, s’elimina la cookie d’autenticació.

Hi ha aplicacions que implementen el logout en un controlador. Implementar-lo al controlador no és una mala pràctica, però és més segur fer-ho a la configuració de Spring Security.

D’aquesta manera, s’assegura que la petició no passi per tots els filtres de l’aplicació i s’evita arribar al servlet.

addFilterBefore

S’afegeix el filtre jwtFilter abans del filtre LogoutFilter. El filtre jwtFilter s’encarrega de validar el token JWT i autenticar l’usuari. Per a determinar on afegir el filtre, es pot fer servir el mètode addFilterBefore o addFilterAfter.

Per a més detalls sobre on afegir el filtre personalitzat, visita la documentació oficial de Spring Security .

csrf

  • Es deshabilita la protecció CSRF.
  • S’estableix la política de creació de sessions com STATELESS.
  • Es configura el servidor de recursos OAuth2 amb la configuració per defecte.

CSRF és un atac que es produeix quan un atacant enganya un usuari perquè realitzi una acció no desitjada en una aplicació en la qual l’usuari està autenticat. En deshabilitar CSRF, s’evita aquest tipus d’atac. Més informació sobre CSRF.

La política de creació de sessions STATELESS indica que no es crearà una sessió per a l’usuari. Cada petició es considera independent i no s’emmagatzema informació de l’usuari a la sessió. Aquesta és una de les parts que assegura que l’API sigui RESTful.

exceptionHandling

  • S’estableix el codi d’estat de la resposta a 401 Unauthorized quan l’usuari no està autenticat.
  • Si necessites més personalització cal crear un AuthenticationEntryPoint personalitzat.

authenticationManager

AuthenticationManager és el component principal de Spring Security que gestiona l’autenticació. Verifica les credencials de l’usuari i l’autentica. La implementació més comuna és DaoAuthenticationProvider i pots veure més informació a la documentació oficial.

methodSecurityExpressionHandler

MethodSecurityExpressionHandler és una interfície per a gestionar les expressions de seguretat als mètodes. Permet definir regles de seguretat a nivell de mètode fent servir anotacions com @PreAuthorize i @PostAuthorize.

Recomano fer servir DefaultMethodSecurityExpressionHandler que és la implementació per defecte i poc més.

roleHierarchy

RoleHierarchy permet definir una jerarquia de rols a la teva aplicació. És útil quan tens rols que hereten permisos d’altres rols.

Per exemple, tens un rol ADMIN que té els seus permisos més els d’un SALES_MANAGER. Spring Security farà servir aquesta jerarquia.

Tornar a l’article principal

Pots tornar a l’autenticació a Spring Security amb JWT.

  • Spring Security
  • Spring Boot
Compartir: