有 Java 编程相关的问题?

你可以在下面搜索框中键入要查询的问题!

使用Spring安全性的java跨源资源共享

我试图让CORS与Spring Security配合得很好,但它没有遵守。我做了this article中描述的更改,并且在applicationContext-security.xml中更改这一行已经获得了我的应用程序的POST和GET请求(临时公开控制器方法,以便我可以测试CORS):

  • 之前:<intercept-url pattern="/**" access="isAuthenticated()" />
  • 之后:<intercept-url pattern="/**" access="permitAll" />

不幸的是,以下允许通过AJAX进行Spring安全登录的URL没有响应:http://localhost:8080/mutopia-server/resources/j_spring_security_check。我正在从http://localhost:80http://localhost:8080发出AJAX请求

当尝试访问j_spring_security_check时,我在Chrome中获得(pending)选项飞行前请求和AJAX调用返回,HTTP状态代码为0,消息为“error”

在Firefox中

预飞行成功,HTTP状态代码为302,之后我仍然直接收到AJAX请求的错误回调,HTTP状态为0,消息为“error”

enter image description here

enter image description here

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

请注意-除了Spring安全登录,我可以通过CORS发布到我应用程序中的任何其他URL。我已经阅读了很多文章,因此对这个奇怪问题的任何见解都将不胜感激


共 (6) 个答案

  1. # 1 楼答案

    大多数情况下,选项请求不包含用于验证spring安全性的cookie
    为了解决这一问题,可以修改spring security的配置,以允许选项请求而无需身份验证
    我做了很多研究,得到了两种解决方案:
    1.使用Java配置和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();
    }
    

    二,。使用XML(注意。不能写“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>
    

    最后是the source for the solution...:)

  2. # 2 楼答案

    这是我的代码,对我来说非常好,非常完美:我花了两天时间研究它,了解spring安全性,所以我希望你接受它作为答案,哈哈

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

    那么您还需要设置要调用的过滤器:

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

    您需要一个bean来创建自定义过滤器:

    <bean id="corsHandler" class="mobilebackbone.mesoft.config.CorsFilter" />
    
  3. # 3 楼答案

    对我来说,问题是OPTIONS飞行前检查未能通过身份验证,因为在该调用中没有传递凭据

    这对我很有用:

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

    相关部分包括:

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

    这解决了飞行前的问题。这里发生的事情是,当你收到一个呼叫,身份验证失败时,你检查它是否是一个OPTIONS呼叫,如果是,就让它通过,让它做它想做的一切。这实际上禁用了所有浏览器端的飞行前检查,但正常的跨域策略仍然适用

    当您使用最新版本的Spring时,可以使用下面的代码全局地允许跨源请求(适用于所有控制器):

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

    请注意,这样硬编码很少是个好主意。在我工作过的几家公司中,允许的源代码可以通过管理门户进行配置,因此在开发环境中,您可以添加所需的所有源代码

  4. # 4 楼答案

    就我而言,答案是肯定的。getWriter()。flush()不起作用

    更改代码如下,它开始工作

    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);
        }
    }
    
  5. # 5 楼答案

    我可以通过扩展UsernamePasswordAuthenticationFilter来做到这一点。。。我的代码是Groovy,希望没问题:

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

    上面重要的几点:

    • 只有在检测到CORS请求时,才将CORS头添加到响应中
    • 用一个简单的非空200响应来响应飞行前选项请求,该响应还包含CORS标题

    您需要在Spring配置中声明这个bean。有很多文章展示了如何做到这一点,所以我不会在这里复制

    在我自己的实现中,我使用了一个源域名白名单,因为我只允许内部开发者访问CORS。以上是我正在做的事情的简化版本,可能需要调整,但这应该给你一个大致的想法

  6. # 6 楼答案

    自Spring Security 4.1以来,这是使Spring Security支持CORS的正确方法(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");
        }
    }
    

    以及:

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

    不要执行以下任何操作,这是解决问题的错误方法:

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

    参考:http://docs.spring.io/spring-security/site/docs/4.2.x/reference/html/cors.html