Java CSRF防护案例如何开发

wen java案例 27

本文目录导读:

Java CSRF防护案例如何开发

  1. 什么是CSRF攻击
  2. 完整的CSRF防护实现案例
  3. 使用Spring Security内置CSRF防护
  4. 最佳实践建议
  5. 测试CSRF防护

我来为你详细介绍Java CSRF防护的完整案例开发。

什么是CSRF攻击

CSRF(Cross-Site Request Forgery)跨站请求伪造,攻击者诱导用户点击链接,在用户不知情的情况下执行恶意操作。

完整的CSRF防护实现案例

1 项目结构

src/main/java/com/example/csrf/
├── config/
│   └── SecurityConfig.java
├── controller/
│   └── TransferController.java
├── util/
│   └── CSRFTokenUtil.java
├── filter/
│   └── CSRFValidationFilter.java
└── Application.java

2 核心工具类 - Token生成与管理

package com.example.csrf.util;
import java.security.SecureRandom;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
public class CSRFTokenUtil {
    private static final String CSRF_TOKEN_KEY = "csrf_token";
    private static final SecureRandom random = new SecureRandom();
    /**
     * 生成CSRF Token
     */
    public static String generateToken() {
        byte[] tokenBytes = new byte[32];
        random.nextBytes(tokenBytes);
        return Base64.getUrlEncoder().withoutPadding().encodeToString(tokenBytes);
    }
    /**
     * 存储Token到Session
     */
    public static void storeToken(HttpSession session, String token) {
        Map<String, String> tokenMap = (Map<String, String>) session.getAttribute(CSRF_TOKEN_KEY);
        if (tokenMap == null) {
            tokenMap = new HashMap<>();
            session.setAttribute(CSRF_TOKEN_KEY, tokenMap);
        }
        // 存储在session中,key可以是token哈希值或唯一标识
        tokenMap.put(token, token);
    }
    /**
     * 验证Token
     */
    public static boolean validateToken(HttpServletRequest request, String token) {
        HttpSession session = request.getSession(false);
        if (session == null) {
            return false;
        }
        Map<String, String> tokenMap = (Map<String, String>) session.getAttribute(CSRF_TOKEN_KEY);
        if (tokenMap == null || !tokenMap.containsKey(token)) {
            return false;
        }
        // 验证成功后移除已使用的token(一次性)
        tokenMap.remove(token);
        return true;
    }
    /**
     * 从请求中获取Token
     */
    public static String getTokenFromRequest(HttpServletRequest request) {
        // 先从Header获取
        String token = request.getHeader("X-CSRF-Token");
        if (token == null || token.isEmpty()) {
            // 再从请求参数获取
            token = request.getParameter("_csrf");
        }
        return token;
    }
}

3 CSRF过滤器

package com.example.csrf.filter;
import com.example.csrf.util.CSRFTokenUtil;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
public class CSRFValidationFilter implements Filter {
    // 排除CSRF验证的路径
    private static final Set<String> EXCLUDED_PATHS = new HashSet<>(
        Arrays.asList("/login", "/logout", "/api/public", "/error")
    );
    // 需要CSRF保护的HTTP方法
    private static final Set<String> PROTECTED_METHODS = new HashSet<>(
        Arrays.asList("POST", "PUT", "DELETE", "PATCH")
    );
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        // 初始化操作
    }
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, 
                         FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        HttpServletResponse response = (HttpServletResponse) servletResponse;
        String requestURI = request.getRequestURI();
        String method = request.getMethod();
        // 检查是否需要CSRF保护
        if (shouldValidateCSRF(requestURI, method)) {
            String token = CSRFTokenUtil.getTokenFromRequest(request);
            if (token == null || !CSRFTokenUtil.validateToken(request, token)) {
                // Token验证失败
                response.setStatus(HttpServletResponse.SC_FORBIDDEN);
                response.setContentType("application/json");
                response.getWriter().write("{\"error\":\"CSRF token validation failed\"}");
                return;
            }
        }
        filterChain.doFilter(request, response);
    }
    private boolean shouldValidateCSRF(String requestURI, String method) {
        // 排除特定路径
        for (String excludedPath : EXCLUDED_PATHS) {
            if (requestURI.startsWith(excludedPath)) {
                return false;
            }
        }
        // 仅对修改操作进行验证
        return PROTECTED_METHODS.contains(method.toUpperCase());
    }
    @Override
    public void destroy() {
        // 清理操作
    }
}

