HttpSecurity在WebSecurityConfigurerAdapter与ResourceServerConfigurerAdapter哪个优先
问题描述
最近因为申请公网域名发布环境需要安全过审,安全提出需要为所有的请求的响应头加上一些安全header,如下信息:
"X-XSS-Protection": "1;mode=block"
"X-Frame-Options": "SAMEORIGIN"
"Content-Security-Policy": "default-src https: data: 'unsafe-inline' 'unsafe-eval'"
"Strict-Transport-Security": "max-age=63072000; includeSubdomains"
系统采用的是SpringBoot+SpringMVC,web容器内嵌Tomcat,启用了Spring Security+OAuth2授权认证,发现不管是添加了Filter还是HandlerInterceptor,都不能完全实现自定义Header的功能,因为其中的一项X-Frame-Options总是被改写成DENY。
1、DENY
表示该页面不允许在frame中展示,即便是在相同域名的页面中嵌套也不允许。
2、SAMEORIGIN
表示该页面可以在相同域名页面的frame中展示。
3、ALLOW-FROM uri
表示该页面可以在指定来源的frame中展示。
问题排查
准备验证代码
过滤器
package com.order.config;
import org.springframework.util.AntPathMatcher;
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.IOException;
import java.util.Map;
/**
* @description <pre>
* 过滤器
* </pre>
* @since 2020/6/23 1:26 下午
*/
public class ResponseHeaderSettingFilter extends OncePerRequestFilter {
private ResponseHeaderSettingProperties responseHeaderSettingProperties;
private static AntPathMatcher antPathMatcher = new AntPathMatcher();
public ResponseHeaderSettingFilter(ResponseHeaderSettingProperties responseHeaderSettingProperties) {
this.responseHeaderSettingProperties = responseHeaderSettingProperties;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String[] excludeUrls = responseHeaderSettingProperties.getExcludeUrls();
boolean isExcludedPage = false;
if (excludeUrls != null && excludeUrls.length > 0) {
//判断是否在过滤url之外
String pathInfo = request.getRequestURI();
for (String page : excludeUrls) {
if (pathInfo != null && antPathMatcher.match(page, pathInfo)) {
isExcludedPage = true;
break;
}
}
}
System.out.println("--------Filter.pre--------");
filterChain.doFilter(request, response);
if (!isExcludedPage) {
Map<String, String> headers = responseHeaderSettingProperties.getHeaders();
if (headers == null || headers.size() == 0) {
response.setHeader("X-XSS-Protection", "1;mode=block");
response.setHeader("X-Frame-Options", "SAMEORIGIN");
response.setHeader("Content-Security-Policy", "default-src https: data: 'unsafe-inline' 'unsafe-eval'");
response.setHeader("Strict-Transport-Security", "max-age=63072000; includeSubdomains");
} else {
headers.forEach((k, v) -> {
response.setHeader(k, v);
});
}
response.setHeader("add-type", "filter");
}
System.out.println("--------Filter.post----------");
}
}
拦截器
package com.order.config;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Map;
/**
* @author zhanghuihuang
* @description <pre>
*
* </pre>
* @since 2020/6/23 1:26 下午
*/
public class ResponseHeaderSettingHandlerInterceptor implements HandlerInterceptor {
private ResponseHeaderSettingProperties responseHeaderSettingProperties;
private static AntPathMatcher antPathMatcher = new AntPathMatcher();
public ResponseHeaderSettingHandlerInterceptor(ResponseHeaderSettingProperties responseHeaderSettingProperties) {
this.responseHeaderSettingProperties = responseHeaderSettingProperties;
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("ResponseHeaderSettingHandlerInterceptor.preHandle");
this.headerSetting(request, response, "Interceptor.preHandle");
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
System.out.println("ResponseHeaderSettingHandlerInterceptor.postHandle");
this.headerSetting(request, response, "Interceptor.postHandle");
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
System.out.println("ResponseHeaderSettingHandlerInterceptor.afterCompletion");
this.headerSetting(request, response, "Interceptor.afterCompletion");
}
protected boolean headerSetting(HttpServletRequest request, HttpServletResponse response, String addType) {
String[] excludeUrls = responseHeaderSettingProperties.getExcludeUrls();
boolean isExcludedPage = false;
if (excludeUrls != null && excludeUrls.length > 0) {
//判断是否在过滤url之外
String pathInfo = request.getRequestURI();
for (String page : excludeUrls) {
if (pathInfo != null && antPathMatcher.match(page, pathInfo)) {
isExcludedPage = true;
break;
}
}
}
if (!isExcludedPage) {
Map<String, String> headers = responseHeaderSettingProperties.getHeaders();
if (headers == null || headers.size() == 0) {
response.setHeader("X-XSS-Protection", "1;mode=block");
response.setHeader("X-Frame-Options", "SAMEORIGIN");
response.setHeader("Content-Security-Policy", "default-src https: data: 'unsafe-inline' 'unsafe-eval'");
response.setHeader("Strict-Transport-Security", "max-age=63072000; includeSubdomains");
} else {
headers.forEach((k, v) -> {
response.setHeader(k, v);
});
}
response.setHeader("add-type", addType);
}
return isExcludedPage;
}
}
HeaderWriter
package com.order.config;
import org.springframework.security.web.header.HeaderWriter;
import org.springframework.util.AntPathMatcher;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Map;
public class ResponseHeaderSettingWriter implements HeaderWriter {
private ResponseHeaderSettingProperties responseHeaderSettingProperties;
private static AntPathMatcher antPathMatcher = new AntPathMatcher();
public ResponseHeaderSettingWriter(ResponseHeaderSettingProperties responseHeaderSettingProperties) {
this.responseHeaderSettingProperties = responseHeaderSettingProperties;
}
@Override
public void writeHeaders(HttpServletRequest request, HttpServletResponse response) {
String[] excludeUrls = responseHeaderSettingProperties.getExcludeUrls();
boolean isExcludedPage = false;
if (excludeUrls != null && excludeUrls.length > 0) {
//判断是否在过滤url之外
String pathInfo = request.getRequestURI();
for (String page : excludeUrls) {
if (pathInfo != null && antPathMatcher.match(page, pathInfo)) {
isExcludedPage = true;
break;
}
}
}
if (!isExcludedPage) {
Map<String, String> headers = responseHeaderSettingProperties.getHeaders();
if (headers == null || headers.size() == 0) {
response.setHeader("X-XSS-Protection", "1;mode=block");
response.setHeader("X-Frame-Options", "SAMEORIGIN");
response.setHeader("Content-Security-Policy", "default-src https: data: 'unsafe-inline' 'unsafe-eval'");
response.setHeader("Strict-Transport-Security", "max-age=63072000; includeSubdomains");
} else {
headers.forEach((k, v) -> {
response.setHeader(k, v);
});
}
response.setHeader("add-type", "writer");
}
System.out.println("==========HeaderWriter=========");
}
}
配置类
package com.order.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.util.Map;
@Component
@ConfigurationProperties(prefix = "response.header.setting")
public class ResponseHeaderSettingProperties {
private boolean enable = false;
private String[] excludeUrls = new String[]{"/webjars/**", "/swagger-ui.html", "/images/**", "/oauth/uncache_approvals", "/oauth/cache_approvals", "/userrole/**"};
private Map<String, String> headers;
public boolean isEnable() {
return enable;
}
public void setEnable(boolean enable) {
this.enable = enable;
}
public String[] getExcludeUrls() {
return excludeUrls;
}
public void setExcludeUrls(String[] excludeUrls) {
this.excludeUrls = excludeUrls;
}
public Map<String, String> getHeaders() {
return headers;
}
public void setHeaders(Map<String, String> headers) {
this.headers = headers;
}
}
package com.order.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* @author zhanghuihuang
* @description <pre>
*
* </pre>
* @since 2020/6/23 10:00 上午
*/
@Configuration
public class ResponseHeaderSettingConfig extends ResourceServerConfigurerAdapter implements WebMvcConfigurer {
@Autowired
private ResponseHeaderSettingProperties responseHeaderSettingProperties;
@Override
public void configure(HttpSecurity http) throws Exception {
http.headers().addHeaderWriter(new ResponseHeaderSettingWriter(responseHeaderSettingProperties));
}
@Bean
public FilterRegistrationBean<ResponseHeaderSettingFilter> responseHeaderSettingFilter() {
FilterRegistrationBean bean = new FilterRegistrationBean();
bean.setFilter(new ResponseHeaderSettingFilter(responseHeaderSettingProperties));
bean.addUrlPatterns("/*");
bean.setOrder(1);
return bean;
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new ResponseHeaderSettingHandlerInterceptor(responseHeaderSettingProperties));
}
}
执行结果
可以看到执行顺序
filter前处理 → interceptor.preHandle → HeaderWriter → interceptor.postHandle → interceptor.afterCompletion → filter后处理
Filter和Interceptor的区别
相同点:
- 两者都是AOP编程的具体实现方式
不同点:
- Filter是依赖于Servlet容器,属于Servlet规范的一部分,而拦截器则是独立存在的,可以在任何情况下使用
- Filter的执行由Servlet容器回调完成,而拦截器通常通过动态代理的方式来执行
- Filter的生命周期由Servlet容器管理,而拦截器则可以通过IoC容器来管理,因此可以通过注入等方式来获取其他Bean的实例,因此使用会更方便
Filter和HandlerInterceptor改写Header为什么不生效
由于系统集成了spring security+oauth,默认启用了一系列的HeaderWriter,这些HeaderWriter放在一个叫HeaderWriterFilter的过滤器里面,而且执行这个后,会修改org.springframework.security.web.util.OnCommittedResponseWrapper#disableOnResponseCommitted,导致后面对header的修改都不能生效,所以在interceptor.postHandle → interceptor.afterCompletion → filter后处理这3个位置对header进行修改不生效。
在filter前处理 → interceptor.preHandle这两不添加的header,会被HeaderWriter覆盖
通过在各个节点打桩,添加header,你也可以观察出来
WebSecurityConfigurerAdapter为什么还不生效
问题解决
最终采用自定义HeaderWriter,然后配置类集成ResourceServerConfigurerAdapter,可以实现完全的header自定义