CORS,即跨域资源共享(Cross-Origin Resource Sharing) 跨站HTTP请求是指发起请求的资源所在域不同于该请求所指向资源所在的域的HTTP请求。

CORS旨在定义一种规范让浏览器在接收到从提供者获取的资源时能够正决定是否应该将此资源分发给消费者作进一步处理。CROS利用资源提供者的显式授权来决定目标资源是否应该与消费者共享。

JavaScript出于安全方面的考虑,不允许跨域调用其他页面的对象,其同源策略说明如下:

链接 说明 是否跨域

www.a.com/a

www.a.com/a

同一域名

www.a.com/a

www.a.com/b

同一域名下的不同目录

www.a.com:8080/a

www.a.com:8081/a

同一域名下的不同端口

www.a.com/a

api.a.com/a

子域不同

www.a.com/a

www.b.com/a

不同域名

一、CORS相关

当一个现代浏览器制图要去进行跨域请求时(method为GET、HEAD时除外),浏览器会发送一个带有Origin参数的OPTIONS请求,即预检查请求,这个参数值就是当前域的域名。比如,当从 http://www.a.com 尝试去访问 www.b.com 下的资源时,

Origin:http://www.a.com

将会附加在发往www.b.com的请求头上

当服务端接受到这个OPTIONS请求时,会有三种处理方式:

1、不允许进行跨域资源请求

2、允许所有的域进行资源访问,即在响应头上加入:

Access-Control-Allow-Origin: *

如果这样做,处于安全性的考虑,响应头上将不能携带cookie,所以一般的情况应该是这样:

3、在响应头上声明可以进行访问的域:

Access-Control-Allow-Origin: http://www.a.com

这样,www.a.com就可以请求到www.b.com的资源。

除此之外,与CORS相关的请求头信息还有:

Access-Control-Request-Method

这个请求头的作用在于指定接下来的跨域请求中的方法。

Access-Control-Request-Header

这个是指定跨域请求时所携带的自定义头信息。

与CORS相关的响应体信息还有:

Access-Control-Allow-Methods

这个响应头的作用在于指定在接下来的真正的请求中被允许的方法(Method),比如在这里设置为”POST”,如果接下来的请求是PUT方法,那么将会失败。

Access-Control-Allow-Headers

这个是指请求中所允许携带的请求头信息。

Access-Control-Max-Age

可以指定缓存时间,避免浏览器频繁发送OPTIONS请求。

Access-Control-Allow-Credentials

这个响应头用来表明服务端是否支持用户凭证,比如Cookie、HTTP-Authentication、证书等。如果需要,则设置此值为true。

二、Spring MVC 中的应用

在实际Spring MVC + Shiro 项目中,由于进行前后端分离改造不可避免的要遇到跨域问题。

首先,先要解决的是Spring MVC对OPTIONS请求拦截的问题。在Spring MVC servlet配置中,添加

<init-param>
    <param-name>dispatchOptionsRequest</param-name>
    <param-value>true</param-value>
</init-param>

允许DispatcherServlet分发器分发Options请求。

在配置bean时,需要注意的是shiro也是会拦截OPTIONS请求的。由于OPTIONS请求中没有携带任何cookie信息,shiro直接会认为该请求没有权限进而拦截在外。所以,在shiro中要自定义一个filter,用于排除OPTIONS请求。

public class CorsUserAuthenticationFilter extends UserFilter {
    /**
     * 不过滤OPTIONS方法
     */
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        HttpServletRequest httpRequest = WebUtils.toHttp(request);
        if ("OPTIONS".equalsIgnoreCase(httpRequest.getMethod())) {
            return true;
        }

        return super.isAccessAllowed(request, response, mappedValue);
    }
}

在这里继承了shiro自带的UserFilter,并重写isAccessAllowed函数,如果请求的Method为OPTIONS时,返回true,表示不过滤。

经过上述两步,OPTIONS请求我们就可以在程序中真正拿到并且可以控制其响应头参数,达到想要的目的。

一开始在项目中使用比较原始的方法,在复杂请求的同名接口上新增一个OPTIONS请求处理:

@RequestMapping(value = "", method = RequestMethod.OPTIONS)
public HttpEntity optionsHandle(AppUser appUser, HttpServletRequest request,
        HttpServletResponse response) {
    MultiValueMap<String, String> map = new LinkedMultiValueMap<String, String>();
    String origin = request.getHeader("Origin");
    map.add("Access-Control-Allow-Credentials", "true");
    map.add("Access-Control-Allow-Origin", origin);
    map.add("Access-Control-Allow-Methods", "POST,GET,OPTIONS,DELETE,PUT,OPTIONS");
    map.add("Access-Control-Allow-Headers", "X-Requested-With,Content-Type,Accept");
    HttpEntity he = new HttpEntity(map);
    return he;
}

要注意的是@RequestMapping中的value要和复杂请求的value一致,即要保证他们俩的URL是一致的。在这边我只是为了方便调试才将请求的Origin直接放入 Access-Control-Allow-Origin 中。在部署的时候,如果为了安全性考虑,可以将这个值写死,指定特定的域名才能跨域请求资源。

当时测试通过后在考虑到项目中这么多接口,是否应该换一种方式进行处理更为妥当。考虑之后决定采用拦截器的方式实现。

在Spring mvc的xml中添加:

<mvc:interceptors>
    <mvc:interceptor>
        <mvc:mapping path="/**" />
        <bean class="xxx.interceptor.CorsRequestInterceptor" />
    </mvc:interceptor>
</mvc:interceptors>

这里配置了一个全地址匹配的拦截器,拦截器的实现如下:

public class CorsRequestInterceptor implements HandlerInterceptor {

    String origin = "";

    @Override
    public void afterCompletion(HttpServletRequest arg0,
            HttpServletResponse arg1, Object arg2, Exception arg3)
            throws Exception {
    }

    @Override
    public void postHandle(HttpServletRequest arg0, HttpServletResponse response,
            Object arg2, ModelAndView arg3) throws Exception {
    }

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
            Object arg2) throws Exception {
        origin = request.getHeader("Origin");
        response.addHeader("Access-Control-Allow-Credentials","true");
        response.addHeader("Access-Control-Allow-Origin", origin);
        response.addHeader("Access-Control-Allow-Methods", "POST,GET,DELETE,PUT,OPTIONS");
        response.addHeader("Access-Control-Allow-Headers", "X-Requested-With,Content-Type,Accept");
        return true;
    }
}

这样,可以在所有的响应中都加上跨域信息。

对于OPTIONS方法的处理,通过一个匹配全地址的Controller进行处理:

@Controller
public class OptionsMethodController {
    @RequestMapping(value = {"/**"}, method = RequestMethod.OPTIONS)
    public void saveOptions(HttpServletRequest resquest,HttpServletResponse response) {
        response.setStatus(200);
    }
}

通过以上配置,就可以实现全接口的跨域处理。

Ps:在Spring MVC 4.2以上版本中还可以通过以下方式实现:

@Configuration
public class MigratedConfiguration {
    @Bean
    public WebMvcConfigurer corsConfigurer() {
        return new WebMvcConfigurerAdapter() {
            @Override
            public void addCorsMappings(CorsRegistry registry) {
                registry.addMapping("/**").allowedOrigins("http://www.a.com");
            }
        };
    }
}