我已经实现了以下CORS过滤器,当代码在服务器上执行时可以正常工作:
/*
* Copyright 2013 BrandsEye (http://www.brandseye.com)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.energyos.espi.datacustodian.web.filter;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Enumeration;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.regex.Pattern;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.stereotype.Component;
/**
* Adds CORS headers to requests to enable cross-domain access.
*/
@Component
public class CORSFilter implements Filter {
private final Log logger = LogFactory.getLog(getClass());
private final Map<String, String> optionsHeaders = new LinkedHashMap<String, String>();
private Pattern allowOriginRegex;
private String allowOrigin;
private String exposeHeaders;
public void init(FilterConfig cfg) throws ServletException {
String regex = cfg.getInitParameter("allow.origin.regex");
if (regex != null) {
allowOriginRegex = Pattern.compile(regex, Pattern.CASE_INSENSITIVE);
} else {
optionsHeaders.put("Access-Control-Allow-Origin", "*");
}
optionsHeaders.put("Access-Control-Allow-Headers", "Origin, Authorization, Accept, Content-Type");
optionsHeaders.put("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
optionsHeaders.put("Access-Control-Max-Age", "1800");
for (Enumeration<String> i = cfg.getInitParameterNames(); i.hasMoreElements(); ) {
String name = i.nextElement();
if (name.startsWith("header:")) {
optionsHeaders.put(name.substring(7), cfg.getInitParameter(name));
}
}
//maintained for backward compatibility on how to set allowOrigin if not
//using a regex
allowOrigin = optionsHeaders.get("Access-Control-Allow-Origin");
//since all methods now go through checkOrigin() to apply the Access-Control-Allow-Origin
//header, and that header should have a single value of the requesting Origin since
//Access-Control-Allow-Credentials is always true, we remove it from the options headers
optionsHeaders.remove("Access-Control-Allow-Origin");
exposeHeaders = cfg.getInitParameter("expose.headers");
}
public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
throws IOException, ServletException {
if (logger.isDebugEnabled()) {
logger.debug("CORSFilter processing: Checking for Cross Origin pre-flight OPTIONS message");
}
if (request instanceof HttpServletRequest && response instanceof HttpServletResponse) {
HttpServletRequest req = (HttpServletRequest)request;
HttpServletResponse resp = (HttpServletResponse)response;
if ("OPTIONS".equals(req.getMethod())) {
allowOrigin = "*"; //%%%%% Test force of allowOrigin
if (checkOrigin(req, resp)) {
for (Map.Entry<String, String> e : optionsHeaders.entrySet()) {
resp.addHeader(e.getKey(), e.getValue());
}
// We need to return here since we don't want the chain to further process
// a preflight request since this can lead to unexpected processing of the preflighted
// request or a 40x - Response Code
return;
}
} else if (checkOrigin(req, resp)) {
if (exposeHeaders != null) {
resp.addHeader("Access-Control-Expose-Headers", exposeHeaders);
}
}
}
filterChain.doFilter(request, response);
}
private boolean checkOrigin(HttpServletRequest req, HttpServletResponse resp) {
String origin = req.getHeader("Origin");
if (origin == null) {
//no origin; per W3C specification, terminate further processing for both pre-flight and actual requests
return false;
}
boolean matches = false;
//check if using regex to match origin
if (allowOriginRegex != null) {
matches = allowOriginRegex.matcher(origin).matches();
} else if (allowOrigin != null) {
matches = allowOrigin.equals("*") || allowOrigin.equals(origin);
}
if (matches) {
// Activate next two lines and comment out third line if Credential Support is required
// resp.addHeader("Access-Control-Allow-Origin", origin);
// resp.addHeader("Access-Control-Allow-Credentials", "true");
resp.addHeader("Access-Control-Allow-Origin", "*");
return true;
} else {
return false;
}
}
public void destroy() {
}
}
以下JUnit测试使用mockMVC,但失败了,因为CORSFilter的“init”逻辑没有执行(通过在JUnit测试中打断点证明):
package org.energyos.espi.datacustodian.integration.web.filters;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import javax.servlet.FilterConfig;
import org.energyos.espi.datacustodian.web.filter.CORSFilter;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Profile;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.RequestBuilder;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.web.context.WebApplicationContext;
import static org.hamcrest.Matchers.is;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.options;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
import static org.springframework.test.web.servlet.setup.MockMvcBuilders.webAppContextSetup;
@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration
@ContextConfiguration("/spring/test-context.xml")
@Profile("test")
public class CORSFilterTests {
private final Log logger = LogFactory.getLog(getClass());
@Autowired
private CORSFilter filter;
@Autowired
private WebApplicationContext wac;
private MockMvc mockMvc;
@Before
public void setup() {
this.mockMvc = webAppContextSetup(this.wac)
.addFilters(filter).build();
}
@Test
public void optionsResponse_hasCorrectFilters() throws Exception {
RequestBuilder requestBuilder = MockMvcRequestBuilders.options("/DataCustodian/oauth/token")
.header("Origin", "foobar")
.header("Access-Control-Allow-Origin", "*");
MvcResult result = mockMvc.perform(requestBuilder)
.andExpect(header().string("Access-Control-Allow-Origin", is("*")))
.andExpect(header().string("Access-Control-Allow-Methods", is("GET, POST, PUT, DELETE, OPTIONS")))
.andExpect(header().string("Access-Control-Allow-Headers", is("origin, authorization, accept, content-type")))
.andExpect(header().string("Access-Control-Max-Age", is("1800")))
.andReturn();
}
}
}
我已经查阅了互联网上的相关资料,似乎暗示着mockMVC @Before部分中的“.addfilter(filter)”元素应该执行CORSFilter init例程。然而,这显然并没有发生。
如果您对如何使用mockMVC能力测试“init”程序感到困惑,那么任何建议或推荐将不胜感激。