4 安全配置

package com.example.csrf.config;
import com.example.csrf.filter.CSRFValidationFilter;
import com.example.csrf.util.CSRFTokenUtil;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
@Configuration
public class SecurityConfig implements WebMvcConfigurer {
    /**
     * 注册CSRF过滤器
     */
    @Bean
    public FilterRegistrationBean<CSRFValidationFilter> csrfFilterRegistration() {
        FilterRegistrationBean<CSRFValidationFilter> registration = new FilterRegistrationBean<>();
        registration.setFilter(new CSRFValidationFilter());
        registration.addUrlPatterns("/*");
        registration.setName("csrfValidationFilter");
        registration.setOrder(1);
        return registration;
    }
    /**
     * 生成并注入CSRF Token到响应
     */
    public static void injectCSRFToken(HttpServletRequest request, HttpServletResponse response) {
        HttpSession session = request.getSession();
        String token = CSRFTokenUtil.generateToken();
        CSRFTokenUtil.storeToken(session, token);
        // 在响应头中添加Token
        response.setHeader("X-CSRF-Token", token);
        // 也可以在Cookie中设置(但需要注意Cookie的安全性)
        // Cookie cookie = new Cookie("csrf_token", token);
        // cookie.setHttpOnly(true);
        // cookie.setSecure(true);
        // response.addCookie(cookie);
    }
}

5 控制器示例

package com.example.csrf.controller;
import com.example.csrf.config.SecurityConfig;
import com.example.csrf.util.CSRFTokenUtil;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Controller
public class TransferController {
    /**
     * 转账页面 - 生成并传递CSRF Token
     */
    @GetMapping("/transfer")
    public String transferPage(HttpServletRequest request, 
                               HttpServletResponse response, 
                               Model model) {
        // 生成并注入CSRF Token
        SecurityConfig.injectCSRFToken(request, response);
        // 从session获取当前token
        String token = (String) request.getSession().getAttribute("csrf_token");
        model.addAttribute("csrfToken", token);
        return "transfer";
    }
    /**
     * 转账操作 - 需要CSRF Token验证
     */
    @PostMapping("/transfer")
    @ResponseBody
    public String processTransfer(@RequestParam String amount,
                                  @RequestParam String toAccount,
                                  HttpServletRequest request) {
        // 实际业务逻辑
        return "Transfer completed successfully";
    }
    /**
     * 获取新的CSRF Token(用于AJAX请求)
     */
    @GetMapping("/api/csrf-token")
    @ResponseBody
    public String getCsrfToken(HttpServletRequest request, 
                               HttpServletResponse response) {
        SecurityConfig.injectCSRFToken(request, response);
        String token = (String) request.getSession().getAttribute("csrf_token");
        return token;
    }
}

6 前端HTML模板

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">转账页面</title>
    <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
</head>
<body>
    <h2>转账</h2>
    <!-- 表单方式 -->
    <form id="transferForm" action="/transfer" method="POST">
        <input type="hidden" name="_csrf" th:value="${csrfToken}" />
        <div>
            <label>金额:</label>
            <input type="text" name="amount" required />
        </div>
        <div>
            <label>收款账户:</label>
            <input type="text" name="toAccount" required />
        </div>
        <button type="submit">提交转账</button>
    </form>
    <!-- AJAX方式 -->
    <button onclick="submitTransfer()">AJAX转账</button>
    <script>
    // 设置全局AJAX CSRF Token
    $(document).ready(function() {
        var csrfToken = $('input[name="_csrf"]').val();
        $(document).ajaxSend(function(e, xhr, options) {
            if (options.type.toUpperCase() === 'POST' || 
                options.type.toUpperCase() === 'PUT' ||
                options.type.toUpperCase() === 'DELETE') {
                xhr.setRequestHeader('X-CSRF-Token', csrfToken);
            }
        });
    });
    function submitTransfer() {
        $.ajax({
            url: '/transfer',
            type: 'POST',
            data: {
                amount: 1000,
                toAccount: '123456789'
            },
            success: function(response) {
                alert('转账成功');
                // 刷新Token
                refreshCSRFToken();
            },
            error: function(xhr) {
                if (xhr.status === 403) {
                    alert('CSRF验证失败');
                    refreshCSRFToken();
                }
            }
        });
    }
    function refreshCSRFToken() {
        $.get('/api/csrf-token', function(token) {
            $('input[name="_csrf"]').val(token);
        });
    }
    </script>
