Java后端安全的第一道闸门,往往不是WAF也不是防火墙,而是你代码里那一行行看似无害的参数接收。SQL注入至今仍稳居OWASP Top 10前列,不是因为漏洞原理多高深,而是因为开发者太容易在拼接SQL时图方便。直接看一个典型反例:用户登录时,有人会写 String sql = "SELECT * FROM users WHERE username = '" + username + "' AND password = '" + password + "'"; 攻击者只要在用户名输入框填上 admin' -- ,后面的密码校验就被注释掉了,直接以管理员身份登入系统。杜绝这种漏洞的方法极其明确:永远不要拼接SQL。MyBatis里用 #{} 而非 ${},JDBC里用PreparedStatement,JPA里老老实实用参数绑定。对于必须动态传入表名或排序字段的场景,建立白名单映射表,把用户输入的值在服务端做一次硬匹配,匹配不到就拒绝请求,不给任何试错空间。

命令注入与路径穿越的硬核防护

Runtime.getRuntime().exec() 是Java里一颗定时炸弹。如果业务需要调用外部命令,比如图片处理、文档转换,直接把用户输入拼进命令字符串等于把服务器钥匙交给攻击者。一个文件名包含 ; rm -rf / 或者 && cat /etc/passwd 就能造成灾难。正确的做法是完全避免使用Runtime.exec,转向ProcessBuilder,并且将命令与参数严格分离:ProcessBuilder pb = new ProcessBuilder("convert", userInputFilename); 这里userInputFilename作为独立参数传入,不会被shell解析。更进一步的,对文件名做严格的正则过滤,只允许字母数字和下划线,长度限制在64字符以内。路径穿越同样需要零信任策略。用户下载文件时,如果URL参数是 fileName=../../etc/shadow,而你直接用 File file = new File(baseDir + fileName); 去读取,系统就沦陷了。修复方案是拿到文件对象后,立即调用 file.getCanonicalPath() 获取规范化路径,再调用 startsWith(baseDirCanonicalPath) 做前缀校验,不匹配就拦截。同时,文件下载接口永远不要接受全路径,只接受文件ID或经过映射的哈希文件名。

跨站脚本XSS的反射型与存储型双重防御

后端输出到前端的数据,只要未经转义,就是XSS的温床。很多开发者以为只有HTML页面才会被注入,实际上JSON接口返回的数据被前端innerHTML直接渲染时,同样会触发。防御策略分两层:输出编码和输入净化。在服务端模板引擎里,Thymeleaf用 th:text 而非 th:utext,JSP里用 <c:out> 而非直接EL表达式 ${},这些是基本操作。对于必须支持富文本的场景,比如用户评论,后端要做严格的HTML白名单过滤。引入OWASP Java HTML Sanitizer库,配置只允许 p, br, b, i, ul, li, a 等安全标签,所有属性除了a标签的href外全部剥离,且href必须校验协议只允许http和https。输入净化放在数据入库前执行一次,输出时再根据上下文做一次编码,形成纵深防御。特别容易被忽略的是HTTP响应头里的XSS防护,务必在拦截器或过滤器中统一设置 X-XSS-Protection: 1; mode=block 和 Content-Type 头中明确指定 charset=UTF-8,防止浏览器做字符集猜测攻击。

跨站请求伪造CSRF的无状态防护方案

CSRF攻击利用的是浏览器自动携带Cookie的特性,在用户不知情的情况下发起恶意请求。传统的同步器令牌模式在前后端分离架构下显得笨重,现代做法倾向于双重提交Cookie模式或自定义请求头校验。具体实现:用户登录成功后,后端生成一个随机Token,通过Set-Cookie写入用户浏览器,同时将这个Token的哈希值存入Redis或用户会话。前端每次发起非GET请求时,从Cookie中读取该Token,并设置到自定义请求头 X-CSRF-Token 中。后端过滤器拦截所有POST、PUT、DELETE请求,比对请求头中的Token与Cookie中的Token是否一致,并校验Redis中是否存在对应哈希。由于攻击者无法读取跨域Cookie,也无法设置自定义请求头,CSRF攻击就此失效。对于纯API服务,更彻底的方案是全面采用Authorization头承载JWT,完全抛弃Cookie认证,从根源上消除CSRF的土壤。

敏感数据保护从脱敏到加密的完整链路

日志里打印用户身份证号、手机号、银行卡号,是很多系统信息泄露的源头。安全编码规范必须强制要求,任何日志输出前,对敏感字段做脱敏处理。封装一个SensitiveDataMask工具类,手机号保留前3后4中间用星号替代,身份证保留前1后1,银行卡仅显示后四位。更关键的是传输和存储环节的加密。密码存储不用再讨论MD5或SHA1,必须用bcrypt、scrypt或Argon2这类慢哈希算法,每个密码独立生成随机盐值。对于需要双向解密的敏感数据,比如支付密钥,使用AES-256-GCM模式,密钥从环境变量或密钥管理服务获取,绝不能硬编码在配置文件中。初始化向量IV每次加密随机生成,并拼接在密文前一起存储。数据库连接字符串中的账号密码同样走配置中心加密下发,应用启动时解密加载到内存。

权限校验的垂直越权与水平越权根治

