Skip to content
第 33 章 架构 ⏱ 11 分钟阅读

第 33 章:应用安全 ​

学习目标 ​

  • 掌握依赖漏洞扫描与修复
  • 学会反序列化漏洞防护
  • 实现文件上传、API 防护
  • 避免代码注入、依赖中毒的坑

一、依赖漏洞扫描 ​

bash
# 1. OWASP Dependency-Check
mvn org.owasp:dependency-check-maven:check

# 2. Snyk
npx snyk test

# 3. Trivy
trivy image myapp:latest
xml
<!-- pom.xml 配置 OWASP -->
<plugin>
    <groupId>org.owasp</groupId>
    <artifactId>dependency-check-maven</artifactId>
    <version>9.0.0</version>
    <configuration>
        <failBuildOnCVSS>7.0</failBuildOnCVSS>
    </configuration>
</plugin>
java
// 漏洞库:Log4j 2.14.0 以前有 RCE(2021/12)
<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-core</artifactId>
    <version>2.17.1</version>  <!-- 升级到 2.17.1+ -->
</dependency>
yaml
# CI/CD 集成
stages:
  - build
  - test
  - security_scan

security_scan:
  stage: security_scan
  script:
    - mvn org.owasp:dependency-check-maven:check
    - trivy image myapp:latest
  artifacts:
    reports:
      dependency_check: 'dependency-check-report.xml'

⚠️ 坑 1:依赖漏洞无人看,Java 反序列化漏洞躺 3 年。CI/CD 集成扫描,高危漏洞阻断发布。

二、反序列化漏洞 ​

java
// ❌ 危险:ObjectInputStream 接收任意对象
public Object deserialize(byte[] bytes) throws Exception {
    ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bytes));
    return ois.readObject();  // 攻击者传入恶意对象,执行任意代码
}

// ✅ 修复:白名单 + 类型检查
public <T> T deserialize(byte[] bytes, Class<T> expected) throws Exception {
    ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bytes)) {
        @Override
        protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException {
            if (!expected.isAssignableFrom(super.resolveClass(desc))) {
                throw new InvalidClassException("Unexpected class: " + desc.getName());
            }
            return super.resolveClass(desc);
        }
    };
    return expected.cast(ois.readObject());
}
java
// ✅ 推荐:用 JSON 替代 Java 序列化
public class UserDto {
    private Long id;
    private String name;
}
ObjectMapper mapper = new ObjectMapper();
UserDto user = mapper.readValue(json, UserDto.class);

⚠️ 坑 2:Redis 用了 Java 序列化,JDK 17 默认改了序列化机制,反序列化失败或兼容性问题。统一用 JSON。

三、文件上传安全 ​

java
// 1. 类型校验
public class FileUploadValidator {
    private static final Set<String> ALLOWED_TYPES = Set.of("image/jpeg", "image/png", "application/pdf");
    private static final long MAX_SIZE = 10 * 1024 * 1024;  // 10MB

    public void validate(MultipartFile file) {
        // 1. 大小
        if (file.getSize() > MAX_SIZE) {
            throw new FileTooLargeException();
        }

        // 2. MIME 类型
        String contentType = file.getContentType();
        if (!ALLOWED_TYPES.contains(contentType)) {
            throw new UnsupportedFileTypeException();
        }

        // 3. 真实类型(魔数)
        try (InputStream is = file.getInputStream()) {
            byte[] magic = new byte[8];
            is.read(magic);
            if (!isValidImage(magic)) {
                throw new FileContentException();
            }
        }
    }

    private boolean isValidImage(byte[] magic) {
        // JPEG: FF D8 FF
        return magic[0] == (byte) 0xFF && magic[1] == (byte) 0xD8 && magic[2] == (byte) 0xFF;
    }
}
java
// 2. 文件名净化
public String sanitizeFilename(String filename) {
    // 防止 ../ 路径穿越
    String name = filename.replaceAll("[\\\\/:*?\"<>|]", "");
    // 防止脚本文件
    if (name.endsWith(".jsp") || name.endsWith(".php") || name.endsWith(".exe")) {
        throw new UnsupportedFileTypeException();
    }
    return name;
}

// 3. 存储到对象存储,不直接存应用服务器
String url = ossService.upload("user-files/" + UUID.randomUUID(), file.getBytes());

四、API 防护 ​

java
// 1. 限流
@RateLimiter(limit = 100, period = 60)
@GetMapping("/api/order/{id}")
public Order getOrder(@PathVariable Long id) {
    return orderRepository.findById(id).orElseThrow();
}

// 2. 防爬虫
@BotDetection   // 自定义注解,校验 UA / 行为
public List<Product> listProducts() {
    return productRepository.findAll();
}

public class BotDetectionAspect {
    @Around("@annotation(BotDetection)")
    public Object around(ProceedingJoinPoint pjp) throws Throwable {
        HttpServletRequest req = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
        String userAgent = req.getHeader("User-Agent");
        if (isBot(userAgent)) {
            throw new BotDetectedException();
        }
        return pjp.proceed();
    }
}
java
// 3. 重要接口加图形验证码 / 短信验证码
@PostMapping("/api/login")
public TokenDto login(@RequestBody LoginRequest req, @RequestParam String captcha) {
    if (!captchaService.verify(req.getUsername(), captcha)) {
        throw new InvalidCaptchaException();
    }
    return authService.login(req);
}

