66 votes

Sécurisation de l'API Spring Boot avec la clé et le secret de l'API

J'aimerais sécuriser l'API de Spring Boot afin qu'elle ne soit accessible qu'aux clients qui possèdent une clé et un secret d'API valides. Cependant, il n'y a pas d'authentification (connexion standard avec nom d'utilisateur et mot de passe) dans le programme car toutes les données sont anonymes. Tout ce que j'essaie d'obtenir, c'est que toutes les requêtes de l'API ne puissent être utilisées que pour un front-end tiers spécifique.

J'ai trouvé beaucoup d'articles sur la façon de sécuriser l'API de Spring Boot avec l'authentification des utilisateurs. Mais je n'ai pas besoin d'une authentification utilisateur. Je pense simplement fournir à mon client la clé et le secret de l'API pour qu'il ait accès aux points de terminaison.

Pouvez-vous me suggérer comment y parvenir ? Je vous remercie.

1 votes

La seule différence est que vous l'appelez API key au lieu de username ou y a-t-il autre chose ?

0 votes

Est-ce que la réponse ci-dessous a résolu votre problème ? Comment avez-vous géré vos utilisateurs et la clé api pour chaque utilisateur ?

82voto

Steve Hall Points 1006

Créez un filtre qui récupère le ou les en-têtes que vous utilisez pour l'authentification.

import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter;

public class APIKeyAuthFilter extends AbstractPreAuthenticatedProcessingFilter {

    private String principalRequestHeader;

    public APIKeyAuthFilter(String principalRequestHeader) {
        this.principalRequestHeader = principalRequestHeader;
    }

    @Override
    protected Object getPreAuthenticatedPrincipal(HttpServletRequest request) {
        return request.getHeader(principalRequestHeader);
    }

    @Override
    protected Object getPreAuthenticatedCredentials(HttpServletRequest request) {
        return "N/A";
    }

}

Configurez le filtre dans votre configuration Web Security.

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;

@Configuration
@EnableWebSecurity
@Order(1)
public class APISecurityConfig extends WebSecurityConfigurerAdapter {

    @Value("${yourapp.http.auth-token-header-name}")
    private String principalRequestHeader;

    @Value("${yourapp.http.auth-token}")
    private String principalRequestValue;

    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception {
        APIKeyAuthFilter filter = new APIKeyAuthFilter(principalRequestHeader);
        filter.setAuthenticationManager(new AuthenticationManager() {

            @Override
            public Authentication authenticate(Authentication authentication) throws AuthenticationException {
                String principal = (String) authentication.getPrincipal();
                if (!principalRequestValue.equals(principal))
                {
                    throw new BadCredentialsException("The API key was not found or not the expected value.");
                }
                authentication.setAuthenticated(true);
                return authentication;
            }
        });
        httpSecurity.
            antMatcher("/api/**").
            csrf().disable().
            sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).
            and().addFilter(filter).authorizeRequests().anyRequest().authenticated();
    }

}

0 votes

C'était très utile. J'ai une application qui doit supporter à la fois l'authentification par nom d'utilisateur/mot de passe et par ApiKey. J'ai réussi à faire fonctionner Username/Password et, après avoir lu votre article, j'ai réussi à faire fonctionner ApiKey. Malheureusement, il semble que j'ai cassé le Username/Password. Je soupçonne qu'il s'agit de l'ordre de mes filtres ou de l'utilisation du même AuthenticationManager pour l'authentification par nom d'utilisateur/mot de passe et ApiKey. Un conseil ?

0 votes

@PhillipStack Vous devriez être en mesure de configurer deux WebSecurityConfigurerAdapter avec différents gestionnaires d'authentification ala : stackoverflow.com/questions/33603156/

0 votes

Si j'ai bien compris, l'APIKey n'est pas privé. Toute personne utilisant le client peut ouvrir la console du développeur et vérifier le contenu de l'en-tête. Est-ce exact ?

9voto

matt forsythe Points 1535

Je me rends compte que j'arrive un peu tard sur ce point, mais j'ai également réussi à faire fonctionner les clés API avec Spring Boot en tandem avec l'authentification par nom d'utilisateur/mot de passe. Je n'étais pas très enthousiaste à l'idée d'utiliser AbstractPreAuthenticatedProcessingFilter car en lisant la JavaDoc, cela semblait être une mauvaise utilisation de cette classe particulière.

