ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Spring Boot Admin Server CSRF 防护完整配置指南:保护 Web UI 并放行客户端注册

Spring Boot Admin Server CSRF 防护完整配置指南:保护 Web UI 并放行客户端注册 Spring Boot Admin Server CSRF 防护完整配置指南保护 Web UI 并放行客户端注册【免费下载链接】spring-boot-adminAdmin UI for administration of spring boot applications项目地址: https://gitcode.com/gh_mirrors/sp/spring-boot-adminSpring Boot Admin Server 内置了基于 Vue 的 SPA 管理界面该界面与后端通过 REST API 交互因此在引入 Spring Security 之后必须同时配置 CSRF 防护并精确豁免客户端注册等无需令牌的端点。本文以仓库文档 30-csrf-protection.md 为骨架结合仓库源码与前端实现完整讲解 CSRF 防护的原理、Cookie 令牌仓库的搭建、豁免端点清单、Context Path 适配、测试方法与常见故障排查让你能直接复制出一套可运行的 Admin Server 安全配置。为什么 Admin Server 需要 CSRF 防护CSRF跨站请求伪造攻击利用的是浏览器自动携带 Cookie 的特性当用户登录 Admin Server 后浏览器会在访问任何站点时自动携带JSESSIONID等会话 Cookie。攻击流程通常如下用户在浏览器中登录 Admin Server获得有效会话用户随后访问一个恶意网站恶意网站向 Admin Server 发送精心构造的请求如POST /instances或执行重启操作浏览器自动带上用户的会话 Cookie若没有 CSRF 防护该请求会被当作合法用户操作执行成功。CSRF 令牌Token的防御思路是每个需要保护的请求都必须携带一个与用户会话绑定的随机令牌而恶意网站既无法读取同源策略保护下的 Cookie也无法预知令牌值因此请求会被拒绝。Spring Boot Admin Server 的架构决定了它的 CSRF 配置有两个核心诉求管理界面UI基于 JavaScript SPA需要能读取并回传令牌因此必须使用Cookie 型令牌仓库CookieCsrfTokenRepository客户端注册被监控的客户端应用通过简单的 HTTPPOST /instances注册、DELETE /instances/{id}注销它们不经过浏览器、没有会话也拿不到令牌因此这些端点必须豁免 CSRF 校验。整体数据流如下浏览器管理界面携带令牌访问 Admin Server服务端校验令牌客户端应用则以无令牌方式向/instances注册豁免端点。完整 CSRF 配置示例下面是一份完整的SecurityFilterChain配置它同时解决了 UI 静态资源放行、登录页放行、CSRF 令牌仓库选择与端点豁免问题package com.example.admin; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; import org.springframework.security.web.csrf.CookieCsrfTokenRepository; import org.springframework.security.web.csrf.CsrfTokenRequestAttributeHandler; import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher; import de.codecentric.boot.admin.server.config.AdminServerProperties; import static org.springframework.http.HttpMethod.DELETE; import static org.springframework.http.HttpMethod.POST; Configuration public class SecurityConfig { private final AdminServerProperties adminServer; public SecurityConfig(AdminServerProperties adminServer) { this.adminServer adminServer; } Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(auth - auth .requestMatchers(PathPatternRequestMatcher.withDefaults() .matcher(adminServer.path(/assets/**))) .permitAll() .requestMatchers(PathPatternRequestMatcher.withDefaults() .matcher(adminServer.path(/login))) .permitAll() .anyRequest().authenticated() ) .formLogin(formLogin - formLogin .loginPage(adminServer.path(/login)) ) .httpBasic(Customizer.withDefaults()); // 自定义 CSRF 过滤器把令牌写入可被 JavaScript 读取的 Cookie http.addFilterAfter(new CustomCsrfFilter(), BasicAuthenticationFilter.class); // CSRF 配置 http.csrf(csrf - csrf // 使用 Cookie 型令牌仓库JavaScript 可读取 .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) // 使用基于属性的令牌处理器配合自定义过滤器把令牌暴露给前端 .csrfTokenRequestHandler(new CsrfTokenRequestAttributeHandler()) // 豁免特定端点 .ignoringRequestMatchers( // 客户端注册 PathPatternRequestMatcher.withDefaults() .matcher(POST, adminServer.path(/instances)), // 客户端注销 PathPatternRequestMatcher.withDefaults() .matcher(DELETE, adminServer.path(/instances/*)), // 通知过滤器管理端点 PathPatternRequestMatcher.withDefaults() .matcher(POST, adminServer.path(/notifications/**)), PathPatternRequestMatcher.withDefaults() .matcher(DELETE, adminServer.path(/notifications/**)), // Admin Server 自身的 actuator 端点 PathPatternRequestMatcher.withDefaults() .matcher(adminServer.path(/actuator/**)) ) ); return http.build(); } }配置要点逐项解读CookieCsrfTokenRepository.withHttpOnlyFalse()把令牌存放在XSRF-TOKENCookie 中且 Cookie 不带HttpOnly标记JavaScript 才能读取到它。这是 SPA 前端能够工作的前提。CsrfTokenRequestAttributeHandlerSpring Security 6 的默认令牌请求处理器。它把令牌放入 request attribute但不会自动把它写回 Cookie——这正是需要下文自定义过滤器的原因。PathPatternRequestMatcher.withDefaults()推荐使用基于PathPattern的匹配器Spring Security 6 新增它天然支持/**通配与adminServer.path()拼接出的完整路径。adminServer.path(...)自动把spring.boot.admin.context-path配置拼接到路径前面避免手写字符串。自定义 CSRF 过滤器把令牌暴露给 JavaScriptSpring Security 6 中令牌默认存放在请求属性里前端无法直接获得。Admin Server 的 UI 是 SPA需要从 Cookie 中读取令牌。因此必须注册一个过滤器把令牌同步写入XSRF-TOKENCookiepackage com.example.admin; import java.io.IOException; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.springframework.security.web.csrf.CsrfToken; import org.springframework.web.filter.OncePerRequestFilter; import org.springframework.web.util.WebUtils; public class CustomCsrfFilter extends OncePerRequestFilter { public static final String CSRF_COOKIE_NAME XSRF-TOKEN; Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { // 从请求属性中取出 Spring Security 生成的 CSRF 令牌 CsrfToken csrf (CsrfToken) request.getAttribute(CsrfToken.class.getName()); if (csrf ! null) { Cookie cookie WebUtils.getCookie(request, CSRF_COOKIE_NAME); String token csrf.getToken(); // 若 Cookie 不存在或令牌已更新则重新写入 if (cookie null || token ! null !token.equals(cookie.getValue())) { cookie new Cookie(CSRF_COOKIE_NAME, token); cookie.setPath(/); response.addCookie(cookie); } } filterChain.doFilter(request, response); } }它的工作流程从 Spring Security 的 request attribute 中取出当前会话的CsrfToken把它写入名为XSRF-TOKEN的 CookieCookie 不带HttpOnly标记因为要由 JavaScript 读取当 Cookie 已存在且令牌未变化时不重复写入减少不必要的响应头。注册方式http.addFilterAfter(new CustomCsrfFilter(), BasicAuthenticationFilter.class)即放在认证过滤器之后、CSRF 校验过滤器之前执行。仓库的前端代码印证了这一约定sba-config.ts 中定义了csrf.headerName X-XSRF-TOKEN、parameterName _csrfaxios.ts 设置了axios.defaults.xsrfHeaderName sbaConfig.csrf.headerName并开启了withCredentials登出表单 sba-dropdown-logout-item.vue 则直接从document.cookie中读取XSRF-TOKEN并作为隐藏表单字段提交。CSRF 在 Admin Server 中的完整交互流程1. 用户打开管理界面浏览器请求首页GET / HTTP/1.1服务端响应时同时下发会话 Cookie 与 CSRF 令牌 CookieHTTP/1.1 200 OK Set-Cookie: XSRF-TOKENabc123; Path/ Set-Cookie: JSESSIONIDxyz789; Path/; HttpOnly !DOCTYPE html html.../html注意XSRF-TOKEN没有HttpOnlyJSESSIONID保持HttpOnly。2. 前端 JavaScript 发起请求Admin UI 读取XSRF-TOKENCookie并在请求头中携带令牌fetch(/instances/123/actuator/info, { method: GET, headers: { X-XSRF-TOKEN: abc123 // 来自 Cookie }, credentials: same-origin })实际发出的 HTTP 请求GET /instances/123/actuator/info HTTP/1.1 X-XSRF-TOKEN: abc123 Cookie: XSRF-TOKENabc123; JSESSIONIDxyz7893. Spring Security 校验令牌Spring Security 将请求头X-XSRF-TOKEN中的令牌与XSRF-TOKENCookie 中的令牌进行比对一致则放行不一致则返回403 Forbidden。这也是典型的Double Submit Cookie模式两个值都来自同一个 Cookie攻击者无法伪造。4. 客户端注册豁免路径客户端应用注册实例时不带任何 CSRF 令牌POST /instances HTTP/1.1 Content-Type: application/json { name: my-service, healthUrl: http://localhost:8081/actuator/health }该请求之所以能成功是因为/instances已加入ignoringRequestMatchers。从源码看注册端点定义在 InstancesController.java 的PostMapping(path /instances, consumes MediaType.APPLICATION_JSON_VALUE)注销端点在 同文件 L145 的DeleteMapping(path /instances/{id})与豁免规则一一对应。Cookie 型与 Session 型令牌仓库的取舍Spring Security 默认使用HttpSessionCsrfTokenRepository而 Admin Server 要求使用 Cookie 型仓库原因在于 UI 是 JavaScript SPA。两者对比Cookie 型SPA 推荐.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())优点JavaScript 可以直接从 Cookie 读取令牌天然适配 SPA无状态不需要服务端保存会话令牌采用 Double Submit Cookie 模式服务端无需存储。缺点Cookie 不设HttpOnly存在被 XSS 窃取的理论风险需配合严格的 CSP 与输入过滤需要自定义过滤器把令牌写入 Cookie如本文的CustomCsrfFilter。Session 型默认.csrfTokenRepository(new HttpSessionCsrfTokenRepository())优点令牌不暴露给 JavaScript安全性更高无需额外过滤器配置更简单。缺点依赖服务端会话存储与 SPA 框架配合困难前端拿不到令牌。结论Spring Boot Admin 的 UI 是 JavaScript SPA因此必须使用 Cookie 型令牌仓库这是前端 axios.ts 中xsrfHeaderName机制能够工作的前提。必须豁免的端点清单客户端注册 / 注销端点.ignoringRequestMatchers( PathPatternRequestMatcher.withDefaults() .matcher(POST, adminServer.path(/instances)), PathPatternRequestMatcher.withDefaults() .matcher(DELETE, adminServer.path(/instances/*)) )原因客户端应用通过简单的 HTTP POST/DELETE 注册与注销它们没有浏览器会话无法提供 CSRF 令牌同时也不存在被 CSRF 攻击的风险攻击面仅在浏览器会话内。Actuator 端点.ignoringRequestMatchers( PathPatternRequestMatcher.withDefaults() .matcher(adminServer.path(/actuator/**)) )原因负载均衡器、监控探针等外部工具需要访问 Admin Server 自身的健康检查等 actuator 端点这些请求无状态、无会话不存在 CSRF 风险。通知过滤器端点.ignoringRequestMatchers( PathPatternRequestMatcher.withDefaults() .matcher(POST, adminServer.path(/notifications/**)), PathPatternRequestMatcher.withDefaults() .matcher(DELETE, adminServer.path(/notifications/**)) )原因这些是 UI 之外的管理端点源码见 NotificationFilterController.java包含POST /notifications/filters与DELETE /notifications/filters/{id}由外部服务或脚本调用无法提供 CSRF 令牌。注意文档中提到的 Slack、Teams 等外部 webhook 通知回调端点/notifications出站方向由服务端主动调用通常不经过浏览器会话若你的部署确实暴露了这类入站 webhook请按实际安全模型决定是否豁免并配合 webhook 密钥等其它认证手段。Context Path 支持当 Admin Server 部署在自定义上下文路径下例如/adminspring: boot: admin: context-path: /admin必须使用adminServer.path()方法自动拼接上下文路径PathPatternRequestMatcher.withDefaults() .matcher(POST, adminServer.path(/instances))上面的匹配器会自动解析为/admin/instances。从源码看AdminServerProperties.path()的实现就是简单的字符串拼接return this.contextPath path;见 AdminServerProperties.javacontextPath来自配置项spring.boot.admin.context-path并通过PathUtils.normalizePath规范化同文件 L81-L83。这保证了你手写任意路径时上下文前缀不会漏配。用 curl 验证 CSRF 防护验证 UI 请求必须携带令牌不带令牌只有会话 Cookiecurl -X POST http://localhost:8080/applications/my-app/restart \ -H Cookie: JSESSIONIDabc123预期响应403 Forbidden携带令牌curl -X POST http://localhost:8080/applications/my-app/restart \ -H Cookie: JSESSIONIDabc123; XSRF-TOKENdef456 \ -H X-XSRF-TOKEN: def456预期响应200 OK验证客户端注册豁免路径curl -X POST http://localhost:8080/instances \ -H Content-Type: application/json \ -d { name: test-service, healthUrl: http://localhost:8081/actuator/health }预期响应201 Created无需 CSRF 令牌验证 Actuator 端点豁免路径curl http://localhost:8080/actuator/health预期响应200 OK无需 CSRF 令牌禁用 CSRF仅限开发环境仅用于本地开发/联调严禁在生产环境禁用Bean Profile(dev) public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(/* ... */) .csrf(csrf - csrf.disable()); // 禁用 CSRF return http.build(); }用 SameSite Cookie 属性增强防护CSRF 防御的纵深之一是让浏览器限制 Cookie 的跨站携带行为server: servlet: session: cookie: same-site: strict三种取值的含义与适用场景strictCookie 仅在同站请求中发送安全性最高但会影响从外部链接直接跳转时的会话恢复laxCookie 在同站请求及顶层导航如直接输入地址、点击普通链接时发送推荐兼顾安全与可用性none所有请求都携带 Cookie必须同时设置securetrue仅 HTTPS安全收益最低。推荐 Admin Server 使用lax这样用户从书签或邮件链接直接打开管理界面时仍能正常登录。常见问题排查问题 1所有请求都返回 403 Forbidden原因CSRF 令牌缺失或无效。检查步骤确认令牌 Cookie 已下发curl -i http://localhost:8080/响应中应能看到Set-Cookie: XSRF-TOKEN...。确认自定义过滤器已注册http.addFilterAfter(new CustomCsrfFilter(), BasicAuthenticationFilter.class)确认令牌仓库为 Cookie 型.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())问题 2客户端注册返回 403原因/instances端点没有加入豁免列表。解决补充ignoringRequestMatchers.csrf(csrf - csrf .ignoringRequestMatchers( PathPatternRequestMatcher.withDefaults() .matcher(POST, adminServer.path(/instances)), PathPatternRequestMatcher.withDefaults() .matcher(DELETE, adminServer.path(/instances/*)) ) )问题 3JavaScript 读不到令牌 Cookie原因Cookie 被标记为HttpOnly。解决改用withHttpOnlyFalse().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())问题 4令牌每次请求都变化这是预期行为Spring Security 会周期性轮换令牌以增强安全性。若你的场景需要更稳定的令牌可自定义仓库的 Cookie/Header 名称CookieCsrfTokenRepository repository CookieCsrfTokenRepository.withHttpOnlyFalse(); repository.setCookieName(XSRF-TOKEN); repository.setHeaderName(X-XSRF-TOKEN);问题 5配置了 context-path 后 CSRF 失效原因匹配器没有包含上下文路径前缀。解决统一使用adminServer.path()PathPatternRequestMatcher.withDefaults() .matcher(POST, adminServer.path(/instances))不要使用下面这种硬编码写法无法感知 context-pathnew AntPathRequestMatcher(/instances, POST.name())高级配置自定义令牌 Cookie / Header 名称CookieCsrfTokenRepository repository new CookieCsrfTokenRepository(); repository.setCookieName(MY-CSRF-TOKEN); repository.setHeaderName(X-MY-CSRF-TOKEN); repository.setParameterName(_csrf); repository.setCookieHttpOnly(false); http.csrf(csrf - csrf .csrfTokenRepository(repository) )同步修改CustomCsrfFilter中的常量public static final String CSRF_COOKIE_NAME MY-CSRF-TOKEN;注意若修改了 Header 名称还需同步调整前端 sba-config.ts 中的csrf.headerName否则 UI 的 axios 请求将无法携带正确令牌。按请求类型条件启用 CSRF只对浏览器类请求启用 CSRF对 API 类请求放行http.csrf(csrf - csrf .requireCsrfProtectionMatcher(request - { // 安全方法不要求令牌 String method request.getMethod(); if (GET.equals(method) || HEAD.equals(method) || TRACE.equals(method) || OPTIONS.equals(method)) { return false; } // AJAX 请求要求令牌 String header request.getHeader(X-Requested-With); if (XMLHttpRequest.equals(header)) { return true; } // JSON API 客户端不要求令牌 String accept request.getHeader(Accept); if (accept ! null accept.contains(application/json)) { return false; } // 其余按浏览器请求处理 return true; }) )注意Admin 前端 axios.ts 会为所有请求设置X-Requested-With: XMLHttpRequest请求头因此该策略与内置 UI 兼容。多 Security Filter ChainUI 与 API 分离如果你向外部暴露了无状态 API可为 API 单独建立一条关闭 CSRF 的过滤链UI 过滤链保持 CSRF 启用Configuration public class SecurityConfig { Bean Order(1) public SecurityFilterChain apiFilterChain(HttpSecurity http, AdminServerProperties adminServer) throws Exception { http .securityMatcher(adminServer.path(/api/**)) .authorizeHttpRequests(auth - auth.anyRequest().authenticated()) .httpBasic(Customizer.withDefaults()) .csrf(csrf - csrf.disable()); // API 无需 CSRF return http.build(); } Bean Order(2) public SecurityFilterChain uiFilterChain(HttpSecurity http, AdminServerProperties adminServer) throws Exception { http .authorizeHttpRequests(auth - auth.anyRequest().authenticated()) .formLogin(/* ... */) .csrf(csrf - csrf // UI 保持 CSRF .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) ); return http.build(); } }完整可运行示例以下配置同时涵盖静态资源与登录页放行、form login HTTP Basic、CSRF Cookie 仓库 自定义过滤器 端点豁免、remember-me 与内存用户package com.example.admin; import java.util.UUID; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.provisioning.InMemoryUserDetailsManager; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler; import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; import org.springframework.security.web.csrf.CookieCsrfTokenRepository; import org.springframework.security.web.csrf.CsrfTokenRequestAttributeHandler; import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher; import de.codecentric.boot.admin.server.config.AdminServerProperties; import static org.springframework.http.HttpMethod.DELETE; import static org.springframework.http.HttpMethod.POST; Configuration public class SecurityConfig { private final AdminServerProperties adminServer; public SecurityConfig(AdminServerProperties adminServer) { this.adminServer adminServer; } Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { SavedRequestAwareAuthenticationSuccessHandler successHandler new SavedRequestAwareAuthenticationSuccessHandler(); successHandler.setTargetUrlParameter(redirectTo); successHandler.setDefaultTargetUrl(adminServer.path(/)); http .authorizeHttpRequests(auth - auth .requestMatchers(PathPatternRequestMatcher.withDefaults() .matcher(adminServer.path(/assets/**))) .permitAll() .requestMatchers(PathPatternRequestMatcher.withDefaults() .matcher(adminServer.path(/login))) .permitAll() .requestMatchers(PathPatternRequestMatcher.withDefaults() .matcher(adminServer.path(/actuator/info))) .permitAll() .requestMatchers(PathPatternRequestMatcher.withDefaults() .matcher(adminServer.path(/actuator/health))) .permitAll() .anyRequest().authenticated() ) .formLogin(formLogin - formLogin .loginPage(adminServer.path(/login)) .successHandler(successHandler) ) .logout(logout - logout .logoutUrl(adminServer.path(/logout)) ) .httpBasic(Customizer.withDefaults()); http.addFilterAfter(new CustomCsrfFilter(), BasicAuthenticationFilter.class) .csrf(csrf - csrf .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) .csrfTokenRequestHandler(new CsrfTokenRequestAttributeHandler()) .ignoringRequestMatchers( PathPatternRequestMatcher.withDefaults() .matcher(POST, adminServer.path(/instances)), PathPatternRequestMatcher.withDefaults() .matcher(DELETE, adminServer.path(/instances/*)), PathPatternRequestMatcher.withDefaults() .matcher(adminServer.path(/actuator/**)) ) ); http.rememberMe(rememberMe - rememberMe .key(UUID.randomUUID().toString()) .tokenValiditySeconds(1209600) ); return http.build(); } Bean public InMemoryUserDetailsManager userDetailsService(PasswordEncoder passwordEncoder) { UserDetails user User.builder() .username(admin) .password(passwordEncoder.encode(System.getenv(ADMIN_PASSWORD))) .roles(ADMIN) .build(); return new InMemoryUserDetailsManager(user); } Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } }配套的CustomCsrfFilter使用上文给出的实现。若在application.yml中配置了spring.boot.admin.context-path例如/admin上述所有adminServer.path()调用都会自动带上前缀。最佳实践清单生产环境始终启用 CSRF不要用csrf.disable()SPA 界面使用 Cookie 型仓库withHttpOnlyFalse()并配合自定义过滤器暴露令牌只豁免必要的端点客户端注册/instances、actuator、通知过滤器管理豁免面越小越安全启用 SameSite Cookie推荐lax作为纵深防御上线前用 curl 验证UI 请求无令牌必须 403、客户端注册无令牌必须成功全程使用 HTTPS防止令牌在传输中被窃取登录后轮换会话 ID如配置会话固定防护关注日志中的 CSRF 拒绝记录及时发现攻击尝试。关联文档Server Authentication服务器认证与 Spring Security 配置Actuator Security保护客户端 actuator 端点SSRF Protection通过实例注册端点阻断 SSRF 攻击前端 CSRF 机制源码sba-config.ts、axios.ts、sba-dropdown-logout-item.vue后端注册/注销与通知过滤器端点源码InstancesController.java、NotificationFilterController.java上下文路径处理源码AdminServerProperties.java【免费下载链接】spring-boot-adminAdmin UI for administration of spring boot applications项目地址: https://gitcode.com/gh_mirrors/sp/spring-boot-admin创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表