繁体   English   中英

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

[英]How to set SameSite and Secure attribute to JSESSIONID cookie

我有一个 Spring Boot Web 应用程序(Spring boot 版本 2.0.3.RELEASE)并在 Apache Tomcat 8.5.5 服务器中运行。

Google Chrome(自 80.0 起推出)最近实施的安全策略要求应用新的SameSite属性,以更安全的方式代替 CSRF 进行跨站点 cookie 访问。 由于我没有做任何相关的事情并且 Chrome 已经为第一方 cookies 设置了默认值SameSite=Lax ,我的一个第三方服务集成失败了,原因是 chrome 在SameSite=Lax时限制跨站点 cookies 的访问SameSite=Lax ,如果第三方响应来自POST请求(一旦程序完成第三方服务重定向到我们的站点POST请求)。 在Tomcat中,无法找到session,因此它附加了新的JSESSIONID (具有88165786602108821220828282828282的新JSESSIONID (具有新的session和前面的session)。 append。

所以我需要更改JSESSIONID cookie 属性( SameSite=None; Secure )并以多种方式尝试它,包括 WebFilters。我在 Stackoverflow 中看到了相同的问题和答案,并尝试了其中的大部分但最终无处可去。

有人可以想出一个解决方案来更改 Spring Boot 中的这些标头吗?


在2021年6月7日更新-增加了正确Path与新sameSite属性属性,以避免与会话cookie重复GenericFilterBean方法。


我能够为此提出自己的解决方案。

我有两种在 Spring boot 上运行的应用程序,它们具有不同的 Spring 安全配置,他们需要不同的解决方案来解决这个问题。

案例一:没有用户认证

解决方案1

在这里,您可能已经在您的应用程序中为第 3 方响应创建了一个端点。 在您在控制器方法中访问 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。

解决方案2

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

案例二:用户需要认证/登录

在已通过HttpSecurityWebSecurity配置大部分安全规则的 Spring Web 应用程序中,检查此解决方案。

我已经测试了解决方案的示例安全配置:

@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 保护的 3rd 方响应 URL(如果它已启用)。

然后我们需要通过扩展GenericFilterBean类( 过滤器类对我不起作用)并通过拦截每个HttpServletRequest并设置响应头将SameSite属性设置为JSESSIONID cookie 来创建一个 HttpServletRequest 过滤器。

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 安全过滤器链并确定过滤器链中的适当位置。 除了BasicAuthenticationFilter ,在SecurityContextPersistanceFilter 之后将是另一个理想的地方。

SameSite cookie 属性将不支持某些旧浏览器版本,在这种情况下,请检查浏览器并避免在不兼容的客户端中设置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)) {

此过滤器在 localhost 环境中不起作用,因为它需要 Secured(HTTPS) 连接来设置Secure cookie 属性。

有关详细说明,请阅读此博客文章

我之前也处于同样的情况。 由于javax.servlet.http.Cookie类中没有像SameSite这样的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();
    }
}

正如这个答案中提到的: 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();
    }
}

或者...更简单,spring boot since 2.6.0 支持在 application.properties 中设置它。

Spring 有关 SameSite 的文档 Cookies

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM