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自定义

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值