仅在菜单层做权限控制是自欺欺人。攻击者只要猜到一个接口URL,就能绕过前端菜单直接调用后端API。安全编码要求每个Controller方法在执行前,必须通过注解或切面完成权限校验。Spring Security的 @PreAuthorize("hasAuthority('USER_DELETE')") 能解决垂直越权问题,确保低权限用户无法访问高权限接口。但水平越权更隐蔽:用户A通过修改请求参数 userId=2 就能查看用户B的订单详情。根治方法是在数据查询层强制注入当前登录用户上下文。在DAO或Repository方法中,所有涉及用户数据的查询,必须追加AND user_id = #{currentUserId} 条件,这个currentUserId从SecurityContextHolder中获取,绝不接受前端传入。封装一个BaseService,在底层统一拼接租户ID和用户ID条件,让业务开发人员即使忘记加过滤条件,框架层也能兜底。

文件上传的魔鬼细节与安全配置

文件上传漏洞的利用链远比想象的深。攻击者上传一个 content-type 声明为 image/jpeg 的JSP文件,或者文件名是 test.jsp;.jpg,一旦被解析就获取了WebShell。防御要覆盖文件扩展名、内容类型、文件头和存储位置四个维度。后端拿到上传文件后,第一步读取文件头魔数,判断真实类型,JPEG是FF D8 FF,PNG是89 50 4E 47,GIF是47 49 46 38,与扩展名交叉验证。第二步用白名单限定允许的扩展名,业务需要什么就放什么,不要用黑名单。第三步将文件重命名为UUID或雪花ID,彻底丢弃原始文件名,后缀从白名单映射表中获取。第四步也是最关键的,文件存储在Web容器无法直接访问的路径,或者使用对象存储服务,应用层通过专门的下载接口以流方式输出,避免文件被直接解析执行。上传接口本身要做大小限制,在Nginx层限制请求体大小,应用层再校验一次,防止内存撑爆。

依赖库漏洞管理与供应链安全

Fastjson的反序列化漏洞、Log4j2的JNDI注入,这些血淋淋的案例告诉我们,不管理依赖库就是裸奔。项目必须集成依赖检查工具,Maven用OWASP Dependency-Check插件,Gradle用dependencyCheck任务,在CI流水线中每次构建都扫描已知CVE漏洞,设定高危漏洞阈值,一旦命中就阻断构建。同时,内部搭建私有制品仓库,代理中央仓库并做安全扫描,禁止开发人员直接从公网拉取依赖。对于反序列化这个重灾区,Jackson和Fastjson都要关闭默认类型推断,Jackson配置 ObjectMapper 禁用 enableDefaultTyping,Fastjson升级到最新版并开启SafeMode。如果业务必须接收序列化对象,改用Protocol Buffers或JSON等非二进制协议,从根源上切断反序列化攻击链。

并发竞态与业务逻辑漏洞的代码级防御

安全不止于注入和XSS,业务逻辑漏洞造成的损失往往更大。优惠券被并发请求刷走多张、账户余额被并行转账扣成负数,这些是典型的竞态条件漏洞。在扣减库存或余额的代码里,不能先查询再更新,必须用数据库行级锁或原子更新语句。写法是 UPDATE account SET balance = balance - #{amount} WHERE user_id = #{userId} AND balance >= #{amount},然后判断影响行数是否为1,为0则回滚事务并提示余额不足。对于需要强一致性的场景,在方法上加 @Transactional 并设置隔离级别为可重复读,同时用Redis分布式锁或数据库悲观锁 SELECT ... FOR UPDATE 串行化关键操作。另一个常被忽视的是幂等性缺失:用户快速点击提交按钮,同一个订单创建了多次。前端防抖只是用户体验优化,后端必须在入库前根据订单号或请求流水号做唯一约束,插入时捕获DuplicateKeyException并返回已有结果,保证一次请求只产生一次效果。

安全日志与监控的实战落地

没有日志的安全是盲人摸象。但日志不是记流水账,要记录的是安全事件:登录失败、权限拒绝、敏感操作、数据导出、异常输入。格式上采用结构化JSON输出,包含时间戳、用户ID、IP地址、操作类型、请求参数、响应状态码和耗时。关键字段做脱敏后记录,IP地址记录X-Forwarded-For链的最左端真实IP,同时记录代理链完整信息用于溯源。日志实时输出到标准输出,由日志收集器统一采集进入Elasticsearch,配置告警规则:同一IP一分钟内登录失败超过5次触发账号爆破告警,非工作时间大量数据导出触发数据泄露告警。应用内部还要埋下异常监控点,全局异常处理器在捕获到未授权异常、SQL语法异常、反序列化异常时,除了返回通用错误信息外,要单独输出WARN级别日志,因为这些往往是攻击探测行为。

安全编码规范的落地与持续集成

再好的规范如果只停留在文档里,就只是一纸空文。落地需要三管齐下:IDE插件、代码审查卡点和自动化测试。在IDE中集成SpotBugs和SonarLint,并导入自定义安全规则集,开发阶段就实时提示SQL注入和路径穿越风险。代码提交时,GitLab或Gerrit的pre-receive钩子调用静态代码扫描,发现高危模式直接拒绝推送。CI流水线中加入SAST工具,对每次构建生成安全报告,安全团队定期审计。同时,编写安全单元测试用例,模拟XSS攻击载荷、SQL注入字符串、超大文件上传,验证防护逻辑是否生效。每季度组织一次安全编码的对抗演练,用实际攻击案例让开发人员加深理解。安全不是安全团队一个部门的责任,而是每一行代码提交时开发者的肌肉记忆。