web-dev-qa-db-fra.com

Sécurisation REST à l'aide de jetons personnalisés (sans état, pas d'interface utilisateur, pas de cookies, pas d'authentification de base, pas OAuth, pas de page de connexion)

Il existe de nombreuses directives, des exemples de codes qui montrent comment sécuriser REST avec Spring Security, mais la plupart d'entre eux supposent un client Web et parlent de la page de connexion, de la redirection, de l'utilisation de cookies, etc. être même un simple filtre qui vérifie le jeton personnalisé dans l'en-tête HTTP peut être suffisant. Comment puis-je implémenter la sécurité pour les exigences ci-dessous? Existe-t-il un projet Gist/github faisant de même? Mes connaissances en sécurité de printemps sont limitées, donc s'il y a un moyen plus simple de mettre en œuvre cela avec la sécurité du printemps, veuillez me le faire savoir.

  • API REST servie par un backend sans état sur HTTPS
  • le client peut être une application Web, une application mobile, n'importe quelle application de style SPA, des API tierces
  • pas d'authentification de base, pas de cookies, pas d'interface utilisateur (pas de ressources JSP/HTML/statiques), pas de redirections, pas de fournisseur OAuth.
  • jeton personnalisé défini sur les en-têtes HTTPS
  • La validation du jeton effectuée par rapport au magasin externe (comme MemCached/Redis/ou même n'importe quel SGBDR)
  • Toutes les API doivent être authentifiées à l'exception des chemins sélectionnés (comme/login,/signup,/public, etc.)

J'utilise Springboot, la sécurité du printemps, etc. préfère une solution avec Java config (no XML)

37
Karthik Karuppannan

Mon exemple d'application fait exactement cela - sécuriser REST points de terminaison à l'aide de Spring Security dans un scénario sans état. Individuel REST sont authentifiés à l'aide d'un En-tête HTTP. Les informations d'authentification sont stockées côté serveur dans un cache en mémoire et fournissent la même sémantique que celles proposées par la session HTTP dans une application Web classique. L'application utilise l'infrastructure Spring Security complète avec un code personnalisé très minimal. Non filtres nus, pas de code en dehors de l'infrastructure Spring Security.

L'idée de base est d'implémenter les quatre composants de sécurité Spring suivants:

  1. org.springframework.security.web.AuthenticationEntryPoint pour intercepter REST appels nécessitant une authentification mais manquant le jeton d'authentification requis et ainsi refuser les demandes.
  2. org.springframework.security.core.Authentication pour conserver les informations d'authentification requises pour l'API REST.
  3. org.springframework.security.authentication.AuthenticationProvider pour effectuer l'authentification réelle (contre une base de données, un serveur LDAP, un service Web, etc.).
  4. org.springframework.security.web.context.SecurityContextRepository pour contenir le jeton d'authentification entre les requêtes HTTP. Dans l'exemple, l'implémentation enregistre le jeton dans une instance EHCACHE.

L'exemple utilise la configuration XML mais vous pouvez facilement trouver l'équivalent Java config.

33
manish

Vous avez raison, ce n'est pas facile et il n'y a pas beaucoup de bons exemples. Les exemples que j'ai vus l'ont fait pour que vous ne puissiez pas utiliser d'autres éléments de sécurité à ressort côte à côte. J'ai fait quelque chose de similaire récemment, voici ce que j'ai fait.

Vous avez besoin d'un jeton personnalisé pour conserver votre valeur d'en-tête

public class CustomToken extends AbstractAuthenticationToken {
  private final String value;

  //Getters and Constructor.  Make sure getAutheticated returns false at first.
  //I made mine "immutable" via:

      @Override
public void setAuthenticated(boolean isAuthenticated) {
    //It doesn't make sense to let just anyone set this token to authenticated, so we block it
    //Similar precautions are taken in other spring framework tokens, EG: UsernamePasswordAuthenticationToken
    if (isAuthenticated) {

        throw new IllegalArgumentException(MESSAGE_CANNOT_SET_AUTHENTICATED);
    }

    super.setAuthenticated(false);
}
}

Vous avez besoin d'un filtre de sécurité à ressort pour extraire l'en-tête et demander au gestionnaire de l'authentifier, quelque chose comme ça texte souligné

public class CustomFilter extends AbstractAuthenticationProcessingFilter {


    public CustomFilter(RequestMatcher requestMatcher) {
        super(requestMatcher);

        this.setAuthenticationSuccessHandler((request, response, authentication) -> {
        /*
         * On success the desired action is to chain through the remaining filters.
         * Chaining is not possible through the success handlers, because the chain is not accessible in this method.
         * As such, this success handler implementation does nothing, and chaining is accomplished by overriding the successfulAuthentication method as per:
         * http://docs.spring.io/autorepo/docs/spring-security/3.2.4.RELEASE/apidocs/org/springframework/security/web/authentication/AbstractAuthenticationProcessingFilter.html#successfulAuthentication(javax.servlet.http.HttpServletRequest,%20javax.servlet.http.HttpServletResponse,%20javax.servlet.FilterChain,%20org.springframework.security.core.Authentication)
         * "Subclasses can override this method to continue the FilterChain after successful authentication."
         */
        });

    }



    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
            throws AuthenticationException, IOException, ServletException {


        String tokenValue = request.getHeader("SOMEHEADER");

        if(StringUtils.isEmpty(tokenValue)) {
            //Doing this check is kinda dumb because we check for it up above in doFilter
            //..but this is a public method and we can't do much if we don't have the header
            //also we can't do the check only here because we don't have the chain available
           return null;
        }


        CustomToken token = new CustomToken(tokenValue);
        token.setDetails(authenticationDetailsSource.buildDetails(request));

        return this.getAuthenticationManager().authenticate(token);
    }



    /*
     * Overriding this method to maintain the chaining on authentication success.
     * http://docs.spring.io/autorepo/docs/spring-security/3.2.4.RELEASE/apidocs/org/springframework/security/web/authentication/AbstractAuthenticationProcessingFilter.html#successfulAuthentication(javax.servlet.http.HttpServletRequest,%20javax.servlet.http.HttpServletResponse,%20javax.servlet.FilterChain,%20org.springframework.security.core.Authentication)
     * "Subclasses can override this method to continue the FilterChain after successful authentication."
     */
    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {


        //if this isn't called, then no auth is set in the security context holder
        //and subsequent security filters can still execute.  
        //so in SOME cases you might want to conditionally call this
        super.successfulAuthentication(request, response, chain, authResult);

        //Continue the chain
        chain.doFilter(request, response);

    }


}

Enregistrez votre filtre personnalisé dans la chaîne de sécurité à ressort

 @Configuration
 public static class ResourceEndpointsSecurityConfig extends WebSecurityConfigurerAdapter {        

      //Note, we don't register this as a bean as we don't want it to be added to the main Filter chain, just the spring security filter chain
      protected AbstractAuthenticationProcessingFilter createCustomFilter() throws Exception {
        CustomFilter filter = new CustomFilter( new RegexRequestMatcher("^/.*", null));
        filter.setAuthenticationManager(this.authenticationManagerBean());
        return filter;
      }

       @Override
       protected void configure(HttpSecurity http) throws Exception {                  

            http
            //fyi: This adds it to the spring security proxy filter chain
            .addFilterBefore(createCustomFilter(), AnonymousAuthenticationFilter.class)
       }
}

Un fournisseur d'authentification personnalisé pour valider ce jeton extrait avec le filtre.

public class CustomAuthenticationProvider implements AuthenticationProvider {


    @Override
    public Authentication authenticate(Authentication auth)
            throws AuthenticationException {

        CustomToken token = (CustomToken)auth;

        try{
           //Authenticate token against redis or whatever you want

            //This i found weird, you need a Principal in your Token...I use User
            //I found this to be very redundant in spring security, but Controller param resolving will break if you don't do this...anoying
            org.springframework.security.core.userdetails.User principal = new User(...); 

            //Our token resolved to a username so i went with this token...you could make your CustomToken take the principal.  getCredentials returns "NO_PASSWORD"..it gets cleared out anyways.  also the getAuthenticated for the thing you return should return true now
            return new UsernamePasswordAuthenticationToken(principal, auth.getCredentials(), principal.getAuthorities());
        } catch(Expection e){
            //TODO throw appropriate AuthenticationException types
            throw new BadCredentialsException(MESSAGE_AUTHENTICATION_FAILURE, e);
        }


    }

    @Override
    public boolean supports(Class<?> authentication) {
        return CustomToken.class.isAssignableFrom(authentication);
    }


}

Enfin, enregistrez votre fournisseur en tant que bean pour que le gestionnaire d'authentification le trouve dans une classe @Configuration. Vous pourriez probablement aussi @Component, je préfère cette méthode

@Bean
public AuthenticationProvider createCustomAuthenticationProvider(injectedDependencies)  {
    return new CustomAuthenticationProvider(injectedDependencies);
}
9
Chris DaMour

Le code sécurise tous les points de terminaison - mais je suis sûr que vous pouvez jouer avec ça :). Le jeton est stocké dans Redis à l'aide de Spring Boot Starter Security et vous devez définir notre propre UserDetailsService que vous passez dans AuthenticationManagerBuilder.

