【问题标题】:Same-Site flag for session cookie in Spring SecuritySpring Security 中会话 cookie 的同站点标志
【发布时间】:2017-08-17 07:48:00
【问题描述】:

是否可以在 Spring Security 中设置Same-site Cookie 标志?

如果没有,请问是否有增加支持的路线图?一些浏览器(例如 Chrome)已经支持。

【问题讨论】:

标签: cookies spring-security csrf jsessionid samesite


【解决方案1】:

New Tomcat version 通过TomcatContextCustomizer 支持 SameSite cookie。所以你应该只自定义tomcat CookieProcessor,例如对于 Spring Boot:

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

对于SameSiteCookies.NONE,请注意,cookie 也是Secure(使用 SSL),否则无法应用。

从 Chrome 80 开始默认 cookie 被视为SameSite=Lax

请参阅 SameSite Cookie in Spring BootSameSite cookie recipes


对于 nginx 代理,可以在 nginx 配置中轻松解决:

if ($scheme = http) {
    return 301 https://$http_host$request_uri;
}

proxy_cookie_path / "/; secure; SameSite=None";

来自@madbreaks 的更新: proxy_cookie_flagsiso proxy_cookie_path

proxy_cookie_flags ~ secure samesite=none;

【讨论】:

【解决方案2】:

您可以在身份验证成功处理程序中以这种方式提及,而不是过滤器。

@Override
public void onAuthenticationSuccess(
        HttpServletRequest request, HttpServletResponse response,
        Authentication authentication) throws IOException {
    response.setStatus(HttpServletResponse.SC_OK);
    clearAuthenticationAttributes(request);
    addSameSiteCookieAttribute(response);
    handle(request, response);
}

private void addSameSiteCookieAttribute(HttpServletResponse response) {
    Collection<String> headers = response.getHeaders(HttpHeaders.SET_COOKIE);
    boolean firstHeader = true;
    // there can be multiple Set-Cookie attributes
    for (String header : headers) {
        if (firstHeader) {
            response.setHeader(HttpHeaders.SET_COOKIE,
                    String.format("%s; %s", header, "SameSite=Strict"));
            firstHeader = false;
            continue;
        }
        response.addHeader(HttpHeaders.SET_COOKIE,
                String.format("%s; %s", header, "SameSite=Strict"));
    }
}

在其中一个答案中提到过。实现后找不到链接。

