Spring Boot 如何将SameSite和Secure属性设置为JSESSIONID cookie

rn0zuynd  于 2023-05-17  发布在  Spring
关注(0)|答案(4)|浏览(412)

我有一个Sping Boot Web应用程序(Spring Boot版本2.0.3.RELEASE),并在Apache Tomcat 8.5.5服务器上运行。
根据Google Chrome最近实施的安全策略(自80.0起推出),要求应用新的SameSite属性,以更安全的方式访问跨站点cookie,而不是CSRF。因为我没有做任何相关的事情,而且Chrome已经为第一方cookie设置了默认值SameSite=Lax,我的第三方服务集成之一是失败的原因,Chrome是限制访问跨站点的cookie时,SameSite=Lax和如果第三方响应是来自POST请求(O* 一旦该过程完成第三-通过POST请求 * 将第三方服务重定向到我们的网站)。在那里Tomcat无法找到会话,所以它在URL的末尾追加了一个新的JSESSIONID(带有一个新的会话,而前一个会话已被杀死)。所以Spring拒绝URL,因为它包含一个新的JSESSIONID append引入的分号。
因此,我需要更改JSESSIONID cookie属性(SameSite=None; Secure),并尝试了几种方法,包括WebFilters。我在Stackoverflow中看到了相同的问题和答案,并尝试了其中的大部分,但最终一无所获。
有人能想出一个解决方案来改变Sping Boot 中的这些头吗?

blpfk2vs

blpfk2vs1#

更新于2021年6月7日-添加了正确的Path属性和新的sameSite属性,以避免使用GenericFilterBean方法复制会话cookie。

我能想出我自己的解决办法。
我有两种在Sping Boot 上运行的应用程序,它们具有不同的Spring安全配置,它们需要不同的解决方案来解决这个问题。

情况1:无用户认证
方案一

在这里,您可能已经在应用程序中为第三方响应创建了一个端点。在控制器方法中访问httpSession之前,您是安全的。如果您正在访问不同控制器方法中的会话,请发送临时重定向请求到那里,如下所示。

@Controller
public class ThirdPartyResponseController{

@RequestMapping(value=3rd_party_response_URL, method=RequestMethod.POST)
public void thirdPartyresponse(HttpServletRequest request, HttpServletResponse httpServletResponse){
    // your logic
    // and you can set any data as an session attribute which you want to access over the 2nd controller 
    request.getSession().setAttribute(<data>)
    try {
        httpServletResponse.sendRedirect(<redirect_URL>);
    } catch (IOException e) {
        // handle error
    }
}

@RequestMapping(value=redirect_URL, method=RequestMethod.GET)
public String thirdPartyresponse(HttpServletRequest request,  HttpServletResponse httpServletResponse, Model model,  RedirectAttributes redirectAttributes, HttpSession session){
    // your logic
        return <to_view>;
    }
}

不过,您需要在安全配置中允许3rd_party_response_url。

方案二

您可以尝试下面描述的相同GenericFilterBean方法。

情况2:用户需要认证/登录

在一个Spring Web应用程序中,您已经通过HttpSecurityWebSecurity配置了大部分安全规则,请检查此解决方案。
示例安全配置,我已经测试了解决方案:

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.
          ......
          ..antMatchers(<3rd_party_response_URL>).permitAll();
          .....
          ..csrf().ignoringAntMatchers(<3rd_party_response_URL>);
    }
}

我想在此配置中强调的要点是,您应该允许来自Spring Security和CSRF保护的第三方响应URL(如果启用)。
然后,我们需要通过扩展GenericFilterBean类(Filter类对我不起作用)并通过拦截每个HttpServletRequest并设置响应头将SameSite属性设置为JSESSIONID cookie来创建HttpServletRequest Filter。

import org.apache.commons.lang3.StringUtils;
import org.springframework.http.HttpHeaders;
import org.springframework.web.filter.GenericFilterBean;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;

public class SessionCookieFilter extends GenericFilterBean {

