Saltar al contenido

Autenticación personalizada de Spring webflux para API

Este grupo de redactores ha estado horas buscando para darle respuesta a tus búsquedas, te dejamos la respuesta así que nuestro deseo es resultarte de mucha apoyo.

Solución:

Después de mucho buscar y probar, creo que he encontrado la solución:

Necesitas un frijol de SecurityWebFilterChain que contiene toda la configuración.
Esto es mío:

@Configuration
public class SecurityConfiguration 

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private SecurityContextRepository securityContextRepository;

    @Bean
    public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) 
        // Disable default security.
        http.httpBasic().disable();
        http.formLogin().disable();
        http.csrf().disable();
        http.logout().disable();

        // Add custom security.
        http.authenticationManager(this.authenticationManager);
        http.securityContextRepository(this.securityContextRepository);

        // Disable authentication for `/auth/**` routes.
        http.authorizeExchange().pathMatchers("/auth/**").permitAll();
        http.authorizeExchange().anyExchange().authenticated();

        return http.build();
    

He desactivado httpBasic, formLogin, csrf y logout para poder realizar mi autenticación personalizada.

Al establecer el AuthenticationManager y SecurityContextRepository Anulé la configuración de seguridad de primavera predeterminada para verificar si un usuario está autenticado / autorizado para una solicitud.

El administrador de autenticación:

@Component
public class AuthenticationManager implements ReactiveAuthenticationManager 

    @Override
    public Mono authenticate(Authentication authentication) 
        // JwtAuthenticationToken is my custom token.
        if (authentication instanceof JwtAuthenticationToken) 
            authentication.setAuthenticated(true);
        
        return Mono.just(authentication);
    

No estoy completamente seguro de dónde está el administrador de autenticación, pero creo que para hacer la autenticación final, así que establezco authentication.setAuthenticated(true); cuando todo va bien.

SecurityContextRepository:

@Component
public class SecurityContextRepository implements ServerSecurityContextRepository 

    @Override
    public Mono save(ServerWebExchange serverWebExchange, SecurityContext securityContext) 
        // Don't know yet where this is for.
        return null;
    

    @Override
    public Mono load(ServerWebExchange serverWebExchange) 
        // JwtAuthenticationToken and GuestAuthenticationToken are custom Authentication tokens.
        Authentication authentication = (/* check if authenticated based on headers in serverWebExchange */) ? 
            new JwtAuthenticationToken(...) :
            new GuestAuthenticationToken();
        return new SecurityContextImpl(authentication);
    

En la carga, lo verificaré en función de los encabezados en el serverWebExchange si el usuario está autenticado. Yo uso https://github.com/jwtk/jjwt. Devuelvo un tipo diferente de token de autenticación si el usuario está autenticado o no.

Para aquellos que tienen el mismo problema (Webflux + Custom Authentication + JWT) Lo resolví usando AuthenticationWebFilter, personalizado ServerAuthenticationConverter y ReactiveAuthenticationManager, siguiendo el código, espero que pueda ayudar a alguien en el futuro. Probado con la última versión (spring-boot 2.2.4.RELEASE).

@EnableWebFluxSecurity
@EnableReactiveMethodSecurity
public class SpringSecurityConfiguration 
    @Bean
    public SecurityWebFilterChain configure(ServerHttpSecurity http) 
    return http
        .csrf()
            .disable()
            .headers()
            .frameOptions().disable()
            .cache().disable()
        .and()
            .authorizeExchange()
            .pathMatchers(AUTH_WHITELIST).permitAll()
            .anyExchange().authenticated()
        .and()
            .addFilterAt(authenticationWebFilter(), SecurityWebFiltersOrder.AUTHENTICATION)
            .httpBasic().disable()
            .formLogin().disable()
            .logout().disable()
            .build();
    

@Autowired private lateinit var userDetailsService: ReactiveUserDetailsService

class CustomReactiveAuthenticationManager(userDetailsService: ReactiveUserDetailsService?) : UserDetailsRepositoryReactiveAuthenticationManager(userDetailsService) 

    override fun authenticate(authentication: Authentication): Mono 
        return if (authentication.isAuthenticated) 
            Mono.just(authentication)
         else super.authenticate(authentication)
    