Longue histoire - copiez-collez EmbeddedRedisConfiguration et SecurityConfig et remplacez AuthenticationManagerBuilder dans votre logique.

HTTP:

Jeton de demande - envoi du contenu d'authentification HTTP de base dans un en-tête de demande. Un jeton est rendu dans un en-tête de réponse.

http --print=hH -a user:password localhost:8080/v1/users

GET /v1/users HTTP/1.1
Accept: */*
Accept-Encoding: gzip, deflate
Authorization: Basic dXNlcjpwYXNzd29yZA==
Connection: keep-alive
Host: localhost:8080
User-Agent: HTTPie/0.9.3

HTTP/1.1 200 OK
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Content-Length: 4
Content-Type: text/plain;charset=UTF-8
Date: Fri, 06 May 2016 09:44:23 GMT
Expires: 0
Pragma: no-cache
Server: Apache-Coyote/1.1
X-Application-Context: application
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
X-XSS-Protection: 1; mode=block
x-auth-token: cacf4a97-75fe-464d-b499-fcfacb31c8af

Même demande mais en utilisant un jeton:

http --print=hH localhost:8080/v1/users 'x-auth-token: cacf4a97-75fe-464d-b499-fcfacb31c8af'

GET /v1/users HTTP/1.1
Accept: */*
Accept-Encoding: gzip, deflate
Connection: keep-alive
Host: localhost:8080
User-Agent: HTTPie/0.9.3
x-auth-token:  cacf4a97-75fe-464d-b499-fcfacb31c8af