五、敏感信息保护 ​

java
// 1. 日志脱敏
public class SensitiveDataMasker {
    private static final Pattern PHONE = Pattern.compile("(\\d{3})\\d{4}(\\d{4})");
    private static final Pattern ID_CARD = Pattern.compile("(\\d{4})\\d{10}(\\w{4})");

    public static String mask(String input) {
        if (input == null) return null;
        input = PHONE.matcher(input).replaceAll("$1****$2");
        input = ID_CARD.matcher(input).replaceAll("$1**********$2");
        return input;
    }
}

// Logback 过滤器
public class MaskingFilter extends Filter<ILoggingEvent> {
    @Override
    public FilterReply decide(ILoggingEvent event) {
        String msg = event.getFormattedMessage();
        if (msg.contains("password") || msg.contains("idCard")) {
            return FilterReply.DENY;  // 整条日志丢弃
        }
        return FilterReply.NEUTRAL;
    }
}
java
// 2. 异常信息不暴露
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorDto> handleException(Exception e) {
    log.error("系统异常", e);
    return ResponseEntity.status(500).body(new ErrorDto("INTERNAL_ERROR", "系统繁忙,请稍后再试"));
    // ❌ 不要返回 e.getMessage(),可能暴露 SQL、文件名
}
java
// 3. 配置文件脱敏
@Value("${spring.datasource.password}")
private String dbPassword;

@Configuration
public class SensitiveConfig {
    @Bean
    public SensitiveDataMessageConverter sensitiveConverter() {
        return new SensitiveDataMessageConverter();
    }
}

⚠️ 坑 3:日志打印 SQL:select * from user where password = '123456',密码明文入日志。参数化日志 + 脱敏中间件。

六、容器与镜像安全 ​

dockerfile
# 1. 最小化镜像
FROM eclipse-temurin:17-jre-alpine
# 不要用 latest,固定版本

# 2. 非 root 用户
RUN adduser -D appuser
USER appuser

# 3. 复制文件
COPY --chown=appuser:appuser app.jar /app/

# 4. 启动
ENTRYPOINT ["java", "-jar", "/app/app.jar"]
yaml
# 5. 镜像扫描
stages:
  - build
  - scan
  - deploy

scan:
  stage: scan
  script:
    - trivy image --severity HIGH,CRITICAL myapp:latest
  allow_failure: false

七、依赖锁定 ​

xml
<!-- pom.xml 锁定版本 -->
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.16.0</version>
</dependency>
bash
# npm 锁定
npm ci   # 用 package-lock.json
yaml
# 私有源
<repositories>
    <repository>
        <id>private</id>
        <url>https://nexus.example.com/repository/maven-public/</url>
    </repository>
</repositories>

八、CSP 跨域 ​

java
// 1. CORS 严格限制
@Configuration
public class CorsConfig {
    @Bean
    public CorsFilter corsFilter() {
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        CorsConfiguration config = new CorsConfiguration();
        config.setAllowedOrigins(List.of("https://app.example.com"));  // 不用 "*"
        config.setAllowedMethods(List.of("GET", "POST"));
        config.setAllowedHeaders(List.of("Authorization", "Content-Type"));
        config.setAllowCredentials(true);
        source.registerCorsConfiguration("/**", config);
        return new CorsFilter(source);
    }
}

// 2. CSP
http.headers(csp -> csp.policyDirectives(
    "default-src 'self'; " +
    "script-src 'self' 'nonce-${nonce}'; " +
    "style-src 'self' 'nonce-${nonce}'; " +
    "img-src 'self' data:; " +
    "object-src 'none'"
));

九、安全扫描工具 ​

bash
# 1. SAST(静态分析)
mvn sonar:sonar

# 2. DAST(动态分析)
zap-cli quick-scan https://app.example.com

# 3. 密钥扫描
gitleaks detect

# 4. 容器扫描
trivy image myapp:latest

十、安全开发规范 ​

text
1. 所有输入必须校验(白名单优于黑名单)
2. 所有输出必须转义(防 XSS)
3. 敏感信息不写日志(密码、密钥、身份证)
4. 错误信息不暴露内部细节
5. 第三方依赖必须扫描
6. 容器以非 root 运行
7. 密钥独立存储,定期轮换

⚠️ 坑 4:开发规范文档躺在 Wiki,没人看。CI 流水线 + Lint 工具,把规范变成自动检查。

本章小结 ​

维度措施
依赖漏洞扫描 + 锁定版本
序列化替代方案(JSON)
上传类型 + 大小 + 真实内容
API限流 + 验证码
容器非 root + 镜像扫描
工具用途
OWASP Dependency-Check依赖漏洞
Snyk多语言依赖
Trivy容器扫描
SonarQube代码质量
Gitleaks密钥扫描

动手练习 ​

  1. 依赖扫描:用 OWASP Dependency-Check 扫描项目,修复高危漏洞
  2. 反序列化:把 Java 序列化改为 JSON,避免反序列化漏洞
  3. 文件上传:实现校验:类型/大小/魔数,存储到 OSS
  4. 日志脱敏:用 Logback Filter 屏蔽密码字段

下一章:第 34 章:审计与合规 →

本站基于 VitePress 构建 · 由 StackHub 团队维护