private fun responseError() : ServerAuthenticationFailureHandler
    return ServerAuthenticationFailureHandler webFilterExchange: WebFilterExchange, _: AuthenticationException ->
        webFilterExchange.exchange.response.statusCode = HttpStatus.UNAUTHORIZED
        webFilterExchange.exchange.response.headers.addIfAbsent(HttpHeaders.LOCATION,"/")
        webFilterExchange.exchange.response.setComplete();
    


    private AuthenticationWebFilter authenticationWebFilter() 
        AuthenticationWebFilter authenticationWebFilter = new AuthenticationWebFilter(reactiveAuthenticationManager());
        authenticationWebFilter.setServerAuthenticationConverter(new JwtAuthenticationConverter(tokenProvider));
        NegatedServerWebExchangeMatcher negateWhiteList = new NegatedServerWebExchangeMatcher(ServerWebExchangeMatchers.pathMatchers(AUTH_WHITELIST));
        authenticationWebFilter.setRequiresAuthenticationMatcher(negateWhiteList);
        authenticationWebFilter.setSecurityContextRepository(new WebSessionServerSecurityContextRepository());
        authenticationWebFilter.setAuthenticationFailureHandler(responseError());
        return authenticationWebFilter;
    



public class JwtAuthenticationConverter implements ServerAuthenticationConverter 
    private final TokenProvider tokenProvider;

    public JwtAuthenticationConverter(TokenProvider tokenProvider) 
    this.tokenProvider = tokenProvider;
    

    private Mono resolveToken(ServerWebExchange exchange) 
    log.debug("servletPath: ", exchange.getRequest().getPath());
    return Mono.justOrEmpty(exchange.getRequest().getHeaders().getFirst(HttpHeaders.AUTHORIZATION))
            .filter(t -> t.startsWith("Bearer "))
            .map(t -> t.substring(7));
    

    @Override
    public Mono convert(ServerWebExchange exchange) 
    return resolveToken(exchange)
            .filter(tokenProvider::validateToken)
            .map(tokenProvider::getAuthentication);
    




public class CustomReactiveAuthenticationManager extends UserDetailsRepositoryReactiveAuthenticationManager 
    public CustomReactiveAuthenticationManager(ReactiveUserDetailsService userDetailsService) 
    super(userDetailsService);
    

    @Override
    public Mono authenticate(Authentication authentication) 
    if (authentication.isAuthenticated()) 
        return Mono.just(authentication);
    
    return super.authenticate(authentication);
    

PD: la clase TokenProvider que encuentra en https://github.com/jhipster/jhipster-registry/blob/master/src/main/java/io/github/jhipster/registry/security/jwt/TokenProvider.java

En mi antiguo proyecto utilicé esta configuración:

@Configuration
@EnableWebSecurity
@Import(WebMvcConfig.class)
@PropertySource(value =  "classpath:config.properties" , encoding = "UTF-8", ignoreResourceNotFound = false)
public class WebSecWebSecurityCfg extends WebSecurityConfigurerAdapter

    private UserDetailsService userDetailsService;
    @Autowired
    @Qualifier("objectMapper")
    private ObjectMapper mapper;
    @Autowired
    @Qualifier("passwordEncoder")
    private PasswordEncoder passwordEncoder;
    @Autowired
    private Environment env;

    public WebSecWebSecurityCfg(UserDetailsService userDetailsService)
    
        this.userDetailsService = userDetailsService;
    



    @Override
    protected void configure(HttpSecurity http) throws Exception
                                                                 
        JWTAuthorizationFilter authFilter = new JWTAuthorizationFilter
                                                                    (   authenticationManager(),//Auth mgr  
                                                                        env.getProperty("config.secret.symmetric.key"), //Chiave simmetrica
                                                                        env.getProperty("config.jwt.header.string"), //nome header
                                                                        env.getProperty("config.jwt.token.prefix") //Prefisso token
                                                                    );
        JWTAuthenticationFilter authenticationFilter = new JWTAuthenticationFilter
                                                                    (
                                                                        authenticationManager(), //Authentication Manager
                                                                        env.getProperty("config.secret.symmetric.key"), //Chiave simmetrica
                                                                        Long.valueOf(env.getProperty("config.jwt.token.duration")),//Durata del token in millisecondi
                                                                        env.getProperty("config.jwt.header.string"), //nome header
                                                                        env.getProperty("config.jwt.token.prefix"), //Prefisso token
                                                                        mapper
                                                                    );
        http        
        .cors()
        .and()
        .csrf()
        .disable()
        .authorizeRequests()
        .anyRequest()
        .authenticated()
        .and()
        .addFilter(authenticationFilter)
        .addFilter(authFilter)
        // Disabilitiamo la creazione di sessione in spring
        .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
    

    @Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception
    
        auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder);
    

    @Bean
    CorsConfigurationSource corsConfigurationSource()
    
        final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", new CorsConfiguration().applyPermitDefaultValues());
        return source;
    