HTTP/1.1 200 OK
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Content-Length: 4
Content-Type: text/plain;charset=UTF-8
Date: Fri, 06 May 2016 09:44:58 GMT
Expires: 0
Pragma: no-cache
Server: Apache-Coyote/1.1
X-Application-Context: application
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
X-XSS-Protection: 1; mode=block

Si vous transmettez un nom d'utilisateur/mot de passe ou un jeton incorrect, vous obtenez 401.

Java

J'ai ajouté ces dépendances dans build.gradle

compile("org.springframework.session:spring-session-data-redis:1.0.1.RELEASE")
compile("org.springframework.boot:spring-boot-starter-security")
compile("org.springframework.boot:spring-boot-starter-web")
compile("com.github.kstyrc:embedded-redis:0.6")

Puis Redis configration

@Configuration
@EnableRedisHttpSession
public class EmbeddedRedisConfiguration {

    private static RedisServer redisServer;

    @Bean
    public JedisConnectionFactory connectionFactory() throws IOException {
        redisServer = new RedisServer(Protocol.DEFAULT_PORT);
        redisServer.start();
        return new JedisConnectionFactory();
    }

    @PreDestroy
    public void destroy() {
        redisServer.stop();
    }

}

Configuration de sécurité:

@Configuration
@EnableWebSecurity
@Order(SecurityProperties.ACCESS_OVERRIDE_ORDER)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    UserService userService;

    @Override
    protected void configure(AuthenticationManagerBuilder builder) throws Exception {
        builder.userDetailsService(userService);
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .requestCache()
                .requestCache(new NullRequestCache())
                .and()
                .httpBasic();
    }

    @Bean
    public HttpSessionStrategy httpSessionStrategy() {
        return new HeaderHttpSessionStrategy();
    }
}

Habituellement, dans les didacticiels, vous trouvez AuthenticationManagerBuilder en utilisant inMemoryAuthentication mais il y a beaucoup plus de choix (LDAP, ...) Jetez simplement un œil à la définition de classe. J'utilise userDetailsService qui nécessite un objet UserDetailsService.

Et enfin mon service utilisateur en utilisant CrudRepository.

@Service
public class UserService implements UserDetailsService {

    @Autowired
    UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        UserAccount userAccount = userRepository.findByEmail(username);
        if (userAccount == null) {
            return null;
        }
        return new User(username, userAccount.getPassword(), AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_USER"));
    }
}
4
radeklos

Un autre exemple de projet qui utilise JWT - Jhipster

Essayez de générer une application de microservice à l'aide de JHipster. Il génère un modèle avec une intégration prête à l'emploi entre Spring Security et JWT.

https://jhipster.github.io/security/

0
Dhananjay