且构网

分享程序员开发的那些事...
且构网 - 分享程序员编程开发的那些事

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

更新时间:2023-01-31 22:50:19


UPDATE 于 06/07/2021 - 添加了正确的 Path 属性和新的 sameSite 属性以避免会话 cookie 与 GenericFilterBean 重复方法.


UPDATE on 06/07/2021 - Added correct Path attribute with new sameSite attributes to avoid session cookie duplication with GenericFilterBean approach.

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

I was able to come up with my own solution for this.

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

I have two kinds of applications which run on Spring boot which has different Spring security configurations and they needed different solutions to fix this.

案例 1:无用户身份验证

解决方案 1

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

In here you might have created an endpoint for the 3rd party response, in your application. You are safe until you access httpSession in a controller method. If you are accessing session in different controller method then send a temporary redirect request to there like follows.

@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.

Still, you need to allow the 3rd_party_response_url in your security configuration.

解决方案 2

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

You can try the same GenericFilterBean approach described below.

案例 2:用户需要进行身份验证/登录

在您通过 HttpSecurityWebSecurity,检查这个解决方案.

In a Spring Web application where you have configured most of your security rules either through HttpSecurity or WebSecurity, check this solution.

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

Sample security config which I have tested the solution:

@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(如果它已启用).

The Important points which I want to highlight in this configuration are you should allow the 3rd party response URL from Spring Security and CSRF protection(if it's enabled).

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

Then we need to create a HttpServletRequest Filter by extending GenericFilterBean class (Filter class did not work for me) and setting the SameSite Attributes to the JSESSIONID cookie by intercepting each HttpServletRequest and setting the response headers.

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 过滤器链中

Then add this filter to the Spring Security filter chain by

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

为了确定需要在Spring的安全过滤器链中放置新过滤器的位置,您可以调试 Spring 安全过滤器链,并在过滤器中识别正确的位置链.除了 BasicAuthenticationFilter,在 SecurityContextPersistanceFilter 将是另一个理想的地方.

in order to determine where you need to place the new filter in Spring’s security filter chain, you can debug the Spring security filter chain easily and identify a proper location in the filter chain. Apart from the BasicAuthenticationFilter, after the SecurityContextPersistanceFilter would be an another ideal place.

SameSite cookie 属性将不支持某些 旧的浏览器版本,在这种情况下,请检查浏览器并避免在不兼容的客户端中设置 SameSite.

This SameSite cookie attribute will not support some old browser versions and in that case, check the browser and avoid setting SameSite in incompatible clients.

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中添加上面的检查,如下所示,

Add above check in SessionCookieFilter like follows,

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

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

This filter won't work in localhost environments as it requires a Secured(HTTPS) connection to set Secure cookie attribute.

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

For a detailed explanation read this blog post.