web-dev-qa-db-fra.com

Authentification personnalisée Spring Webflux pour API

Je crée une API pour une application Angular 5. Je voudrais utiliser JWT pour l'authentification.
Je souhaite utiliser les fonctionnalités fournies par Spring Security pour pouvoir facilement travailler avec des rôles.

J'ai réussi à désactiver l'authentification de base. Mais lorsque j'utilise http.authorizeExchange().anyExchange().authenticated(); j'obtiens toujours une invite de connexion.
Je voudrais juste donner un 403 au lieu de l'invite. Donc, remplacer l'invite de connexion par une "chose" (est-ce un filtre?) Qui vérifie l'en-tête Authorization pour le jeton.

La connexion que je veux simplement faire dans un contrôleur qui renverra un jeton JWT. Mais quel bean de sécurité de printemps dois-je utiliser pour vérifier les informations d'identification de l'utilisateur? Je peux créer mes propres services et référentiels, mais j'aimerais utiliser autant que possible les fonctionnalités fournies par Spring Security.

La version courte de cette question est juste:
Comment puis-je personnaliser l'authentification de Spring Security?
Quels haricots dois-je créer?
Où dois-je mettre la configuration? (J'ai maintenant un bean de SecurityWebFilterChain)

La seule documentation que j'ai pu trouver sur l'authentification dans webflux avec sécurité Spring est la suivante: https://docs.spring.io/spring-security/site/docs/5.0.0.BUILD-SNAPSHOT/reference/htmlsingle/ # jc-webflux

17
Jan Wytze

Après beaucoup de recherches et d'essais, je pense avoir trouvé la solution:

Vous avez besoin d'un bean de SecurityWebFilterChain qui contient toute la configuration.
C'est à moi:

@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();
    }
}

J'ai désactivé httpBasic, formLogin, csrf et logout pour pouvoir faire mon authentification personnalisée.

En définissant AuthenticationManager et SecurityContextRepository, j'ai remplacé la configuration de sécurité par défaut du printemps pour vérifier si un utilisateur est authentifié/autorisé pour une demande.

Le gestionnaire d'authentification:

@Component
public class AuthenticationManager implements ReactiveAuthenticationManager {

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

Je ne sais pas exactement où le gestionnaire d'authentification est destiné, mais je pense que pour faire l'authentification finale, définissez donc authentication.setAuthenticated(true); quand tout va bien.

SecurityContextRepository:

@Component
public class SecurityContextRepository implements ServerSecurityContextRepository {

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

    @Override
    public Mono<SecurityContext> 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);
    }
}

Dans la charge, je vérifierai sur la base des en-têtes dans le serverWebExchange si l'utilisateur est authentifié. J'utilise https://github.com/jwtk/jjwt . Je renvoie un autre type de jeton d'authentification si l'utilisateur est authentifié ou non.

21
Jan Wytze

Dans mon ancien projet, j'ai utilisé cette configuration:

@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;
    }
}

JWTAuthorizationFilter est:

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 ) || ( failed.getCause() instanceof Exception ) )
        {
            customErrorCode = AuthenticationErrorEnum.ERRORE_GENERICO;
        }
        builder.append("Errore duranre l'autorizzazione. ");
        builder.append(failed.getMessage());
        JwtAuthApiError apiError = new JwtAuthApiError(HttpStatus.UNAUTHORIZED, failed.getMessage(), Arrays.asList(builder.toString()), customErrorCode);
        String errore = ( new ObjectMapper() ).writeValueAsString(apiError);
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        response.sendError(HttpStatus.UNAUTHORIZED.value(), errore);
        request.setAttribute(IRsConstants.API_ERROR_REQUEST_ATTR_NAME, apiError);
    }

Et JWTAuthenticationFilter est

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;
    }
}

Le détail de l'utilisateur est un détail de service utilisateur classique

@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());
    }
}

Veuillez noter que je n'ai pas utilisé Spring Webflux

J'espère que c'est utile

Angelo

5
Angelo Immediata

Merci Jan, vous m'avez beaucoup aidé avec votre exemple pour personnaliser l'authentification dans mon application Spring Webflux et sécuriser l'accès aux API.
Dans mon cas, j'ai juste besoin de lire un en-tête pour définir les rôles utilisateur et je veux que la sécurité Spring vérifie les autorisations utilisateur pour sécuriser l'accès à mes méthodes.
Vous avez donné la clé avec une http.securityContextRepository(this.securityContextRepository); personnalisée dans SecurityConfiguration (pas besoin d'un gestionnaire d'authentification personnalisé).

Grâce à ce SecurityContextRepository, j'ai pu créer et définir une authentification personnalisée (simplifiée ci-dessous).

@Override
public Mono<SecurityContext> load(ServerWebExchange serverWebExchange) {
    String role = serverWebExchange.getRequest().getHeaders().getFirst("my-header");
    Authentication authentication =
       new AnonymousAuthenticationToken("authenticated-user", someUser,  AuthorityUtils.createAuthorityList(role) );

    return Mono.just(new SecurityContextImpl(authentication));
}

Et ainsi je peux sécuriser mes méthodes en utilisant ces rôles:

@Component
public class MyService {
    @PreAuthorize("hasRole('ADMIN')")
    public Mono<String> checkAdmin() {
        // my secure method
   }
}
2
Mica

Pour ceux qui ont le même problème (Webflux + Custom Authentication + JWT) J'ai résolu en utilisant AuthenticationWebFilter, personnalisé ServerAuthenticationConverter et ReactiveAuthenticationManager, en suivant le code, j'espère que cela pourrait aider quelqu'un à l'avenir. Testé avec la dernière version (spring-boot 2.1.1.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();
    }

    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<String> 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<Authentication> 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<Authentication> authenticate(Authentication authentication) {
    if (authentication.isAuthenticated()) {
        return Mono.just(authentication);
    }
    return super.authenticate(authentication);
    }
}

PS: La classe TokenProvider que vous trouverez sur https://github.com/jhipster/jhipster-registry/blob/master/src/main/Java/io/github/jhipster/registry/security/jwt/TokenProvider. Java

2
user2669657