后端Java安全编码的漏洞绝大多数不是框架缺陷,而是开发者对输入边界、序列化行为、权限模型和日志习惯的失控。很多攻击之所以能穿透生产环境,是因为代码里留下了看似无害的“捷径”——拼接SQL、直接反序列化客户端对象、用日志记录敏感字段、把异常堆栈返回给前端。要真正规避程序漏洞,必须从这些高频风险点入手,把安全约束写进编码动作本身,而不是等渗透测试后再修补。

输入校验必须发生在控制器入口,而不是服务层深处

Java后端最常见的漏洞入口是参数校验滞后。很多项目在Controller里拿到DTO后直接传给Service,在业务逻辑执行到一半才发现字段非法,此时数据库连接可能已经建立,甚至部分写操作已经提交。正确做法是在Controller方法签名上使用@Valid配合JSR-303注解,对字符串长度、数值范围、正则格式做第一道拦截。对于集合类型参数,要限制最大元素数量,防止攻击者传入超大JSON数组导致内存耗尽。对于文件上传接口,除了校验扩展名,还必须校验文件头魔数和实际内容类型,不能信任Content-Type请求头。所有来自客户端的ID、状态码、排序字段如果参与数据库查询或文件路径拼接,必须做白名单校验,而不是黑名单过滤。

SQL注入的根因不是拼接,而是把数据当成了代码

MyBatis的${}和JDBC的Statement是SQL注入的高发区。即使项目中使用了预编译,只要有一处${}引用用户输入,整个数据边界就被击穿。安全编码规范应当明确禁止在SQL映射文件中使用${}处理任何外部输入,排序字段、表名、列名这类无法使用#{}的场景,必须通过枚举映射或服务端白名单转换。对于动态SQL中的<if>标签,要警惕攻击者通过构造特殊参数改变SQL结构,例如传入一个非预期的条件分支值导致绕过权限过滤。更隐蔽的风险是Like查询中的通配符注入,用户输入%或_可能造成全表扫描,需要在拼接前对通配符进行转义。所有数据库账号应遵循最小权限原则,应用连接账号不授予DDL权限,避免SQL注入后被用于删表或提权。

反序列化漏洞的防御重点是拒绝未知类型

Java原生反序列化是历史最悠久也最危险的安全问题之一。如果业务中必须接收序列化对象,绝不能直接调用ObjectInputStream的readObject方法。安全做法是自定义ObjectInputStream子类,重写resolveClass方法,只允许反序列化白名单内的类。对于RMI、JMX、JNDI等涉及远程对象加载的组件,要禁用远程代码库加载,并严格限制JNDI的lookup地址为受信任的内部服务。很多现代应用已经转向JSON或Protobuf作为数据交换格式,但Jackson、Fastjson等库同样存在多态类型绑定风险。启用Default Typing或autoType相当于重新打开了反序列化攻击面,生产环境必须关闭这些特性,或者通过自定义类型解析器严格限制可实例化的类列表。

权限校验不能只停留在URL层面

基于URL的拦截器只能防住未登录访问,防不住水平越权和垂直越权。一个用户通过修改请求体中的userId或orderId,就可能读取或修改他人的数据。安全编码规范要求每个涉及资源访问的Service方法都必须校验当前登录用户对该资源的归属关系,而不是依赖前端隐藏按钮。对于角色权限,要在方法级使用@PreAuthorize注解或自定义AOP切面,明确声明该方法需要的最小权限。数据权限更复杂,涉及部门、区域、租户等维度时,需要在SQL查询层统一注入过滤条件,避免每个开发者自行实现导致遗漏。特别要注意的是,批量操作接口不能只校验列表中的第一个ID,必须对每一个被操作的资源ID做归属校验。

敏感信息脱敏要从日志和异常处理两端同时下手

