浏览器关闭后持久化Spring BFF架构SESSION与X-CSRF Cookie方案咨询
场景说明
搭建了一套采用BFF(Backend for Frontend)架构的NextJS应用,基于Spring Security OAuth2 Client、Spring Cloud Gateway对接Spring Authorization Server,实现逻辑与公开的gateway-demo示例高度相似。
当前应用运行状态良好:从BFF端获取的SESSION和X-CSRF令牌会以Cookie形式写入NextJS应用所在浏览器,功能链路无异常。但存在问题:关闭浏览器窗口后会话立即失效,原因是两类Cookie的MAX-AGE属性均被设置为Session(会话级生命周期)。
已知安全最佳实践是保持该默认配置,让会话随服务端超时或浏览器关闭自动失效,但出于技术调研目的,需要梳理关闭浏览器后持久化SESSION与X-CSRF Cookie的正确实现方案。
补充环境说明:当前BFF与Spring Authorization Server均集成Spring Session,基于Redis存储会话数据,二者使用不同的Redis命名空间。
待确认问题
- 是否仅需在BFF与Spring Authorization Server两端将Cookie的MAX-AGE设置为固定时长即可实现持久化?
- 是否需要启用Spring Security Remember Me功能?当前BFF采用WebFlux Security,并不支持该功能。
- X-CSRF Cookie是否需要和会话Cookie一样,配置为浏览器关闭后仍持久化存储?
- 服务端会话超时时间是否需要与两类Cookie设置的MAX-AGE时长保持一致?
- 如果部署多实例BFF节点,是否需要将X-CSRF令牌持久化到数据库中?
- 额外疑问:登录流程在Spring Authorization Server完成,但用户在BFF侧也会因持有SESSION、X-CSRF令牌处于登录状态;虽然浏览器只会收到BFF下发的Cookie,但两个应用都会生成自身的会话Cookie,是否两端的会话配置需要保持完全一致?
现有配置
BFF端Spring Security配置
@Bean public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) { http .authorizeExchange(authorizeExchange -> authorizeExchange.anyExchange().authenticated() ) .exceptionHandling(exceptionHandling -> exceptionHandling.authenticationEntryPoint(authenticationEntryPoint()) ) .csrf(csrf -> csrf.csrfTokenRepository(csrfTokenRepository()) ) .cors(Customizer.withDefaults()) .oauth2Login(oauth2 -> oauth2.authenticationSuccessHandler(authenticationSuccessHandler()) ) .logout(logout -> logout .logoutHandler(logoutHandler()) .logoutSuccessHandler(logoutSuccessHandler()) ) .oauth2Client(Customizer.withDefaults()); return http.build(); }
Spring Authorization Server端安全配置
// AuthorizationServerConfig class @Bean @Order(Ordered.HIGHEST_PRECEDENCE) public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception { OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http); return http.cors(Customizer.withDefaults()) .formLogin(Customizer.withDefaults()) .build(); } // WebSecurityConfig class @Bean SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception { http .authorizeRequests(authorizeRequests -> authorizeRequests.anyRequest().authenticated() ) .cors(withDefaults()) .formLogin(withDefaults()); return http.build(); }
方案解答
问题1:仅设置两端Cookie MAX-AGE是否可实现持久化
不是。仅修改Cookie MAX-AGE只能保证浏览器侧不主动删除Cookie,服务端侧两个核心配置必须同步调整,否则持久化完全不生效:
- Spring Session默认会话过期时间为30分钟,需要同步修改对应Redis命名空间下的会话最大存活时长,否则浏览器携带Cookie发起请求时,服务端已删除对应会话,依然会判定为未登录。
- Spring Authorization Server颁发的访问令牌、刷新令牌过期时间需要和BFF侧会话时长匹配,否则BFF会话仍有效,但持有的令牌已过期,请求资源服务时会直接鉴权失败。
另外注意:登录流程完成后,用户不会再跳转至Spring Authorization Server域名,因此不需要修改授权服务器的Cookie MAX-AGE。浏览器仅存储对应域名下的Cookie,用户访问NextJS业务域名时不会携带授权服务器的Cookie,该部分Cookie仅在授权登录跳转流程中生效。
问题2:是否需要启用Remember Me功能
不需要。Remember Me的核心逻辑是会话Cookie过期后,通过额外持久化的令牌自动完成二次登录,当前需求是直接延长现有会话生命周期,而非会话过期后自动续登,完全不需要启用该功能。
且WebFlux Security无原生Remember Me支持,自定义实现成本极高,没有必要为了这个需求做额外扩展。
问题3:X-CSRF Cookie是否需要配置持久化
需要。X-CSRF令牌与用户会话强绑定,如果仅持久化会话Cookie,X-CSRF Cookie仍为会话级生命周期,关闭浏览器后X-CSRF Cookie被清除,后续所有需要CSRF校验的POST/PUT/DELETE等请求都会被直接拦截,业务链路无法跑通。
配置时注意保持X-CSRF Cookie的httpOnly属性为关闭状态,否则前端无法读取令牌值,无法将其放入请求头传递给后端完成校验。
问题4:服务端会话超时是否需要与Cookie MAX-AGE保持一致
建议保持一致,最多让服务端会话超时时间比Cookie MAX-AGE长1~2分钟做网络请求容错即可。
如果Cookie仍在有效期但服务端会话已过期,用户打开页面会无征兆跳转登录,体验极差;如果服务端会话时长远大于Cookie MAX-AGE,浏览器侧已经清除Cookie,服务端还会留存大量无效会话数据,白白占用Redis存储空间。
问题5:多实例BFF部署是否需要将X-CSRF令牌持久化到数据库
不需要。已经集成Spring Session基于Redis存储会话,直接将CSRF令牌存入会话即可,多实例部署时所有节点从Redis拉取同一份会话数据,自然能获取到对应的CSRF令牌,不需要单独持久化到数据库。
默认提供的WebSessionServerCsrfTokenRepository本身就是将CSRF令牌存储在WebSession中,只要没有自定义修改为Cookie存储模式,多实例场景下天然支持,无需额外配置。
额外疑问:两端会话配置是否需要完全一致
不需要完全一致。如前所述,浏览器仅会携带当前访问域名下的Cookie,授权服务器的Cookie仅在跳转至授权服务器完成登录、授权确认流程时才会被携带,正常业务访问过程中不会传递到BFF侧。
只需要保证BFF侧的会话过期时间、Cookie MAX-AGE、持有的OAuth2令牌过期时间三者匹配即可。授权服务器侧的会话超时、Cookie生命周期可以保持默认配置,比如默认30分钟过期即可。即便用户关闭浏览器后授权服务器侧会话失效,只要BFF持有的访问令牌未过期,业务访问完全不受影响,只有后续需要重新跳转至授权服务器登录时,才会要求用户重新输入账号密码。
内容的提问来源于stack exchange,提问作者bojackhorseman99

