次の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初期化ルーチンを実行する必要があることを意味するように思われる、インターネットで利用可能な資料を確認しました。しかし、それは明らかに起こっていません。
私がmockMVC機能を使用して「init」ルーチンをテストする方法を理解するのに本当に行き詰まっているので、提案や推奨は大歓迎です。
Spring MVCテストスイートはコンテナー構成をテストするためのものではなく、MVC(_@Controller
_およびその他のマッピング)構成をテストするためのものです。 Filter#init(ServletConfig)
はコンテナ管理のメソッドです。
本当にテストする必要がある場合は、それも模擬できます
_@Before
public void setup() {
filter.init(someMockFilterConfig); // using a mock that you construct with init params and all
this.mockMvc = webAppContextSetup(this.wac)
.addFilters(filter).build();
}
_
たくさんのテストの後、これが私たちが採用したものです:
@RestController
_をテストするには、MockMvcを使用します。TestRestTemplate
を使用します。MockMvcでは、addFilter(Filter)
はフィルターをまったく実行しませんでした。 TestRestTemplate
を使用したソリューションはより原始的ですが、アプリケーション/ライブラリで構成されたすべてのフィルターが実行されます。例:
_@RunWith(SpringRunner.class)
@SpringBootTest(classes = MySpringBootApplication.class, webEnvironment=
SpringBootTest.WebEnvironment.RANDOM_PORT)
public class MyRestControllerTest {
@LocalServerPort
private int port;
@Test
public void myTestCase() throws Exception {
HttpStatus expectedStatusCode = HttpStatus.OK;
String expectedResponseBody = "{\"someProperty\" : \"someValue\" }";
HttpHeaders headers = new HttpHeaders();
headers.add("Authorization", "Bearer YourTokenJwtForExample");
HttpEntity<String> entity = new HttpEntity<>(null, headers);
TestRestTemplate restTemplate = new TestRestTemplate();
ResponseEntity<String> response = restTemplate.exchange(
"http://localhost:" + port + "/my-rest-uri",
HttpMethod.GET, entity, String.class);
Assert.assertEquals(expectedStatusCode, response.getStatusCode());
Assert.assertEquals(expectedResponseBody, response.getBody());
}
}
_
_Spring Boot
_アプリの場合、@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
を使用すると、filter.init()が自動的に呼び出されます。 _@SpringBootTest
_がデフォルトのパラメーターで使用されている場合、filter.init()を手動で呼び出す必要があります。
統合テストの代わりに真の単体テストが必要な場合は、org.springframework.mock.web.MockServletConfig
も参照してください。
org.springframework:spring-test
mavenアーティファクト
モックオブジェクトに設定パラメータを設定できます。 HttpServletRequest、HttpServletResponse、FilterChainのモックもあります。