Dónde JWTAuthorizationFilter es:

public class JWTAuthorizationFilter extends BasicAuthenticationFilter
{
    private static final Logger logger = LoggerFactory.getLogger(JWTAuthenticationFilter.class.getName());
    private String secretKey;
    private String headerString;
    private String tokenPrefix; 

    public JWTAuthorizationFilter(AuthenticationManager authenticationManager, AuthenticationEntryPoint authenticationEntryPoint, String secretKey, String headerString, String tokenPrefix)
    
        super(authenticationManager, authenticationEntryPoint);
        this.secretKey = secretKey;
        this.headerString = headerString;
        this.tokenPrefix = tokenPrefix;
    
    public JWTAuthorizationFilter(AuthenticationManager authenticationManager, String secretKey, String headerString, String tokenPrefix)
    
        super(authenticationManager);
        this.secretKey = secretKey;
        this.headerString = headerString;
        this.tokenPrefix = tokenPrefix;
    
    @Override
    protected void onUnsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException
    
        AuthenticationErrorEnum customErrorCode = null;
        StringBuilder builder = new StringBuilder();
        if( failed.getCause() instanceof MissingJwtTokenException )
        
            customErrorCode = AuthenticationErrorEnum.TOKEN_JWT_MANCANTE;
        
        else if( failed.getCause() instanceof ExpiredJwtException )
        
            customErrorCode = AuthenticationErrorEnum.TOKEN_JWT_SCADUTO;
        
        else if( failed.getCause() instanceof MalformedJwtException )
        
            customErrorCode = AuthenticationErrorEnum.TOKEN_JWT_NON_CORRETTO;
        
        else if( failed.getCause() instanceof MissingUserSubjectException )
        
            customErrorCode = AuthenticationErrorEnum.TOKEN_JWT_NESSUN_UTENTE_TROVATO;
        
