web-dev-qa-db-fra.com

Partage de ressources d'origine croisée avec Spring Security

J'essaie de faire en sorte que CORS joue bien avec Spring Security mais ce n'est pas conforme. J'ai apporté les modifications décrites dans cet article et la modification de cette ligne dans applicationContext-security.xml A obtenu POST et les demandes GET fonctionnent pour mon application (expose temporairement les méthodes du contrôleur) , donc je peux tester CORS):

  • Avant: <intercept-url pattern="/**" access="isAuthenticated()" />
  • Après: <intercept-url pattern="/**" access="permitAll" />

Malheureusement, l'URL suivante qui autorise les connexions Spring Security via AJAX ne répond pas: http://localhost:8080/mutopia-server/resources/j_spring_security_check. Je fais la demande AJAX de http://localhost:80 À http://localhost:8080.

Dans Chrome

Lorsque j'essaie d'accéder à j_spring_security_check, J'obtiens (pending) Dans Chrome pour la demande de contrôle en amont OPTIONS et AJAX l'appel renvoie avec l'état HTTP) code 0 et message "erreur".

Dans Firefox

Le contrôle en amont réussit avec le code d'état HTTP 302 et j'obtiens toujours le rappel d'erreur pour ma AJAX directement après avec l'état HTTP 0 et le message "erreur".

enter image description here

enter image description here

Code de demande AJAX

function get(url, json) {
    var args = {
        type: 'GET',
        url: url,
        // async: false,
        // crossDomain: true,
        xhrFields: {
            withCredentials: false
        },
        success: function(response) {
            console.debug(url, response);
        },
        error: function(xhr) {
            console.error(url, xhr.status, xhr.statusText);
        }
    };
    if (json) {
        args.contentType = 'application/json'
    }
    $.ajax(args);
}

function post(url, json, data, dataEncode) {
    var args = {
        type: 'POST',
        url: url,
        // async: false,
        crossDomain: true,
        xhrFields: {
            withCredentials: false
        },
        beforeSend: function(xhr){
            // This is always added by default
            // Ignoring this prevents preflight - but expects browser to follow 302 location change
            xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
            xhr.setRequestHeader("X-Ajax-call", "true");
        },
        success: function(data, textStatus, xhr) {
            // var location = xhr.getResponseHeader('Location');
            console.error('success', url, xhr.getAllResponseHeaders());
        },
        error: function(xhr) {
            console.error(url, xhr.status, xhr.statusText);
            console.error('fail', url, xhr.getAllResponseHeaders());
        }
    }
    if (json) {
        args.contentType = 'application/json'
    }
    if (typeof data != 'undefined') {
        // Send JSON raw in the body
        args.data = dataEncode ? JSON.stringify(data) : data;
    }
    console.debug('args', args);
    $.ajax(args);
}

var loginJSON = {"j_username": "username", "j_password": "password"};

// Fails
post('http://localhost:8080/mutopia-server/resources/j_spring_security_check', false, loginJSON, false);

// Works
post('http://localhost/mutopia-server/resources/j_spring_security_check', false, loginJSON, false);

// Works
get('http://localhost:8080/mutopia-server/landuses?projectId=6', true);

// Works
post('http://localhost:8080/mutopia-server/params', true, {
    "name": "testing",
    "local": false,
    "generated": false,
    "project": 6
}, true);

Veuillez noter - je peux POST vers toute autre URL de mon application via CORS à l'exception de la connexion Spring Security. J'ai parcouru beaucoup d'articles, donc tout aperçu de ce problème étrange serait grandement apprécié

26
Aram Kocharyan

J'ai pu le faire en étendant UsernamePasswordAuthenticationFilter ... mon code est dans Groovy, j'espère que c'est OK:

