Compartilhamento de resources de origem cruzada com a segurança do Spring

Eu estou tentando fazer CORS jogar muito bem com o Spring Security, mas não está cumprindo. Fiz as alterações descritas neste artigo e alterei essa linha em applicationContext-security.xml tem solicitações POST e GET funcionando para o meu aplicativo (expõe temporariamente os methods do controlador para que eu possa testar o CORS):

  • Antes:
  • Depois:

Infelizmente, o seguinte URL que permite logins do Spring Security através do AJAX não está respondendo: http://localhost:8080/mutopia-server/resources/j_spring_security_check . Eu estou fazendo o pedido AJAX de http://localhost:80 para http://localhost:8080 .

No Chrome

Ao tentar acessar j_spring_security_check , recebo (pending) no Chrome a solicitação de comprovação OPTIONS e a chamada AJAX retorna com o código de status HTTP 0 e a mensagem “error”.

No Firefox

O preflight é bem-sucedido com o código de status HTTP 302 e ainda recebo o retorno de chamada de erro para minha solicitação AJAX diretamente depois com o status HTTP 0 e a mensagem “error”.

insira a descrição da imagem aqui

insira a descrição da imagem aqui

Código de pedido 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); 

Por favor, note – POST para qualquer outro URL no meu aplicativo via CORS, exceto o login de segurança da spring. Eu passei por muitos artigos, então qualquer insight sobre este estranho assunto seria muito apreciado

Consegui fazer isso estendendo o UsernamePasswordAuthenticationFilter … meu código está no Groovy, espero que esteja tudo bem:

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

Os bits importantes acima:

  • Somente adicione headers CORS à resposta se a solicitação CORS for detectada
  • Responda à solicitação OPTIONS pré-flight com uma resposta 200 não vazia simples, que também contém os headers CORS.

Você precisa declarar esse bean na sua configuração do Spring. Há muitos artigos mostrando como fazer isso, então não vou copiar isso aqui.

Na minha própria implementação, eu uso uma lista de permissions de domínio de origem, já que estou permitindo o CORS apenas para access interno do desenvolvedor. O texto acima é uma versão simplificada do que estou fazendo, portanto, talvez seja necessário fazer ajustes, mas isso deve lhe dar uma ideia geral.

Bem, este é o meu código funcionando muito bem e perfeito para mim: passei dois dias trabalhando nisso e entendendo a segurança da primavera, então espero que você aceite isso como a resposta, 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); } } } 

bem, então você também precisa configurar seu filtro para ser invocado:

  ... //your other configs  // this goes to your filter  

Bem, você precisa de um bean para o filtro personalizado que você criou:

  

Desde o Spring Security 4.1, esta é a maneira correta de fazer com que o Spring Security suporte o CORS (também necessário no 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"); } } 

e:

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

Não faça nada abaixo, que é o caminho errado para tentar resolver o problema:

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

Referência: http://docs.spring.io/spring-security/site/docs/4.2.x/reference/html/cors.html

Principalmente, a solicitação OPTIONS não carrega cookie para a autenticação da segurança da primavera.
Para recuperar isso, pode modificar a configuração de segurança de mola para permitir a solicitação de OPTIONS sem autenticação.
Eu pesquiso muito e obtenho duas soluções:
1.Usando configuração Java com configuração de segurança de primavera,

 @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.Usando XML ( note. Não posso escrever “POST, GET”):

      

No final, há a fonte da solução … 🙂

Para mim, o problema foi que a verificação de pré-verificação OPTIONS falhou na autenticação, porque as credenciais não foram passadas naquela chamada.

Isso funciona para mim:

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

A parte relevante é:

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

Isso corrige o problema de preflight OPTIONS . O que acontece aqui é quando você recebe uma chamada e a autenticação falha, você verifica se é uma chamada OPTIONS e, se estiver, deixe-a passar e deixe-a fazer tudo o que deseja. Isso basicamente desativa todas as verificações de comprovação do lado do navegador, mas a diretiva crossdomain normal ainda se aplica.

Quando você usa a versão mais recente do Spring, pode usar o código abaixo para permitir solicitações de origem cruzada globalmente (para todos os seus controladores):

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

Note que raramente é uma boa idéia simplesmente codificar como este. Em algumas empresas para as quais trabalhei, as origens permitidas eram configuráveis ​​por meio de um portal administrativo, portanto, nos ambientes de desenvolvimento, você poderia adicionar todas as origens necessárias.

No meu caso, response.getWriter (). Flush () não estava funcionando

Alterou o código como abaixo e começou a funcionar

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

Eu concordo totalmente com a resposta dada por Bludream, mas tenho algumas observações:

Eu estenderia a cláusula if no filtro CORS com uma verificação NULL no header de origem:

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

Além disso, observei o seguinte comportamento indesejado: Se eu tentar acessar uma API REST com uma function não autorizada, a segurança do Spring retornará um status HTTP 403: FORBIDDEN e os headers CORS serão retornados. No entanto, se eu usar um token desconhecido ou um token que não é mais válido, um status HTTP 401: UNAUTHORIZED será retornado SEM headers CORS.

Consegui fazê-lo funcionar alterando a configuração do filtro no XML de segurança assim:

  ... //your other configs   

E o seguinte bean para nosso filtro personalizado:

  

Como a parte principal da questão é sobre solicitação CORS POST não autorizada para o ponto de login, eu imediatamente aponto para a etapa 2 .

Mas em relação às respostas, essa é a pergunta mais relevante para a solicitação do Spring Security CORS . Então vou descrever uma solução mais elegante para configurar o CORS com o Spring Security. Porque, exceto em raras situações, não é necessário criar filtros / interceptadores /… para colocar qualquer coisa em resposta. Vamos fazer isso declarativamente pela primavera. Desde o Spring Framework 4.2, temos o CORS-stuff como filtro, processador, etc pronto para o uso. E alguns links para ler 1 2 .

Vamos:

1. prepare a fonte de configuração do CORS.

Isso pode ser feito de diferentes maneiras:

  • como configuração global do Spring MVC CORS (em classs de configuração como o WebMvcConfigurerAdapter )

     ... @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOrigins("*") ... } 
  • como separado corsConfigurationSource bean

     @Bean CorsConfigurationSource corsConfigurationSource() { CorsConfiguration config = new CorsConfiguration(); config.applyPermitDefaultValues(); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", config); } 
  • como class externa (que pode ser usada via construtor ou autowired como um componente)

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

2. ativar o suporte CORS com a configuração definida

Vamos ativar o suporte a CORS em classs do Spring Security, como o WebSecurityConfigurerAdapter . Certifique-se de que o corsConfigurationSource esteja acessível para este suporte. Outra opção é fornecer via autowiring @Resource ou definir explicitamente (consulte no exemplo). Também permitimos o access não autorizado a alguns endpoints como login:

  ... // @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. personalize a configuração do CORS

Se a configuração básica funcionar, então podemos personalizar mapeamentos, origens, etc. Adicione várias configurações para diferentes mapeamentos. Por exemplo, declaro explicitamente todos os parâmetros CORS e deixo que o UrlPathHelper não apare meu caminho 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. solução de problemas

Para depurar meu problema, estava rastreando o método org.springframework.web.filter.CorsFilter#doFilterInternal . E vi que a pesquisa CorsConfiguration retorna null porque a configuração global de CORS do Spring MVC não era vista pelo Spring Security. Então usei solução com uso direto de class externa:

 http.cors().configurationSource(corsConfig); 
Intereting Posts