        else if( ( failed.getCause() instanceof GenericJwtAuthorizationException ) 

Y JWTAuthenticationFilter es

public class JWTAuthenticationFilter extends UsernamePasswordAuthenticationFilter

    private AuthenticationManager authenticationManager;
    private String secretKey;
    private long tokenDurationMillis;
    private String headerString;
    private String tokenPrefix;
    private ObjectMapper mapper;

    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException
    
        AuthenticationErrorEnum customErrorCode = null;
        StringBuilder builder = new StringBuilder();
        if( failed instanceof BadCredentialsException )
        
            customErrorCode = AuthenticationErrorEnum.CREDENZIALI_SERVIZIO_ERRATE;
        

        else
        
            //Teoricamente nella fase di autenticazione all'errore generico non dovrebbe mai arrivare
            customErrorCode = AuthenticationErrorEnum.ERRORE_GENERICO;
               
        builder.append("Errore durante l'autenticazione del servizio. ");
        builder.append(failed.getMessage());
        JwtAuthApiError apiError = new JwtAuthApiError(HttpStatus.UNAUTHORIZED, failed.getMessage(), Arrays.asList(builder.toString()), customErrorCode);
        String errore = mapper.writeValueAsString(apiError);
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        response.sendError(HttpStatus.UNAUTHORIZED.value(), errore);
        request.setAttribute(IRsConstants.API_ERROR_REQUEST_ATTR_NAME, apiError);
    

    public JWTAuthenticationFilter(AuthenticationManager authenticationManager, String secretKey, long tokenDurationMillis, String headerString, String tokenPrefix, ObjectMapper mapper)
    
        super();
        this.authenticationManager = authenticationManager;
        this.secretKey = secretKey;
        this.tokenDurationMillis = tokenDurationMillis;
        this.headerString = headerString;
        this.tokenPrefix = tokenPrefix;
        this.mapper = mapper;
    

    @Override
    public Authentication attemptAuthentication(HttpServletRequest req, HttpServletResponse res) throws AuthenticationException
    
        try
        
            ServiceLoginDto creds = new ObjectMapper().readValue(req.getInputStream(), ServiceLoginDto.class);

            return authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(creds.getCodiceServizio(), creds.getPasswordServizio(), new ArrayList<>()));
        
        catch (IOException e)
        
            throw new RuntimeException(e);
        
    

    @Override
    protected void successfulAuthentication(HttpServletRequest req, HttpServletResponse res, FilterChain chain, Authentication auth) throws IOException, ServletException
    
        DateTime dt = new DateTime();
        Date expirationTime = dt.plus(getTokenDurationMillis()).toDate();
        String token = Jwts
                        .builder()
                        .setSubject(((User) auth.getPrincipal()).getUsername())
                        .setExpiration(expirationTime)
                        .signWith(SignatureAlgorithm.HS512, getSecretKey().getBytes())
                        .compact();
        res.addHeader(getHeaderString(), getTokenPrefix() + token);
        res.addHeader("jwtExpirationDate", expirationTime.toString());
        res.addHeader("jwtTokenDuration", String.valueOf(TimeUnit.MILLISECONDS.toMinutes(getTokenDurationMillis()))+" minuti");
    
    public String getSecretKey()
    
        return secretKey;
    

    public void setSecretKey(String secretKey)
    
        this.secretKey = secretKey;
    

    public long getTokenDurationMillis()
    
        return tokenDurationMillis;
    

    public void setTokenDurationMillis(long tokenDurationMillis)
    
        this.tokenDurationMillis = tokenDurationMillis;
    

    public String getHeaderString()
    
        return headerString;
    

    public void setHeaderString(String headerString)
    
        this.headerString = headerString;
    

    public String getTokenPrefix()
    
        return tokenPrefix;
    

    public void setTokenPrefix(String tokenPrefix)
    
        this.tokenPrefix = tokenPrefix;
    

El detalle del usuario es un detalle clásico del servicio al usuario.

@Service
public class UserDetailsServiceImpl implements UserDetailsService

    @Autowired
    private IServizioService service;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException
    
        Service svc;
        try
        
            svc = service.findBySvcCode(username);
        
        catch (DbException e)
        
            throw new UsernameNotFoundException("Errore durante il processo di autenticazione; "+e.getMessage(), e);
        
        if (svc == null)
        
            throw new UsernameNotFoundException("Nessun servizio trovato per il codice servizio "+username);
        
        else if( !svc.getAbilitato().booleanValue() )
        
            throw new UsernameNotFoundException("Servizio "+username+" non abilitato");
        
        return new User(svc.getCodiceServizio(), svc.getPasswordServizio(), Collections.emptyList());
    

Tenga en cuenta que no utilicé Spring webflux

Espero que sea de utilidad

Angelo

Te mostramos las reseñas y valoraciones de los usuarios

Si te ha sido de utilidad nuestro artículo, sería de mucha ayuda si lo compartieras con el resto seniors de esta manera nos ayudas a extender este contenido.

¡Haz clic para puntuar esta entrada!
(Votos: 0 Promedio: 0)



Utiliza Nuestro Buscador

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *