簡體   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