</body>
</html>

7 Spring Boot入口

package com.example.csrf;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

使用Spring Security内置CSRF防护

如果使用Spring Security,可以直接使用其内置的CSRF防护:

package com.example.csrf.config;
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.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
public class SpringSecurityConfig {
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .csrf(csrf -> csrf
                .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
                .ignoringRequestMatchers("/api/public/**")
                .csrfTokenRequestHandler(new CsrfTokenRequestAttributeHandler())
            )
            .authorizeHttpRequests(authz -> authz
                .requestMatchers("/login", "/logout", "/api/public/**").permitAll()
                .anyRequest().authenticated()
            )
            .formLogin(form -> form
                .loginPage("/login")
                .permitAll()
            );
        return http.build();
    }
}

最佳实践建议

1 Token安全性

  • 使用强随机数生成器
  • Token应具有足够长度(至少32字节)
  • 限制Token有效期
  • 每次使用后刷新Token

2 Cookie安全配置

Cookie cookie = new Cookie("csrf_token", token);
cookie.setHttpOnly(true);  // 防止XSS读取
cookie.setSecure(true);    // 仅HTTPS传输
cookie.setSameSite("Strict");  // 防止跨站请求
cookie.setPath("/");
cookie.setMaxAge(3600);    // 1小时过期

3 双重验证

// 验证Referer和Origin头部
public boolean validateReferer(HttpServletRequest request) {
    String referer = request.getHeader("Referer");
    String origin = request.getHeader("Origin");
    if (referer != null && !referer.startsWith("https://yourdomain.com")) {
        return false;
    }
    if (origin != null && !origin.equals("https://yourdomain.com")) {
        return false;
    }
    return true;
}

4 日志记录

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class CSRFValidationFilter extends OncePerRequestFilter {
    private static final Logger logger = LoggerFactory.getLogger(CSRFValidationFilter.class);
    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                   HttpServletResponse response,
                                   FilterChain filterChain) {
        try {
            // CSRF验证逻辑
            if (!validateCSRF(request)) {
                logger.warn("CSRF validation failed for request: {} from IP: {}", 
                           request.getRequestURI(), 
                           request.getRemoteAddr());
                response.sendError(HttpServletResponse.SC_FORBIDDEN, "CSRF validation failed");
                return;
            }
            filterChain.doFilter(request, response);
        } catch (Exception e) {
            logger.error("CSRF filter error", e);
        }
    }
}

测试CSRF防护

package com.example.csrf;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.web.servlet.MockMvc;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
@SpringBootTest
@AutoConfigureMockMvc
public class CSRFProtectionTest {
    @Autowired
    private MockMvc mockMvc;
    @Test
    public void testTransferWithoutCSRFToken() throws Exception {
        // 测试无CSRF Token的请求应被拒绝
        mockMvc.perform(post("/transfer")
                .param("amount", "1000")
                .param("toAccount", "123456"))
               .andExpect(status().isForbidden());
    }
    @Test
    public void testTransferWithInvalidCSRFToken() throws Exception {
        // 测试无效CSRF Token应被拒绝
        mockMvc.perform(post("/transfer")
                .header("X-CSRF-Token", "invalid-token")
                .param("amount", "1000")
                .param("toAccount", "123456"))
               .andExpect(status().isForbidden());
    }
    @Test
    public void testTransferWithValidCSRFToken() throws Exception {
        // 测试有效CSRF Token应成功
        // 这里需要先获取有效的Token
        String token = fetchCSRFToken();
        mockMvc.perform(post("/transfer")
                .header("X-CSRF-Token", token)
                .param("amount", "1000")
                .param("toAccount", "123456"))
               .andExpect(status().isOk());
    }
    private String fetchCSRFToken() throws Exception {
        // 从/token接口获取有效的CSRF Token
        var result = mockMvc.perform(get("/api/csrf-token"))
                            .andExpect(status().isOk())
                            .andReturn();
        return result.getResponse().getContentAsString();
    }
}

这个完整的案例涵盖了CSRF防护的主要实现方式,包括自定义实现和Spring Security集成方案,建议根据项目需求选择合适的实现方式。

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