J'ai fini par créer un nouveau ApiKeyAuthenticationToken ainsi qu'un filtre de servlet brut assez simple pour y parvenir :

import java.util.Collection;

import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.Transient;

@Transient
public class ApiKeyAuthenticationToken extends AbstractAuthenticationToken {

    private String apiKey;

    public ApiKeyAuthenticationToken(String apiKey, Collection<? extends GrantedAuthority> authorities) {
        super(authorities);
        this.apiKey = apiKey;
        setAuthenticated(true);
    }

    @Override
    public Object getCredentials() {
        return null;
    }

    @Override
    public Object getPrincipal() {
        return apiKey;
    }
}

Et le filtre

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.context.SecurityContextHolder;

public class ApiKeyAuthenticationFilter implements Filter {

    static final private String AUTH_METHOD = "api-key";

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException
    {
        if(request instanceof HttpServletRequest && response instanceof HttpServletResponse) {
            String apiKey = getApiKey((HttpServletRequest) request);
            if(apiKey != null) {
                if(apiKey.equals("my-valid-api-key")) {
                    ApiKeyAuthenticationToken apiToken = new ApiKeyAuthenticationToken(apiKey, AuthorityUtils.NO_AUTHORITIES);
                    SecurityContextHolder.getContext().setAuthentication(apiToken);
                } else {
                    HttpServletResponse httpResponse = (HttpServletResponse) response;
                    httpResponse.setStatus(401);
                    httpResponse.getWriter().write("Invalid API Key");
                    return;
                }
            }
        }

        chain.doFilter(request, response);

    }

    private String getApiKey(HttpServletRequest httpRequest) {
        String apiKey = null;

        String authHeader = httpRequest.getHeader("Authorization");
        if(authHeader != null) {
            authHeader = authHeader.trim();
            if(authHeader.toLowerCase().startsWith(AUTH_METHOD + " ")) {
                apiKey = authHeader.substring(AUTH_METHOD.length()).trim();
            }
        }

        return apiKey;
    }
}

Il ne reste plus qu'à injecter le filtre au bon endroit dans la chaîne. Dans mon cas, je voulais que l'authentification par clé API soit évaluée avant toute authentification par nom d'utilisateur ou mot de passe, afin d'authentifier la demande avant que l'application ne tente de rediriger vers une page de connexion :

@Override
protected void configure(HttpSecurity http) throws Exception {
    http
        .csrf()
            .disable()
        .addFilterBefore(new ApiKeyAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
        .authorizeRequests()
            .anyRequest()
                .fullyAuthenticated()
                .and()
        .formLogin();
}

Une autre chose à laquelle vous devez faire attention est que vos requêtes authentifiées par une clé API ne créent pas et n'abandonnent pas un tas d'informations de type HttpSession sur votre serveur.

0 votes

Cela a vraiment fonctionné pour moi. mais, est-il recommandé de l'utiliser en production ?

1 votes

Les clés d'API sont en général moins sûres que, par exemple, OAuth. Mais elles sont plus simples, ce qui fait partie de leur intérêt. Le fait que ce compromis en vaille la peine ou non dépend de vos besoins et de la manière dont votre application est déployée. Mon application particulière était une application interne, n'acceptant pas de connexions du monde extérieur, donc le compromis en valait la peine dans mon cas. Mais je ne déploierais pas, par exemple, une clé d'API dans une application mobile comme seul mécanisme de sécurité, car n'importe quel utilisateur de l'application sera en mesure d'obtenir cette clé d'API.

2voto

Yoda Points 1044

La réponse de @MarkOfHall est correcte et je veux juste ajouter un peu plus de détails. Une fois que vous aurez le code, vous devrez ajouter les valeurs des propriétés à la section application.properties comme ci-dessous :

yourapp.http.auth-token-header-name=X-API-KEY
yourapp.http.auth-token=abc123

La valeur d'authentification dans le Postman est définie comme suit :

enter image description here

Vous pouvez utiliser Postman mais si vous utilisez cURL sera similaire à celle fournie ci-dessous :

$ curl -H "X-API-KEY: abc123" "http://localhost:8080/api/v1/property/1"

Si vous ne fournissez pas la clé et la valeur correctes, l'application ne fonctionnera pas.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X