package cops.com.deppon.notice.restful.service;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.UndeclaredThrowableException;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class SimpleCORSFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
if (request.getHeader(HttpHeader.ORIGIN) != null) {
boolean preFlightRequest = "OPTIONS".equals(request.getMethod())
&& request.getHeader(HttpHeader.ACCESS_CONTROL_REQUEST_METHOD) != null;
this.processRequest(request, response, preFlightRequest);
if (preFlightRequest) {
return;
}
}
filterChain.doFilter(request, response);
}
protected void processRequest(HttpServletRequest request, HttpServletResponse response, boolean preFlightRequest)
throws IOException {
Map
reqHeaderFirstValMap = CorsUtils.createReqHeaderFirstValMap(request);
// 同域情况下不需要跨域头
if (CorsUtils.isSameOrigin(request, reqHeaderFirstValMap)) {
logger.debug("Skip CORS processing: request is from same origin");
return;
}
// 根据指定规则配置跨域头
this.handleInternal(reqHeaderFirstValMap, request.getMethod(), response, preFlightRequest);
}
/** Handle the given request. */
private void handleInternal(Map reqHeaderFirstValMap, String requestMethod,
HttpServletResponse response, boolean preFlightRequest) throws IOException {
String allowOrigin = reqHeaderFirstValMap.get(HttpHeader.ORIGIN);
Set allowHeaders = preFlightRequest ?
Collections.singleton(reqHeaderFirstValMap.get(HttpHeader.ACCESS_CONTROL_REQUEST_HEADERS)) :
reqHeaderFirstValMap.keySet();
String allowMethod = preFlightRequest ?
reqHeaderFirstValMap.get(HttpHeader.ACCESS_CONTROL_REQUEST_METHOD) :
requestMethod;
response.addHeader(HttpHeader.ACCESS_CONTROL_ALLOW_ORIGIN, allowOrigin);
response.addHeader(HttpHeader.VARY, HttpHeader.ORIGIN);
response.addHeader(HttpHeader.ACCESS_CONTROL_ALLOW_CREDENTIALS, "true");
if (preFlightRequest) {
response.addHeader(HttpHeader.ACCESS_CONTROL_ALLOW_METHODS, allowMethod);
if (!allowHeaders.isEmpty()) {
response.addHeader(HttpHeader.ACCESS_CONTROL_ALLOW_HEADERS,
StringUtils.collectionToCommaDelimitedString(allowHeaders));
}
}
}
private interface HttpHeader {
/**
* The CORS {@code Access-Control-Request-Headers} request header field name.
*
* @see CORS W3C recommendation
*/
String ACCESS_CONTROL_REQUEST_HEADERS = "Access-Control-Request-Headers";
/**
* The CORS {@code Access-Control-Request-Method} request header field name.
*
* @see CORS W3C recommendation
*/
String ACCESS_CONTROL_REQUEST_METHOD = "Access-Control-Request-Method";
/**
* The HTTP {@code Origin} header field name.
*
* @see RFC 6454
*/
String ORIGIN = "Origin";
/**
* The CORS {@code Access-Control-Allow-Origin} response header field name.
*
* @see CORS W3C recommendation
*/
String ACCESS_CONTROL_ALLOW_ORIGIN = "Access-Control-Allow-Origin";
/**
* The CORS {@code Access-Control-Allow-Credentials} response header field name.
*
* @see CORS W3C recommendation
*/
String ACCESS_CONTROL_ALLOW_CREDENTIALS = "Access-Control-Allow-Credentials";
/**
* The CORS {@code Access-Control-Allow-Methods} response header field name.
*
* @see CORS W3C recommendation
*/
String ACCESS_CONTROL_ALLOW_METHODS = "Access-Control-Allow-Methods";
/**
* The CORS {@code Access-Control-Allow-Headers} response header field name.
*
* @see CORS W3C recommendation
*/
String ACCESS_CONTROL_ALLOW_HEADERS = "Access-Control-Allow-Headers";
/**
* The HTTP {@code Vary} header field name.
*
* @see Section 7.1.4 of RFC 7231
*/
String VARY = "Vary";
}
private static abstract class CorsUtils {
private static final String SCHEME_PATTERN = "([^:/?#]+):";
private static final String USERINFO_PATTERN = "([^@\\[/?#]*)";
private static final String HOST_IPV4_PATTERN = "[^\\[/?#:]*";
private static final String HOST_IPV6_PATTERN = "\\[[\\p{XDigit}\\:\\.]*[%\\p{Alnum}]*\\]";
private static final String HOST_PATTERN = "(" + HOST_IPV6_PATTERN + "|" + HOST_IPV4_PATTERN + ")";
private static final String PORT_PATTERN = "(\\d*(?:\\{[^/]+?\\})?)";
private static final String PATH_PATTERN = "([^?#]*)";
private static final String QUERY_PATTERN = "([^#]*)";
private static final String LAST_PATTERN = "(.*)";
// Regex patterns that matches URIs. See RFC 3986, appendix B
private static final Pattern URI_PATTERN = Pattern.compile(
"^(" + SCHEME_PATTERN + ")?" + "(//(" + USERINFO_PATTERN + "@)?" + HOST_PATTERN + "(:" + PORT_PATTERN +
")?" + ")?" + PATH_PATTERN + "(\\?" + QUERY_PATTERN + ")?" + "(#" + LAST_PATTERN + ")?");
private static final Pattern FORWARDED_HOST_PATTERN = Pattern.compile("host=\"?([^;,\"]+)\"?");
private static final Pattern FORWARDED_PROTO_PATTERN = Pattern.compile("proto=\"?([^;,\"]+)\"?");
public static boolean isSameOrigin(HttpServletRequest request, Map reqHeaderFirstValMap) {
String actualScheme = request.getScheme();
String actualHost = request.getServerName();
int actualPort = request.getServerPort();
String forwardedHeader = reqHeaderFirstValMap.get("Forwarded");
if (StringUtils.hasText(forwardedHeader)) {
String forwardedToUse = StringUtils.commaDelimitedListToStringArray(forwardedHeader)[0];
Matcher matcher = FORWARDED_HOST_PATTERN.matcher(forwardedToUse);
if (matcher.find()) {
actualHost = matcher.group(1).trim();
}
matcher = FORWARDED_PROTO_PATTERN.matcher(forwardedToUse);
if (matcher.find()) {
actualScheme = matcher.group(1).trim();
}
} else {
String hostHeader = reqHeaderFirstValMap.get("X-Forwarded-Host");
if (StringUtils.hasText(hostHeader)) {
String hostToUse = StringUtils.tokenizeToStringArray(hostHeader, ",")[0];
int portSeparatorIdx = hostToUse.lastIndexOf(":");
if (portSeparatorIdx > hostToUse.lastIndexOf("]")) {
actualHost = hostToUse.substring(0, portSeparatorIdx);
actualPort = Integer.parseInt(hostToUse.substring(portSeparatorIdx + 1));
} else {
actualHost = hostToUse;
actualPort = -1;
}
}
String portHeader = reqHeaderFirstValMap.get("X-Forwarded-Port");
if (StringUtils.hasText(portHeader)) {
actualPort = Integer.parseInt(StringUtils.commaDelimitedListToStringArray(portHeader)[0]);
}
String protocolHeader = reqHeaderFirstValMap.get("X-Forwarded-Proto");
if (StringUtils.hasText(protocolHeader)) {
actualScheme = StringUtils.commaDelimitedListToStringArray(protocolHeader)[0];
}
}
actualPort = resolvePort(actualPort, actualScheme);
String origin = reqHeaderFirstValMap.get(HttpHeader.ORIGIN);
Matcher matcher = URI_PATTERN.matcher(origin);
if (matcher.matches()) {
String originScheme = matcher.group(2);
if (!StringUtils.hasLength(originScheme)) {
originScheme = null;
}
String originHost = matcher.group(6);
int originPort = resolvePort(matcher.group(8), originScheme);
return actualHost.equals(originHost) && actualPort == originPort;
} else {
throw new IllegalArgumentException("[" + origin + "] is not a valid \"Origin\" header value");
}
}
private static int resolvePort(String port, String scheme) {
if (StringUtils.hasLength(port)) {
return resolvePort(Integer.parseInt(port), scheme);
}
return resolvePort(-1, scheme);
}
private static int resolvePort(int port, String scheme) {
if (port == -1) {
if ("https".equals(scheme) || "wss".equals(scheme)) {
port = 443;
} else if ("http".equals(scheme) || "ws".equals(scheme)) {
port = 80;
}
}
return port;
}
public static Map createReqHeaderFirstValMap(HttpServletRequest request) {
Map reqHeaderFirstValMap = new LinkedHashMap() {
private Map caseInsensitiveKeys = new HashMap();
@Override
public String put(String key, String value) {
String oldKey = this.caseInsensitiveKeys.put(convertKey(key), key);
if (oldKey != null && !oldKey.equals(key)) {
super.remove(oldKey);
}
return super.put(key, value);
}
@Override
public void putAll(Map extends String, ? extends String> map) {
if (map.isEmpty()) {
return;
}
for (Map.Entry extends String, ? extends String> entry : map.entrySet()) {
put(entry.getKey(), entry.getValue());
}
}
@Override
public boolean containsKey(Object key) {
return (key instanceof String && this.caseInsensitiveKeys.containsKey(convertKey((String) key)));
}
@Override
public String get(Object key) {
if (key instanceof String) {
String caseInsensitiveKey = this.caseInsensitiveKeys.get(convertKey((String) key));
if (caseInsensitiveKey != null) {
return super.get(caseInsensitiveKey);
}
}
return null;
}
// Overridden to avoid LinkedHashMap's own hash computation in its getOrDefault impl JDK 1.8
public String getOrDefault(Object key, String defaultValue) {
if (key instanceof String) {
String caseInsensitiveKey = this.caseInsensitiveKeys.get(convertKey((String) key));
if (caseInsensitiveKey != null) {
return super.get(caseInsensitiveKey);
}
}
return defaultValue;
}
@Override
public String remove(Object key) {
if (key instanceof String) {
String caseInsensitiveKey = this.caseInsensitiveKeys.remove(convertKey((String) key));
if (caseInsensitiveKey != null) {
return super.remove(caseInsensitiveKey);
}
}
return null;
}
@Override
public void clear() {
this.caseInsensitiveKeys.clear();
super.clear();
}
@Override @SuppressWarnings("unchecked")
public Object clone() {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream out = null;
try {
// stream closed in the finally
out = new ObjectOutputStream(bos);
out.writeObject(this);
} catch (final IOException e) {
throw new UndeclaredThrowableException(e);
} finally {
try {
if (out != null) {
out.close();
}
} catch (final IOException e) { // NOPMD
// ignore close exception
}
}
ByteArrayInputStream bis =
new ByteArrayInputStream(bos.toByteArray());
ObjectInputStream ois = null;
try {
ois = new ObjectInputStream(bis);
return ois.readObject();
} catch (Exception e) {
throw new UndeclaredThrowableException(e);
} finally {
try {
if (ois != null) {
ois.close();
}
} catch (final IOException e) { // NOPMD
// ignore close exception
}
}
}
protected String convertKey(String key) {
return key.toLowerCase(Locale.ENGLISH);
}
};
Enumeration> headerNames = request.getHeaderNames();
while (headerNames.hasMoreElements()) {
String headerName = (String) headerNames.nextElement();
reqHeaderFirstValMap.put(headerName,
(String) request.getHeaders(headerName).nextElement());
}
return Collections.unmodifiableMap(reqHeaderFirstValMap);
}
}
}