Java防CSRF流程如何统一

wen java案例 33

本文目录导读:

Java防CSRF流程如何统一

  1. 核心思路:统一拦截器 + 统一Token管理
  2. 方案一:基于Spring Security(最推荐,全自动统一)
  3. 方案二:自定义统一Filter(手动实现,通用性强)
  4. 方案三:前后端分离(SPA + API)
  5. 统一方案核心对比
  6. 关键注意事项(统一流程的坑)

在Java Web应用中统一防CSRF(跨站请求伪造),核心思路是将Token的生成、校验、更新机制封装到最顶层的框架或过滤器(Filter)/拦截器(Interceptor)中,从而对业务代码透明。

以下是实现统一防CSRF流程的几种主流方案及具体实现步骤:

核心思路:统一拦截器 + 统一Token管理

不再让每个后端接口手动校验Token,而是通过一个统一的全局过滤器(Filter)Spring MVC拦截器(HandlerInterceptor)来完成。


基于Spring Security(最推荐,全自动统一)

如果你的项目使用了Spring Security,它内置了完备的CSRF防护,且对开发者是透明的,这是最标准的统一方式。

统一流程:

  1. 开启CSRF保护:Spring Security默认开启。HttpSecurity.csrf().
  2. 自动注入Token:Spring Security会自动在每一个GET请求的响应页面(或通过_csrf属性)中注入一个_csrf token。
  3. 统一校验:对于所有POSTPUTDELETE等非安全方法,Spring Security的CsrfFilter自动拦截,并从请求参数或头中提取_csrf值,与Session中的Token比对。
  4. 失败处理:如果校验失败,自动抛出AccessDeniedException,返回403或自定义错误页面。

对业务代码的影响: 业务逻辑中不需要写任何校验代码,只需在表单中加入:

<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}" />

或者在前端JS的请求头中统一设置:

// 从Cookie或meta标签读取CSRF Token
const token = document.querySelector('meta[name="_csrf"]').content;
fetch('/api/data', {
  method: 'POST',
  headers: { 'X-CSRF-TOKEN': token }
});

优点:零侵入,天然统一,与Spring生态深度集成。


自定义统一Filter(手动实现,通用性强)

适用于不使用Spring Security的场景(如纯Servlet、Spring MVC + Shiro等)。

统一流程设计:

定义统一Filter

创建一个CsrfFilter,实现javax.servlet.Filter

核心逻辑(在doFilter中)

public class UnifiedCsrfFilter implements Filter {
    @Override
    public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) 
            throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) resp;
        HttpSession session = request.getSession();
        // 步骤A:统一为所有GET/POST初始化Token
        if (session.getAttribute("CSRF_TOKEN") == null) {
            session.setAttribute("CSRF_TOKEN", UUID.randomUUID().toString());
        }
        // 步骤B:只拦截需要保护的HTTP方法(如POST、PUT、DELETE)
        String method = request.getMethod();
        if ("POST".equalsIgnoreCase(method) || "PUT".equalsIgnoreCase(method) 
            || "DELETE".equalsIgnoreCase(method)) {
            // 从请求中获取Token(支持从Header或Parameter获取)
            String clientToken = request.getHeader("X-CSRF-Token");
            if (clientToken == null) {
                clientToken = request.getParameter("_csrf");
            }
            String serverToken = (String) session.getAttribute("CSRF_TOKEN");
            // 校验:Token不存在 或 不匹配
            if (clientToken == null || !clientToken.equals(serverToken)) {
                // 返回403 Forbidden
                response.sendError(HttpServletResponse.SC_FORBIDDEN, "CSRF Token无效或缺失");
                return; // 阻止请求继续
            }
            // 可选:校验成功后立即刷新Token(增强安全性)
            session.setAttribute("CSRF_TOKEN", UUID.randomUUID().toString());
        }
        // 步骤C:放行合法请求
        chain.doFilter(request, response);
    }
}

注册Filter到全局

web.xml或Spring Boot的@Bean中注册,确保对所有URL生效()。

前端配合(统一注入Token)

  • Form表单:在页面渲染时,统一在全局布局模板中加入:
    <input type="hidden" name="_csrf" value="${session.getAttribute('CSRF_TOKEN')}" />
  • Ajax请求:在前端全局拦截器中(如axios、jQuery的ajaxSend),统一从Cookie或隐藏域读取,并设置请求头:
    // 假设通过meta标签或某个JS变量获取
    let csrfToken = document.querySelector('meta[name="csrf-token"]').content;
    // 全局设置
    axios.defaults.headers.common['X-CSRF-Token'] = csrfToken;

前后端分离(SPA + API)

对于前后端完全分离的项目,通常使用JWT替代Session,此时CSRF防护的逻辑变为:

统一流程: 不使用Session Token防CSRF,改用自定义请求头 + CORS策略

  1. 禁用Session:服务端不生成CSRF Token。
  2. 验证来源(替代方案):
    • 检查Origin/Referer:在统一Filter中检查请求的Origin头是否在允许的白名单内。
    • 双重Cookie认证(Double Submit Cookie):服务端生成一个随机Token,同时写入Cookie(SameSite=LaxStrict)并作为隐藏请求参数返回,前端发起请求时,从Cookie读取Token并放入Header,服务端校验Cookie中的值与Header中的值是否一致。
    • SameSite Cookie:设置Cookie属性SameSite=StrictLax,浏览器会自动阻止跨站请求携带Cookie,从根本上防御CSRF。

统一代码示例(检查Origin):

public class SameOriginFilter implements Filter {
    private static final List<String> ALLOWED_ORIGINS = Arrays.asList("https://yourdomain.com");
    @Override
    public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) {
        HttpServletRequest request = (HttpServletRequest) req;
        String origin = request.getHeader("Origin");
        String method = request.getMethod();
        // 只对写操作检查
        if ("POST,PUT,DELETE".contains(method)) {
            if (origin == null || !ALLOWED_ORIGINS.contains(origin)) {
                response.sendError(403);
                return;
            }
        }
        chain.doFilter(request, response);
    }
}

统一方案核心对比

方案 统一程度 侵入性 适用场景 推荐指数
Spring Security ★★★★★ 几乎0 任何Spring Boot/Cloud项目 ⭐⭐⭐⭐⭐
自定义Filter ★★★★☆ 低,需修改前端模板 非Spring生态或自定义需求 ⭐⭐⭐⭐
前后端分离 ★★★★☆ 中,需前端配合Header SPA + API ⭐⭐⭐

关键注意事项(统一流程的坑)

  1. GET请求与CSRF不要为GET请求校验CSRF Token(不影响状态变更,且容易破坏正常访问)。
  2. Token刷新策略:如果每个POST/登录后都刷新Token,可能会造成用户体验问题(用户回到上个页面时Token已失效),建议只在登录/登出时刷新。
  3. 排除特定URL:统一流程中,需要白名单例外(如/webhook/callback等来自三方服务的回调接口),可以在Filter配置中添加excludePaths列表。
  4. 防止Token被窃取:确保Token只通过HTTPS传输,且不使用URL参数传递(优先使用Header)。
  5. REST API特殊情况:对于非浏览器客户端(如App、Postman),可以选择禁用CSRF或允许自定义Header绕过。

最好的统一方式是方案一(Spring Security),它帮你把生成、校验、更新、失败处理全部封装在框架内部,业务代码完全无需操心,如果你需要手动实现,方案二(自定义Filter) 是最接近“统一”目标的——所有需要保护的请求在Filter内被集中拦截校验,业务层零修改。

抱歉,评论功能暂时关闭!