【讨论】:

    【解决方案3】:

    如果您可以获得HttpServletResponse 的实例,您始终可以在Java 世界中自己设置cookie 值。

    那么你可以这样做:

    response.setHeader("Set-Cookie", "key=value; HttpOnly; SameSite=strict")
    

    在 spring-security 中,您可以使用过滤器轻松完成此操作,这是一个示例:

    public class CustomFilter extends GenericFilterBean {
        @Override
        public void doFilter(ServletRequest request, ServletResponse response,
                           FilterChain chain) throws IOException, ServletException {
            HttpServletResponse resp = (HttpServletResponse) response;
            resp.setHeader("Set-Cookie", "locale=de; HttpOnly; SameSite=strict");
            chain.doFilter(request, response);
        }
    }
    

    像这样将此过滤器添加到您的 SecurityConfig 中:

    http.addFilterAfter(new CustomFilter(), BasicAuthenticationFilter.class)
    

    或通过 XML:

    <http>
        <custom-filter after="BASIC_AUTH_FILTER" ref="myFilter" />
    </http>
    
    <beans:bean id="myFilter" class="org.bla.CustomFilter"/>
    

    【讨论】:

    • 如果你想对所有 cookie 应用这个,你可以这样做:String cookie = resp.getHeader("Set-Cookie");if (cookie != null) { resp.setHeader("Set-Cookie", cookie + "; HttpOnly; SameSite=strict");}
    【解决方案4】:

    这里所有可能的解决方案对我来说都失败了。每次尝试过滤器或拦截器时,尚未添加 Set-Cookie 标头。我能够完成这项工作的唯一方法是添加 Spring Session 并将这个 bean 添加到我的 @Configuration 文件之一中:

    @Bean
    public CookieSerializer cookieSerializer() {
        DefaultCookieSerializer serializer = new DefaultCookieSerializer();
        serializer.setSameSite("none");
        return serializer;
    }
    

    无论如何,希望这可以帮助我遇到同样情况的其他人。

    【讨论】:

      【解决方案5】:

      这是不可能的。 Spring Session 中支持此功能:https://spring.io/blog/2018/10/31/spring-session-bean-ga-released

      我想出了一个类似于 Ron 的解决方案。但有一点需要注意:

      跨站点使用的 Cookie 必须指定 SameSite=None; Secure 允许包含在第三方上下文中。

      所以我在标题中包含了 Secure 属性。此外,当您不使用它们时,您不必重写所有三种方法。仅在实现HandlerInterceptor 时才需要。

      import org.apache.commons.lang.StringUtils;
      
      public class CookiesInterceptor extends HandlerInterceptorAdapter {
          final String sameSiteAttribute = "; SameSite=None";
          final String secureAttribute = "; Secure";
      
          @Override
          public void postHandle(HttpServletRequest request, HttpServletResponse response,
                                 Object handler, ModelAndView modelAndView) throws Exception {
      
              addEtagHeader(request, response);
      
              Collection<String> setCookieHeaders = response.getHeaders(HttpHeaders.SET_COOKIE);
      
              if (setCookieHeaders == null || setCookieHeaders.isEmpty())
                  return;
      
              setCookieHeaders
                  .stream()
                  .filter(StringUtils::isNotBlank)
                  .map(header -> {
                      if (header.toLowerCase().contains("samesite")) {
                          return header;
                      } else {
                          return header.concat(sameSiteAttribute);
                      }
                  })
                  .map(header -> {
                      if (header.toLowerCase().contains("secure")) {
                          return header;
                      } else {
                          return header.concat(secureAttribute);
                      }
                  })
                  .forEach(finalHeader -> response.setHeader(HttpHeaders.SET_COOKIE, finalHeader));
          }
      }
      

      我在我的项目中使用了 xml,所以我不得不将它添加到我的配置文件中:

      <mvc:interceptors>
          <bean class="com.zoetis.widgetserver.mvc.CookiesInterceptor"/>
      </mvc:interceptors>
      

      【讨论】:

        【解决方案6】:

        在 SpringBoot 中使用拦截器。

        我正在寻找添加 SameSite 的解决方案,我只想将属性添加到现有的“Set-Cookie”,而不是创建新的“Set-Cookie”。 我尝试了几种方法来满足这个要求,包括:

        1. 如@unwichtich 所说,添加自定义过滤器,
        2. 以及更多我覆盖了 basicAuthenticationFilter。它确实添加了 SameSite 属性。虽然 Spring 添加“Set-Cookie”的时机很难把握。我认为在 onAuthenticationSuccess() 方法中,响应必须有这个头,但它没有。我不确定这是否是我的自定义 basicAuthenticationFilter 顺序的错。
        3. 使用cookieSerializer,但spring-session版本出现问题。似乎只有最新版本支持它,但我仍然无法弄清楚应该将版本号添加到依赖列表中。
          不幸的是,上面没有一个可以像预期的那样很好地添加相同的站点。

        最后,我发现spring中的interceptor可以帮我实现。 我花了一个星期才拿到它。如果有人遇到同样的问题,希望这可以帮助您。

        @Component
        public class CookieServiceInterceptor extends HandlerInterceptorAdapter {
            @Override
            public boolean preHandle(
                    HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
                return true;
            }
        
            @Override
            public void postHandle(
                    HttpServletRequest request, HttpServletResponse response, Object handler,
                    ModelAndView modelAndView) throws Exception {
                //check whether it has "set-cookie" in the response, if it has, then add "SameSite" attribute
                //it should be found in the response of the first successful login
                Collection<String> headers = response.getHeaders(HttpHeaders.SET_COOKIE);
                boolean firstHeader = true;
                for (String header : headers) { // there can be multiple Set-Cookie attributes
                    if (firstHeader) {
                        response.setHeader(HttpHeaders.SET_COOKIE, String.format("%s; %s", header, "SameSite=strict"));
                        firstHeader = false;
                        continue;
                    }
                    response.addHeader(HttpHeaders.SET_COOKIE, String.format("%s; %s", header, "SameSite=strict"));
                }
            }
        
            @Override
            public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
                                        Object handler, Exception exception) throws Exception {
            }
        }
        

        你还需要让这个拦截器在你的应用程序中工作,这意味着你应该添加一个bean,如下所示:

        @Autowired
        CookieServiceInterceptor cookieServiceInterceptor;
        
        @Bean
        public MappedInterceptor myInterceptor() {
            return new MappedInterceptor(null, cookieServiceInterceptor);
        }
        

        这个拦截器有一个缺陷,当请求被重定向(ex.return 302)或失败(ex.return 401)时,它无法添加samesite,而当SSO时它使我的应用程序失败。最终,我必须使用 Tomcat cookie,因为我没有在我的 springboot 应用程序中嵌入 tomcat。我加了

        <Context>
            <CookieProcessor sameSiteCookies="none" />
        </Context>
        

        在我的应用程序的 /META-INF 下的 context.xml 中。它将在每个响应的 set-cookie 标头中添加 SameSite 属性。请注意,从 Tomcat 9.0.21 和 8.5.42 开始,这种行为是可能的。根据https://stackoverflow.com/a/57622508/4033979

        【讨论】:

          【解决方案7】:

          对于 Spring Webflux(反应式环境),这对我有用:

          @Configuration
          @EnableSpringWebSession
          public class SessionModule {
              @Bean
              public ReactiveSessionRepository<MapSession> reactiveSessionRepository() {
                  return new ReactiveMapSessionRepository(new ConcurrentHashMap<>());
              }
          
              @Bean
              public WebSessionIdResolver webSessionIdResolver() {
                  CookieWebSessionIdResolver resolver = new CookieWebSessionIdResolver();
                  resolver.setCookieName("SESSION");
                  resolver.addCookieInitializer((builder) -> {
                      builder.path("/")
                              .httpOnly(true)
                              .secure(true)
                              .sameSite("None; Secure");
                  });
                  return resolver;
              }
          }
          

          【讨论】:

            【解决方案8】:

            您可以通过使用 ResponseCookie 自己添加 cookie 并将其添加到您的 HttpServletResponse。

            ResponseCookie cookie = ResponseCookie.from("cookiename", "cookieValue")
                        .maxAge(3600) // one hour
                        .domain("test.com")
                        .sameSite("None")
                        .secure(true)
                        .path("/")
                        .build();
             response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString());
            

            【讨论】:

              【解决方案9】:

              我已经在没有spring-security 的情况下为spring-webmvc 测试了这个解决方案,但我认为它也应该适用于spring-boot

              使用来自spring-session-coreSessionRepositoryFilter bean

              您可以使用 spring Session 扩展默认 java HttpSession 并将 JSESSIONID cookie 替换为自定义 cookie,如下所示:

              Set-Cookie: JSESSIONID=NWU4NzY4NWUtMDY3MC00Y2M1LTg1YmMtNmE1ZWJmODcxNzRj; Path=/; Secure; HttpOnly; SameSite=None
              

              额外的spring Session cookie 标志可以使用DefaultCookieSerializer 设置:

              @Configuration
              @EnableSpringHttpSession
              public class WebAppConfig implements WebApplicationInitializer {
                  @Override
                  public void onStartup(ServletContext servletContext) {
                      servletContext
                              .addFilter("sessionRepositoryFilter", DelegatingFilterProxy.class)
                              .addMappingForUrlPatterns(null, false, "/*");
                  }
              
                  @Bean
                  public MapSessionRepository sessionRepository() {
                      final Map<String, Session> sessions = new ConcurrentHashMap<>();
                      MapSessionRepository sessionRepository =
                              new MapSessionRepository(sessions) {
                                  @Override
                                  public void save(MapSession session) {
                                      sessions.entrySet().stream()
                                              .filter(entry -> entry.getValue().isExpired())
                                              .forEach(entry -> sessions.remove(entry.getKey()));
                                      super.save(session);
                                  }
                              };
                      sessionRepository.setDefaultMaxInactiveInterval(60*5);
                      return sessionRepository;
                  }
              
                  @Bean
                  public SessionRepositoryFilter<?> sessionRepositoryFilter(MapSessionRepository sessionRepository) {
                      SessionRepositoryFilter<?> sessionRepositoryFilter =
                              new SessionRepositoryFilter<>(sessionRepository);
              
                      DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();
                      cookieSerializer.setCookieName("JSESSIONID");
                      cookieSerializer.setSameSite("None");
                      cookieSerializer.setUseSecureCookie(true);
              
                      CookieHttpSessionIdResolver cookieHttpSessionIdResolver =
                              new CookieHttpSessionIdResolver();
                      cookieHttpSessionIdResolver.setCookieSerializer(cookieSerializer);
              
                      sessionRepositoryFilter.setHttpSessionIdResolver(cookieHttpSessionIdResolver);
              
                      return sessionRepositoryFilter;
                  }
              }
              

              我已经扩展了一点 MapSessionRepository 实现,因为它不支持触发 SessionDeletedEvent 或 SessionExpiredEvent - 我在添加新会话之前添加了过期会话的清除。我认为这对于小型应用程序来说可能已经足够了。

              【讨论】:

                猜你喜欢
                • 2020-02-11
                • 2017-03-17
                • 2018-11-14
                • 2014-11-03
                • 2018-04-26
                • 2011-02-06
                • 2015-02-09
                • 2011-12-29
                相关资源
                最近更新 更多