    private final List<String> PATHS_TO_IGNORE_SETTING_SAMESITE = Arrays.asList("resources", <add other paths you want to exclude>);
    private final String SESSION_COOKIE_NAME = "JSESSIONID";
    private final String SESSION_PATH_ATTRIBUTE = ";Path=";
    private final String ROOT_CONTEXT = "/";
    private final String SAME_SITE_ATTRIBUTE_VALUES = ";HttpOnly;Secure;SameSite=None";

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest req = (HttpServletRequest) request;
        HttpServletResponse resp = (HttpServletResponse) response;
        String requestUrl = req.getRequestURL().toString();
        boolean isResourceRequest = requestUrl != null ? StringUtils.isNoneBlank(PATHS_TO_IGNORE_SETTING_SAMESITE.stream().filter(s -> requestUrl.contains(s)).findFirst().orElse(null)) : null;
        if (!isResourceRequest) {
            Cookie[] cookies = ((HttpServletRequest) request).getCookies();
            if (cookies != null && cookies.length > 0) {
                List<Cookie> cookieList = Arrays.asList(cookies);
                Cookie sessionCookie = cookieList.stream().filter(cookie -> SESSION_COOKIE_NAME.equals(cookie.getName())).findFirst().orElse(null);
                if (sessionCookie != null) {
                    String contextPath = request.getServletContext() != null && StringUtils.isNotBlank(request.getServletContext().getContextPath()) ? request.getServletContext().getContextPath() : ROOT_CONTEXT;
                    resp.setHeader(HttpHeaders.SET_COOKIE, sessionCookie.getName() + "=" + sessionCookie.getValue() + SESSION_PATH_ATTRIBUTE + contextPath + SAME_SITE_ATTRIBUTE_VALUES);
                }
            }
        }
        chain.doFilter(request, response);
    }
}

然后通过以下方式将此过滤器添加到Spring Security过滤器链

@Override
protected void configure(HttpSecurity http) throws Exception {
        http.
           ....
           .addFilterAfter(new SessionCookieFilter(), BasicAuthenticationFilter.class);
}

为了确定您需要将新过滤器放置在Spring的安全过滤器链中的位置,您可以轻松地调试Spring安全过滤器链并确定过滤器链中的正确位置。除了BasicAuthenticationFilterSecurityContextPersistanceFilter之后将是另一个理想的地方。
SameSite cookie属性将不支持某些old browser versions,在这种情况下,请检查浏览器并避免在不兼容的客户端中设置SameSite

private static final String _I_PHONE_IOS_12 = "iPhone OS 12_";
    private static final String _I_PAD_IOS_12 = "iPad; CPU OS 12_";
    private static final String _MAC_OS_10_14 = " OS X 10_14_";
    private static final String _VERSION = "Version/";
    private static final String _SAFARI = "Safari";
    private static final String _EMBED_SAFARI = "(KHTML, like Gecko)";
    private static final String _CHROME = "Chrome/";
    private static final String _CHROMIUM = "Chromium/";
    private static final String _UC_BROWSER = "UCBrowser/";
    private static final String _ANDROID = "Android";

    /*
     * checks SameSite=None;Secure incompatible Browsers
     * https://www.chromium.org/updates/same-site/incompatible-clients
     */
    public static boolean isSameSiteInCompatibleClient(HttpServletRequest request) {
        String userAgent = request.getHeader("user-agent");
        if (StringUtils.isNotBlank(userAgent)) {
            boolean isIos12 = isIos12(userAgent), isMacOs1014 = isMacOs1014(userAgent), isChromeChromium51To66 = isChromeChromium51To66(userAgent), isUcBrowser = isUcBrowser(userAgent);
            //TODO : Added for testing purpose. remove before Prod release.
            LOG.info("*********************************************************************************");
            LOG.info("is iOS 12 = {}, is MacOs 10.14 = {}, is Chrome 51-66 = {}, is Android UC Browser = {}", isIos12, isMacOs1014, isChromeChromium51To66, isUcBrowser);
            LOG.info("*********************************************************************************");
            return isIos12 || isMacOs1014 || isChromeChromium51To66 || isUcBrowser;
        }
        return false;
    }

    private static boolean isIos12(String userAgent) {
        return StringUtils.contains(userAgent, _I_PHONE_IOS_12) || StringUtils.contains(userAgent, _I_PAD_IOS_12);
    }

    private static boolean isMacOs1014(String userAgent) {
        return StringUtils.contains(userAgent, _MAC_OS_10_14)
            && ((StringUtils.contains(userAgent, _VERSION) && StringUtils.contains(userAgent, _SAFARI))  //Safari on MacOS 10.14
            || StringUtils.contains(userAgent, _EMBED_SAFARI)); // Embedded browser on MacOS 10.14
    }

    private static boolean isChromeChromium51To66(String userAgent) {
        boolean isChrome = StringUtils.contains(userAgent, _CHROME), isChromium = StringUtils.contains(userAgent, _CHROMIUM);
        if (isChrome || isChromium) {
            int version = isChrome ? Integer.valueOf(StringUtils.substringAfter(userAgent, _CHROME).substring(0, 2))
                : Integer.valueOf(StringUtils.substringAfter(userAgent, _CHROMIUM).substring(0, 2));
            return ((version >= 51) && (version <= 66));    //Chrome or Chromium V51-66
        }
        return false;
    }

    private static boolean isUcBrowser(String userAgent) {
        if (StringUtils.contains(userAgent, _UC_BROWSER) && StringUtils.contains(userAgent, _ANDROID)) {
            String[] version = StringUtils.splitByWholeSeparator(StringUtils.substringAfter(userAgent, _UC_BROWSER).substring(0, 7), ".");
            int major = Integer.valueOf(version[0]), minor = Integer.valueOf(version[1]), build = Integer.valueOf(version[2]);
            return ((major != 0) && ((major < 12) || (major == 12 && (minor < 13)) || (major == 12 && minor == 13 && (build < 2)))); //UC browser below v12.13.2 in android
        }
        return false;
    }