生产环境的日志经常成为数据泄露的源头。密码、身份证号、银行卡号、手机号、Token、SessionId这些字段一旦被完整打印,就等于把敏感数据暴露给了所有能查看日志系统的人。安全编码规范应要求对实体类的toString方法进行脱敏处理,或者使用日志框架的MessageConverter统一过滤。在记录异常日志时,不能使用e.printStackTrace(),而应通过log.error输出,并控制堆栈信息不包含请求参数中的敏感值。全局异常处理器返回给前端的错误信息必须区分内部错误和用户可感知错误,SQL异常、文件路径、类名、内部IP这些信息绝不能出现在响应体中。对于调试级别的日志,要确保生产环境默认关闭,避免通过修改日志级别临时排查问题时忘记恢复。

文件操作和路径穿越的防御核心是规范化路径

任何涉及文件读写、下载、删除的功能,如果文件名或路径来自用户输入,都必须先通过Path.normalize()规范化,再校验规范化后的路径是否仍然位于允许的根目录内。不能只做字符串包含判断,因为攻击者可以使用../、..\、URL编码、Unicode编码等方式绕过。对于文件下载接口,建议使用服务端生成的唯一文件ID映射到实际存储路径,而不是直接接收客户端传入的文件名。上传文件时,除了限制大小和类型,还要对文件内容进行病毒扫描或至少做格式解析验证。临时文件要存放在非Web可访问目录,使用完毕后及时删除,避免被利用为WebShell载体。

加密和哈希的误用比不使用更危险

密码存储必须使用专门的慢哈希算法,如bcrypt、scrypt或Argon2,不能使用MD5、SHA-1或单轮SHA-256。即使加了盐,快速哈希算法也无法抵御GPU暴力破解。对于需要双向加密的敏感数据,要使用AES-GCM或AES-CBC加HMAC的模式,密钥必须通过密钥管理服务获取,不能硬编码在配置文件或源码中。初始化向量IV必须每次加密随机生成,不能固定。对于API签名和Token验证,要使用HMAC-SHA256或RSA签名,不能使用简单的MD5拼接。随机数生成必须使用SecureRandom,不能使用java.util.Random,后者在安全场景下是可预测的。

并发和竞态条件造成的逻辑漏洞需要显式加锁

涉及库存扣减、余额转账、优惠券领取、抽奖次数限制等场景,如果只是先查询再更新,在高并发下必然出现超卖或重复领取。安全编码规范要求这类操作必须使用数据库行级锁、乐观锁版本号或Redis原子操作来保证一致性。对于防止重复提交,可以使用幂等键加唯一索引,或者使用Redis的SETNX实现分布式锁。需要注意的是,锁的粒度要足够小,避免长时间持有锁造成性能瓶颈。同时要处理锁超时和死锁问题,设置合理的等待时间和自动释放机制。

第三方依赖的安全管理是持续过程而非一次性任务

Java项目的依赖树往往非常庞大,一个底层库的漏洞可能影响所有上层应用。安全编码规范应要求使用依赖检查工具在构建阶段扫描已知漏洞,并设置漏洞等级阈值,超过阈值的构建直接失败。对于不再维护的依赖,要评估迁移方案。私有仓库中的内部构件同样需要定期扫描,不能因为是自己开发的就放松要求。在引入新依赖时,要关注其许可证、维护活跃度、社区安全记录,避免引入功能强大但攻击面过大的库。对于框架的自动配置功能,要审查默认开启的端点和管理接口,关闭不需要的actuator、h2-console、swagger等暴露面。

安全编码规范的落地需要自动化工具配合

规范如果只停留在文档层面,很快就会被遗忘。有效做法是将安全规则集成到CI/CD流水线中,使用静态代码分析工具在代码提交时自动检测SQL注入、路径穿越、硬编码密钥、不安全反序列化等问题。对于高危规则,设置阻断合并请求的机制。代码评审清单中应包含安全检查项,评审人需要确认新增接口的权限校验、输入校验、日志脱敏是否到位。定期进行安全编码培训和漏洞复盘,把真实生产事件拆解成具体代码案例,比泛泛讲解安全理论更有警示作用。安全编码不是额外负担,而是后端Java开发的基本功,每一行代码都要对攻击者的输入保持警惕。