public class CorsAwareAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
    static final String Origin = 'Origin'

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response){
        if (request.getHeader(Origin)) {
            String Origin = request.getHeader(Origin)
            response.addHeader('Access-Control-Allow-Origin', Origin)
            response.addHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE')
            response.addHeader('Access-Control-Allow-Credentials', 'true')
            response.addHeader('Access-Control-Allow-Headers',
                    request.getHeader('Access-Control-Request-Headers'))
        }
        if (request.method == 'OPTIONS') {
            response.writer.print('OK')
            response.writer.flush()
            return
        }
        return super.attemptAuthentication(request, response)
    }
}

Les bits importants ci-dessus:

  • Ajoutez uniquement des en-têtes CORS à la réponse si une demande CORS est détectée
  • Répondez à la demande OPTIONS avant le vol avec une simple réponse 200 non vide, qui contient également les en-têtes CORS.

Vous devez déclarer ce bean dans votre configuration Spring. Il existe de nombreux articles montrant comment procéder, je ne les copierai pas ici.

Dans ma propre implémentation, j'utilise une liste blanche de domaine Origin car je n'autorise CORS que pour les développeurs internes. Ce qui précède est une version simplifiée de ce que je fais, il faudra peut-être l'ajuster, mais cela devrait vous donner une idée générale.

20
Keeth

bien Ceci est mon code qui fonctionne très bien et parfait pour moi: j'ai passé deux jours à travailler dessus et à comprendre la sécurité du printemps donc j'espère que vous l'acceptez comme réponse, lol

 public class CorsFilter extends OncePerRequestFilter  {
    static final String Origin = "Origin";

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        System.out.println(request.getHeader(Origin));
        System.out.println(request.getMethod());
        if (request.getHeader(Origin).equals("null")) {
            String Origin = request.getHeader(Origin);
            response.setHeader("Access-Control-Allow-Origin", "*");//* or Origin as u prefer
            response.setHeader("Access-Control-Allow-Credentials", "true");
           response.setHeader("Access-Control-Allow-Headers",
                    request.getHeader("Access-Control-Request-Headers"));
        }
        if (request.getMethod().equals("OPTIONS")) {
            try {
                response.getWriter().print("OK");
                response.getWriter().flush();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }else{
        filterChain.doFilter(request, response);
        }
    }
}

eh bien, vous devez également définir votre filtre à invoquer:

<security:http use-expressions="true" .... >
     ...
     //your other configs
    <security:custom-filter ref="corsHandler" after="PRE_AUTH_FILTER"/> // this goes to your filter
</security:http>

Eh bien et vous avez besoin d'un bean pour le filtre personnalisé que vous avez créé:

<bean id="corsHandler" class="mobilebackbone.mesoft.config.CorsFilter" />
16
azerafati

Depuis Spring Security 4.1, c'est la bonne façon de faire en sorte que Spring Security prenne en charge CORS (également nécessaire dans Spring Boot 1.4/1.5):

@Configuration
public class WebConfig extends WebMvcConfigurerAdapter {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedMethods("HEAD", "GET", "PUT", "POST", "DELETE", "PATCH");
    }
}

et:

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
//        http.csrf().disable();
        http.cors();
    }

    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        final CorsConfiguration configuration = new CorsConfiguration();
        configuration.setAllowedOrigins(ImmutableList.of("*"));
        configuration.setAllowedMethods(ImmutableList.of("HEAD",
                "GET", "POST", "PUT", "DELETE", "PATCH"));
        // setAllowCredentials(true) is important, otherwise:
        // The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when the request's credentials mode is 'include'.
        configuration.setAllowCredentials(true);
        // setAllowedHeaders is important! Without it, OPTIONS preflight request
        // will fail with 403 Invalid CORS request
        configuration.setAllowedHeaders(ImmutableList.of("Authorization", "Cache-Control", "Content-Type"));
        final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);
        return source;
    }
}

Ne pas faites l'une des choses ci-dessous, qui sont la mauvaise façon de tenter de résoudre le problème:

  • http.authorizeRequests().antMatchers(HttpMethod.OPTIONS, "/**").permitAll();
  • web.ignoring().antMatchers(HttpMethod.OPTIONS);