在SessionCookieFilter中添加上述检查,如下所示,

if (!isResourceRequest && !UserAgentUtils.isSameSiteInCompatibleClient(req)) {

此过滤器无法在本地主机环境中工作,因为它需要安全(HTTPS)连接来设置Secure cookie属性。
有关详细说明,请阅读此blog post

jgwigjjp

jgwigjjp2#

我之前也是这样。因为在javax.servlet.http.Cookie类中没有类似SameSite的东西,所以不可能添加它。

**第1部分:**所以我写了一个过滤器,它只拦截所需的第三方请求。

public class CustomFilter implements Filter {

    private static final String THIRD_PARTY_URI = "/third/party/uri";

    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) res;
        if(THIRD_PARTY_URI.equals(request.getRequestURI())) {
            chain.doFilter(request, new CustomHttpServletResponseWrapper(response));
        } else {
            chain.doFilter(request, response);
        }
    }
enter code here
    // ... init destroy methods here
    
}

**第2部分:**Cookie作为Set-Cookie响应头发送。因此,这个CustomHttpServletResponseWrapper覆盖了addCookie方法,并检查它是否是所需的cookie(JSESSIONID),而不是将其添加到cookie中,而是直接添加到具有SameSite=None属性的响应头Set-Cookie中。

public class CustomHttpServletResponseWrapper extends HttpServletResponseWrapper {

    public CustomHttpServletResponseWrapper(HttpServletResponse response) {
        super(response);
    }

    @Override
    public void addCookie(Cookie cookie) {
        if ("JSESSIONID".equals(cookie.getName())) {
            super.addHeader("Set-Cookie", getCookieValue(cookie));
        } else {
            super.addCookie(cookie);
        }
    }

    private String getCookieValue(Cookie cookie) {

        StringBuilder builder = new StringBuilder();
        builder.append(cookie.getName()).append('=').append(cookie.getValue());
        builder.append(";Path=").append(cookie.getPath());
        if (cookie.isHttpOnly()) {
            builder.append(";HttpOnly");
        }
        if (cookie.getSecure()) {
            builder.append(";Secure");
        }
        // here you can append other attributes like domain / max-age etc.
        builder.append(";SameSite=None");
        return builder.toString();
    }
}
qyzbxkaa

qyzbxkaa3#

正如本答复所述:Same-Site flag for session cookie in Spring Security

@Configuration
public static class WebConfig implements WebMvcConfigurer {
    @Bean
    public TomcatContextCustomizer sameSiteCookiesConfig() {
        return context -> {
            final Rfc6265CookieProcessor cookieProcessor = new Rfc6265CookieProcessor();
            cookieProcessor.setSameSiteCookies(SameSiteCookies.NONE.getValue());
            context.setCookieProcessor(cookieProcessor);
        };
    }
}

但这个看起来更简单

@Configuration
public static class WebConfig implements WebMvcConfigurer {
    @Bean
    public CookieSameSiteSupplier cookieSameSiteSupplier(){
        return CookieSameSiteSupplier.ofNone();
    }
}

或者......更简单,Sping Boot 自2.6.0起支持在application.properties中设置它。
关于SameSite Cookies的Spring文档

server.servlet.session.cookie.same-site = none
jrcvhitl

jrcvhitl4#

对于未来的开发者来说,根据spring的版本,通过代码修改配置(如在其他回复中所见)可能只是修改嵌入式tomcat服务器。如果您的项目部署在现有的Tomcat服务器中,则可以通过context.xml将Samesite添加到JSessionid。但是,有几种方法可以设置上下文,因此请检查此参考以确定您的场景。https://octopus.com/blog/defining-tomcat-context-paths

相关问题