Référence: http://docs.spring.io/spring-security/site/docs/4.2.x/reference/html/cors.html

10
Hendy Irawan

Généralement, la demande OPTIONS ne contient pas de cookie pour l'authentification de la sécurité Spring.
Pour résoudre ce problème, vous pouvez modifier la configuration de la sécurité du ressort pour autoriser [~ # ~] options [~ # ~] demande sans authentification.
Je recherche beaucoup et j'obtiens deux solutions:
1.Utilisation de Java avec la configuration de sécurité Spring,

@Override
protected void configure(HttpSecurity http) throws Exception
{
    http
    .csrf().disable()
    .authorizeRequests()
    .antMatchers(HttpMethod.OPTIONS,"/path/to/allow").permitAll()//allow CORS option calls
    .antMatchers("/resources/**").permitAll()
    .anyRequest().authenticated()
    .and()
    .formLogin()
    .and()
    .httpBasic();
}

2.En utilisant XML ( note. ne peut pas écrire "POST, GET"):

<http auto-config="true">
    <intercept-url pattern="/client/edit" access="isAuthenticated" method="GET" />
    <intercept-url pattern="/client/edit" access="hasRole('EDITOR')" method="POST" />
    <intercept-url pattern="/client/edit" access="hasRole('EDITOR')" method="GET" />
</http>

Au final, il y a la source de la solution ... :)

5
nolan4954

Pour moi, le problème était que la vérification de contrôle en amont OPTIONS a échoué l'authentification, car les informations d'identification n'ont pas été transmises lors de cet appel.

Cela fonctionne pour moi:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.security.SecurityProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.data.web.config.EnableSpringDataWebSupport;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.authentication.www.BasicAuthenticationEntryPoint;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import Java.io.IOException;

@Configuration
@EnableAsync
@EnableScheduling
@EnableSpringDataWebSupport
@Order(SecurityProperties.ACCESS_OVERRIDE_ORDER)
class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserDetailsService userDetailsService;

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

        http.csrf().disable()
                .httpBasic().and()
                .authorizeRequests()
                .anyRequest().authenticated()
                .and().anonymous().disable()
                .exceptionHandling().authenticationEntryPoint(new BasicAuthenticationEntryPoint() {
            @Override
            public void commence(final HttpServletRequest request, final HttpServletResponse response, final AuthenticationException authException) throws IOException, ServletException {
                if(HttpMethod.OPTIONS.matches(request.getMethod())){
                    response.setStatus(HttpServletResponse.SC_OK);
                    response.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_Origin, request.getHeader(HttpHeaders.Origin));
                    response.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS, request.getHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS));
                    response.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS, request.getHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD));
                    response.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS, "true");
                }else{
                    response.sendError(HttpServletResponse.SC_UNAUTHORIZED, authException.getMessage());
                }
            }
        });

    }

    @Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth
                .userDetailsService(userDetailsService)
                .passwordEncoder(new BCryptPasswordEncoder());
    }
}

La partie pertinente étant:

.exceptionHandling().authenticationEntryPoint(new BasicAuthenticationEntryPoint() {
            @Override
            public void commence(final HttpServletRequest request, final HttpServletResponse response, final AuthenticationException authException) throws IOException, ServletException {
                if(HttpMethod.OPTIONS.matches(request.getMethod())){
                    response.setStatus(HttpServletResponse.SC_OK);
                    response.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_Origin, request.getHeader(HttpHeaders.Origin));
                    response.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS, request.getHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS));
                    response.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS, request.getHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD));
                    response.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS, "true");
                }else{
                    response.sendError(HttpServletResponse.SC_UNAUTHORIZED, authException.getMessage());
                }
            }
        });

Cela corrige le problème de contrôle en amont OPTIONS. Ce qui se passe ici, c'est lorsque vous recevez un appel et que l'authentification échoue, vous vérifiez s'il s'agit d'un appel OPTIONS et s'il l'est, laissez-le passer et laissez-le faire tout ce qu'il veut. Cela désactive essentiellement toutes les vérifications de contrôle en amont côté navigateur, mais la stratégie de domaine croisé normale s'applique toujours.

Lorsque vous utilisez la dernière version de Spring, vous pouvez utiliser le code ci-dessous pour autoriser les demandes Cross Origin à l'échelle mondiale (pour tous vos contrôleurs):

import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;

@Component
public class WebMvcConfigurer extends WebMvcConfigurerAdapter {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**").allowedOrigins("http://localhost:3000");
    }
}

Notez que c'est rarement une bonne idée de simplement coder en dur comme ça. Dans quelques entreprises pour lesquelles j'ai travaillé, les origines autorisées étaient configurables via un portail d'administration, donc sur les environnements de développement, vous pourriez ajouter toutes les origines dont vous avez besoin.

3
Anthony De Smet

Dans mon cas, response.getWriter (). Flush () ne fonctionnait pas

Changé le code comme ci-dessous et il a commencé à fonctionner

public void doFilter(ServletRequest request, ServletResponse res, FilterChain chain)
        throws IOException, ServletException {

    LOGGER.info("Start API::CORSFilter");
    HttpServletRequest oRequest = (HttpServletRequest) request;
    HttpServletResponse response = (HttpServletResponse) res;
    response.setHeader("Access-Control-Allow-Origin", "*");
    response.setHeader("Access-Control-Allow-Methods", "POST,PUT, GET, OPTIONS, DELETE");
    response.setHeader("Access-Control-Max-Age", "3600");
    response.setHeader("Access-Control-Allow-Headers",
            " Origin, X-Requested-With, Content-Type, Accept,AUTH-TOKEN");
    if (oRequest.getMethod().equals("OPTIONS")) {
        response.flushBuffer();
    } else {
        chain.doFilter(request, response);
    }
}
3
Ajinath

Étant donné que la partie principale de la question concerne la demande POST CORS non autorisée au point de connexion, je vous indique immédiatement l'étape 2 .

Mais en ce qui concerne le nombre de réponses, c'est la question la plus pertinente pour la demande Spring Security CORS. Je décrirai donc une solution plus élégante pour configurer CORS avec Spring Security. Car sauf cas rares il n'est pas nécessaire de créer des filtres/intercepteurs /… pour mettre quoi que ce soit en réponse. Nous le ferons de manière déclarative d'ici le printemps. Depuis Spring Framework 4.2, nous avons des éléments CORS tels que filtre, processeur, etc. prêts à l'emploi. Et quelques liens pour lire 12 .

Allons-y:

1. préparer la source de configuration CORS.

Cela peut se faire de différentes manières:

  • en tant que configuration globale Spring MVC CORS (dans les classes de configuration comme WebMvcConfigurerAdapter)

    ...
        @Override
        public void addCorsMappings(CorsRegistry registry) {
            registry.addMapping("/**")
                .allowedOrigins("*")
                ...
        }
    
  • en tant que bean corsConfigurationSource séparé

    @Bean
    CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration config = new CorsConfiguration();
        config.applyPermitDefaultValues();
    
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", config);
    }
    
  • en tant que classe externe (qui peut être utilisée via le constructeur ou câblée en tant que composant)

    // @Component // <- for autowiring
    class CorsConfig extends UrlBasedCorsConfigurationSource {
    
        CorsConfig() {
            orsConfiguration config = new CorsConfiguration();
            config.applyPermitDefaultValues(); // <- frequantly used values
    
            this.registerCorsConfiguration("/**", config);
        }
    }
    

2. activer le support CORS avec la configuration définie

Nous activerons la prise en charge de CORS dans les classes Spring Security comme WebSecurityConfigurerAdapter. Assurez-vous que corsConfigurationSource est accessible pour ce support. Sinon, fournissez-le via @Resource câblage automatique ou défini explicitement (voir dans l'exemple). Nous autorisons également l'accès non autorisé à certains points de terminaison comme la connexion:

    ...
    // @Resource // <- for autowired solution
    // CorseConfigurationSource corsConfig;

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

        // or autowiring
        // http.cors().configurationSource(corsConfig);

        // or direct set
        // http.cors().configurationSource(new CorsConfig());

        http.authorizeRequests()
                .antMatchers("/login").permitAll() // without this line login point will be unaccessible for authorized access
                .antMatchers("/*").hasAnyAuthority(Authority.all()); // <- all other security stuff
    }

3. personnaliser la configuration de CORS

Si la configuration de base fonctionne, nous pouvons personnaliser les mappages, les origines, etc. Ajoutez même plusieurs configurations pour différents mappages. Par exemple, je déclare explicitement tous les paramètres CORS et laisse UrlPathHelper ne pas couper mon chemin de servlet:

class RestCorsConfig extends UrlBasedCorsConfigurationSource {

    RestCorsConfig() {
        this.setCorsConfigurations(Collections.singletonMap("/**", corsConfig()));
        this.setAlwaysUseFullPath(true);
    }

    private static CorsConfiguration corsConfig() {
        CorsConfiguration config = new CorsConfiguration();
        config.addAllowedHeader("*");
        config.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE"));
        config.setAllowCredentials(true);
        config.addAllowedOrigin("*");
        config.setMaxAge(3600L);
        return config;
    }
}

4. dépannage

Pour déboguer mon problème, je traçais org.springframework.web.filter.CorsFilter#doFilterInternal méthode. Et j'ai vu que la recherche CorsConfiguration renvoie null car la configuration globale de Spring MVC CORS n'était pas vue par Spring Security. J'ai donc utilisé une solution avec une utilisation directe de la classe externe:

http.cors().configurationSource(corsConfig);

Je suis totalement d'accord avec la réponse donnée par Bludream, mais j'ai quelques remarques:

Je voudrais étendre la clause if dans le filtre CORS avec une vérification NULL sur l'en-tête Origin:

public class CorsFilter extends OncePerRequestFilter {

    private static final String Origin = "Origin";


    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
        throws ServletException, IOException {

        if (request.getHeader(Origin) == null || request.getHeader(Origin).equals("null")) {
            response.addHeader("Access-Control-Allow-Origin", "*");
            response.setHeader("Access-Control-Allow-Credentials", "true");
            response.addHeader("Access-Control-Max-Age", "10");

            String reqHead = request.getHeader("Access-Control-Request-Headers");

            if (!StringUtils.isEmpty(reqHead)) {
                response.addHeader("Access-Control-Allow-Headers", reqHead);
            }
        }
        if (request.getMethod().equals("OPTIONS")) {
            try {
                response.getWriter().print("OK");
                response.getWriter().flush();
            } catch (IOException e) {
            e.printStackTrace();
            }
        } else{
            filterChain.doFilter(request, response);
        }
    }
 }

De plus, j'ai remarqué le comportement indésirable suivant: Si j'essaie d'accéder à une API REST avec un rôle non autorisé, la sécurité Spring me renvoie un état HTTP 403: INTERDIT et les en-têtes CORS sont renvoyés. Cependant, si j'utilise un jeton inconnu ou un jeton qui n'est plus valide, un état HTTP 401: NON AUTORISÉ est renvoyé SANS en-têtes CORS.

J'ai réussi à le faire fonctionner en changeant la configuration du filtre dans le XML de sécurité comme ceci:

<security:http use-expressions="true" .... >
    ...
    //your other configs
    <sec:custom-filter ref="corsFilter" before="HEADERS_FILTER"/>
</security:http>

Et le bean suivant pour notre filtre personnalisé:

<bean id="corsFilter" class="<<location of the CORS filter class>>" />
2
Mathias G.