☰
Java项目安全整改:用户名密码明文传输的排查与修复实战
2026/10/15 17:37:44 网站建设 项目流程

从入口抓包到全链路整改:Java项目里用户名密码明文传输的排查与修复实录

先讲个我上周遇到的真实场景。某项目上线前做安全巡检,我用抓包工具在登录页随便提交了一次账号密码,明文数据直接躺在HTTP请求里,浏览器开发者工具里看得一清二楚。这还不是最离谱的,同一个系统里,修改密码接口、找回密码接口、甚至后台管理员的登录接口,全部是明文传输。我当时就问开发同事,你们平时测试的时候,是不是从来没看过请求报文?

这个场景在Java Web项目里太常见了。越核心的系统,越容易出现这种低级但致命的问题。用户密码一旦明文暴露在网络链路上,等同于把家门钥匙挂在门外,任何一个能监听网络的人都能直接拿走。这篇文章我打算完整记录一次针对性的安全整改过程,从抓包分析、根因定位,到HTTPS加密、应用层加密、日志脱敏、重放防御,以及最后的验收排查,把这一整套链路讲透。不论你用的是Spring Boot、Spring Cloud还是传统Servlet项目,思路都是通用的。

1. 明文传输的准确危害,别再说“内网没事”

1.1 明文传输到底是什么

明文传输,指客户端和服务端之间的数据交换没有经过任何加密处理,数据包里的内容以原始字符形式直接传输。登录请求里携带用户名和密码,如果这段数据是明文,网络链路上的任何中间节点都能直接读取。

我用一句话总结危害:明文传输让身份凭证暴露在网络上,攻击者不需要破解任何东西,只需要能看到数据包,就能拿到密码。这和“密文传输被逆向破解”是两码事,明文传输的安全等级约等于零。

很多开发者的第一反应是:“我的系统在内网,外网访问不到,没那么大风险。”但这个认知需要纠正。内网威胁同样真实存在:办公Wi-Fi可以被伪造,局域网内可以通过ARP欺骗劫持流量,运维跳板机可能被攻破,甚至内部员工也可能因为好奇或者恶意去监听。这些都是真实发生过的案例,我没法列出具体公司,但可以说,因为内网明文传输导致账号被撞库或者越权的安全事件,在行业里并不罕见。

1.2 明文传输的四种典型危害

我列一个风险对照表,方便你评估自己项目的暴露程度。

风险类型具体表现可能造成的后果
窃听泄露密码、身份证号、手机号在链路上被第三方截获账号被盗、隐私泄露、撞库攻击
中间人篡改攻击者截获数据包后修改内容再转发给服务端登录请求被改、转账金额被改、数据被注入
重放攻击攻击者记录完整的合法请求,之后原样重发无需知道密码也能重复登录或重复提交
信任链污染用户以为在访问正规系统,实际流量被导向伪造服务端钓鱼登录页套取大量账号密码

这里面最容易被忽视的是重放攻击。有些开发者认为就算密码泄露,攻击者拿到的也只是加密后的密文。但如果整个请求可以完整重放,攻击者根本不需要解密,他把抓到的一大段密文原封不动再发送一次,服务端照样会认为这是一个合法请求。这就是为什么加密之外还必须考虑时间戳、随机数和会话绑定。

2. 根因排查:90%的明文问题都出在这几个环节

2.1 链路层没有全站HTTPS

明文传输最直接的根源就是链路层没有启用HTTPS。很多Java项目上线时,HTTP请求仍然可以直接访问,或者HTTPS和HTTP并存,用户访问的是HTTP版本,前端代码里写的接口地址也是HTTP,密码自然就明着走了。

见过不少项目,证书配置在Nginx上,但Java应用本身的接口还能通过HTTP直接访问。运维想着“反正Nginx是入口,HTTP会自动跳HTTPS”,结果发现合同里配置的是软链,只对部分路径做了跳转,登录接口漏掉了。排查第一步,就是确认所有入口是否强制HTTPS,而且要拿真实请求验证,不能只看配置。

2.2 代码习惯里埋下的隐患

看登录接口的代码是最能发现问题的方式。不少项目的登录接口长这样:

@PostMapping("/login") public Result login(String username, String password) { // 直接处理登录逻辑 }

前端调用时,参数是直接拼在请求体里的:

$.post("/api/login", { username: username, password: password });

这类代码最大的问题不是“没有用某种加密技术”,而是整个前后端链路中没有任何一个人对敏感信息的安全传输提出质疑。前端的诉求是“把数据发出去”,后端的诉求是“收到数据做校验”,中间的安全责任被默认忽略了。

还有一个隐蔽的问题:服务端日志里打印请求参数。排查问题时图省事,在拦截器或者AOP切面里直接输出完整请求内容,密码字段就跟着日志文件一起被写入磁盘。日志文件一旦被拖走,密码也就跟着走了。

2.3 接口设计没有为安全留余地

部分系统把登录逻辑做得“过分简单”:只有一个username字段加一个password字段,没有验证码、没有设备信息、没有任何会话上下文。这种接口设计天然不利于后续做风控。即使你加了HTTPS,也仍然面临撞库和暴力破解的风险。更别提如果还是明文传输,那整个登录接口基本等同于不设防。

3. 修复第一步:链路层强制HTTPS,覆盖全部访问入口

3.1 HTTPS加密的目标和原理

先明确一个关键点:用户名密码这种敏感信息,链路层加密是第一道锁,也是最基础的一道锁。HTTPS在TCP之上增加了TLS加密层,使得数据在客户端和服务端之间传输时处于密文状态。

它的核心流程是:客户端向服务端发起握手请求,服务端返回数字证书;客户端验证证书合法性后,双方协商出会话密钥;之后所有HTTP报文内容都用会话密钥加密传输。这个过程中即便有人抓包,拿到的也只是密文,无法直接还原出用户名和密码。

3.2 Java应用自身开启HTTPS

如果Spring Boot应用直接对外提供服务,没有前置Nginx,那么可以直接在应用层开启HTTPS。首先生成证书。生产环境建议从正规CA机构申请证书;测试环境可以使用自签名证书方便联调。

下面是一个完整的配置示例。首先要准备证书文件,然后配置application.yml:

server: port: 8443 ssl: enabled: true key-store: classpath:keystore.p12 key-store-type: PKCS12 key-store-password: ${SSL_KEYSTORE_PASSWORD} key-alias: tomcat

这里有一个容易忽略的点:证书库密码不要硬编码在配置文件里,要通过环境变量占位符引用,比如上面的${SSL_KEYSTORE_PASSWORD}。我在不少项目里看到过写在application.yml里的明文证书口令,这种属于“锁装好了,钥匙挂在旁边的墙壁上”。

如果项目需要同时限制HTTP访问,可以做请求重定向。用Java配置强制跳转:

@Bean public ServletWebServerFactory servletContainer() { TomcatServletWebServerFactory factory = new TomcatServletWebServerFactory(); factory.addAdditionalTomcatConnectors(httpConnector()); return factory; } private Connector httpConnector() { Connector connector = new Connector("org.apache.coyote.http11.Http11NioProtocol"); connector.setScheme("http"); connector.setPort(8080); connector.setSecure(false); connector.setRedirectPort(8443); return connector; }

这只是一个典型的Servlet容器配置方案。实际部署中我更推荐把TLS卸载放到Nginx层做,应用层专注业务逻辑,证书更新和密钥管理都集中到网关,运维负担明显小很多。

3.3 Nginx层强制跳转HTTPS

大多数Java项目的架构是Nginx + Spring Boot,Nginx承担静态资源服务和反向代理。这种架构下,HTTPS的开启主要在Nginx层。我给出一个我常用的配置片段:

server { listen 80; server_name api.example.com; return 301 https://$host$request_uri; } server { listen 443 ssl; server_name api.example.com; ssl_certificate /etc/nginx/certs/server.crt; ssl_certificate_key /etc/nginx/certs/server.key; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers HIGH:!aNULL:!MD5; location / { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Real-IP $remote_addr; } }

配置完成后,还需要处理一个Java程序经常遇到的反向代理问题:应用收到的请求协议还是HTTP,导致Spring Security等组件在做安全判断时误判。解决方式是在Nginx里加上X-Forwarded-Proto头,然后在Spring Boot中配置:

server: forward-headers-strategy: framework

3.4 证书管理与更新

HTTPS不是配一次就能一劳永逸。证书有有效期,常见的坑是证书过期后,客户端访问时出现安全警告,部分用户直接放弃访问。更麻烦的是,Java应用内部的HTTP客户端调用其他服务时证书校验失败,接口从“偶尔通”变成“必现不通”。

生产环境建议把证书续期自动化。使用ACME协议实现证书自动申请和续期,配合Nginx的reload操作,可以做到无人工介入。关键点是设置好续期任务的定时执行时间,并及时检查证书剩余天数。我习惯加一个巡检脚本,每天检查一次证书有效期,剩余少于30天自动告警。

4. 修复第二步:应用层加密,别让数据裸奔到服务端

4.1 为什么有了HTTPS还要做应用层加密

很多开发者问过一个问题:有了HTTPS,为什么还要在应用层加密密码?这不是重复劳动。理由有两点。

第一,HTTPS只能保证传输过程加密,服务端拿到数据之后,如果日志打印、异常抛出、数据库存储不当,密码仍然会在其他环节泄露。第二,部分场景下客户端到服务端之间不止一跳,例如经过了第三方API网关、消息队列或者日志采集系统,每个中间环节都有可能接触到报文内容。应用层加密相当于给数据本身加了一层保险,即使传输链路被突破,敏感字段也不会直接暴露。

最典型的一层防护是:前端使用非对称加密的公钥对密码进行加密,后端持有私钥进行解密。即使HTTPS证书被劫持或者中间链路泄露,攻击者拿到的也只是RSA密文,没有私钥无法还原。

4.2 前端加密与后端解密的完整实现

我以一个实际用到的方式为例。前端使用公钥加密密码,后端用私钥解密,然后再进入业务校验流程。

首先在后端生成RSA密钥对,生产环境中私钥不会出现在代码仓库里,而是放在独立的密钥管理服务或者环境变量中。前端加密逻辑如下:

import JSEncrypt from 'jsencrypt'; const encryptor = new JSEncrypt(); encryptor.setPublicKey(publicKey); // publicKey由后端接口下发 const encryptedPassword = encryptor.encrypt(password);

需要说明的是,让后端接口下发公钥的做法并不推荐,如果公钥被攻击者替换,加密就形同虚设。更稳妥的做法是公钥直接内置在前端构建产物中,或者通过独立的配置中心下发。前端提交时把加密后的密码放入请求体:

$.post("/api/login", { username: username, password: encryptedPassword });

后端接收后解密。我习惯用Spring的@RequestBody配合工具类处理:

@PostMapping("/login") public Result login(@RequestBody LoginRequest request) { String username = request.getUsername(); String password = rsaUtil.decrypt(request.getEncryptedPassword()); // 后续走正常的登录校验流程 }

解密完成后,密码在处理完之后应当立即清空引用,避免长时间滞留在内存中:

char[] passwordChars = rsaUtil.decryptToCharArray(request.getEncryptedPassword()); try { // 进行登录校验 } finally { Arrays.fill(passwordChars, '\0'); }

用char[]处理密码,用完后填充清零,是Java安全编程里的一个细节。String是不可变对象,处理完无法主动清除,会一直缓存在内存里。

4.3 密码存储:明文密码绝不允许落库

就算登录请求的密文传输做得无懈可击,密码存储环节照样可能翻车。我排查过某个遗留系统,数据库里用户表的password字段直接存的是明文,运维人员登录数据库就能看到所有人的密码。更夸张的是,有一些系统的数据库备份文件被拖走后,攻击者直接拿到了完整密码本,拿去撞其他网站。

正确的做法是哈希加盐存储。哈希是单向的,无法从摘要还原出原文。但仅做哈希还不够,明文相同的用户哈希值也相同,攻击者可以通过彩虹表预计算来批量破解。加盐就是给每个用户生成一个随机字符串,拼接后再做哈希,让相同的密码得到不同的摘要。

推荐直接使用BCrypt。Spring Security内置了BCryptPasswordEncoder,使用非常方便:

@Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); }

注册用户时加密存储:

String encodedPassword = passwordEncoder.encode(rawPassword);

登录校验时使用matches方法:

boolean matches = passwordEncoder.matches(rawPassword, encodedPassword);

BCrypt的好处在于每次加密都会生成不同的盐值,而且算法内部有成本因子,可以控制计算耗时,增加暴力破解的成本。我在项目中会把成本因子从默认的10调整为12,虽然登录时略微变慢,但对破解的阻力提升明显。

4.4 非对称加密在实战中的细节取舍

非对称加密也有一堆坑。最大的坑是RSA的加密长度限制:使用1024位RSA时,最多只能加密117字节数据;使用2048位时最多245字节。如果密码字段后面还要拼接其他信息,很可能超出限制。

解决方式有两条。一是用RSA加密一个随机的对称密钥(AES密钥),再用AES加密完整的数据体,也就是常见的“混合加密”方案。这个方案实际效果更好,但实现复杂度更高,需要前后端约定好加密框架,同时管理好AES密钥的生命周期。二是干脆拆字段,只对password这种固定长度字段用RSA加密,其他字段走正常传输。

还有一种常见的错误是前后端加密方式不一致。前端用CryptoJS的AES加密,后端用Java原生API解密,结果因为填充模式、字符集、Key和IV的编码方式不同,解出来全是乱码。这个问题在后面的排错章节里我会单独展开。

5. 收尾工程:日志脱敏、接口规范与统一安全响应

5.1 日志脱敏,别让排查问题变成信息泄露

安全整改做到底层时,往往不是技术方案不够,而是日志泄露把前面所有的努力全部推翻。登录接口报错,后端日志输出:

2025-01-06 10:23:45 ERROR Login failed, username=admin, password=123456

这行日志等于把密码写在纸上放在办公室桌面上。正确做法有两种,一是统一打印参数时不打印敏感字段,二是使用脱敏工具对输出内容做替换。我推荐第一种,因为更彻底。

如果项目里用了拦截器做请求日志记录,一定要对敏感字段做过滤。我用过一个简单的脱敏规则类:

public class SensitiveFieldMasker { private static final Pattern PASSWORD_PATTERN = Pattern.compile("(\"password\"\\s*:\\s*\")([^\"]+)(\")"); private static final Pattern TOKEN_PATTERN = Pattern.compile("(\"token\"\\s*:\\s*\")([^\"]+)(\")"); public static String mask(String raw) { return PASSWORD_PATTERN.matcher(raw).replaceAll("$1******$3"); } }

在日志输出前统一走一遍SensitiveFieldMasker.mask()。工具虽小,但能防止“排查线上问题时打印完整请求体,随手把密码一起打出来”的低级失误。

5.2 统一Web层的安全响应头

除了传输加密,Web安全响应头也是容易被忽略的一部分。Java项目通常在过滤器里统一输出响应头:

@WebFilter(urlPatterns = "/*") public class SecurityHeadersFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletResponse httpResponse = (HttpServletResponse) response; httpResponse.setHeader("X-Content-Type-Options", "nosniff"); httpResponse.setHeader("X-Frame-Options", "DENY"); httpResponse.setHeader("Cache-Control", "no-store"); chain.doFilter(request, response); } }

Cache-Control: no-store这个头对登录页特别重要。不然浏览器会把包含敏感信息的页面内容缓存到本地,其他用户登录同一台机器时,可以从浏览器缓存里翻出上一任用户的登录响应。

5.3 接口防重放的基本动作

前面提过重放攻击。光靠加密和HTTPS解决不了重放问题,因为重放的是完整合法数据包。标准的防重放姿势是在请求中增加时间戳和随机数nonce,服务端校验时间戳在有效期内且nonce未被使用过。

简单实现方式如下:

public class AntiReplayService { private Cache<String, Boolean> nonceCache = new CaffeineCache("nonce", 5 * 60 * 1000); public boolean check(String nonce, long timestamp) { long now = System.currentTimeMillis(); if (Math.abs(now - timestamp) > 60 * 1000) { return false; } String key = nonce + "_" + timestamp; if (nonceCache.get(key) != null) { return false; } nonceCache.put(key, true); return true; } }

这个实现里时间窗口设为60秒,窗口之外直接拒收,窗口之内则靠nonce去重,确保同一个请求只能被服务端成功处理一次。

6. 实战向排错速查:密文乱码、证书过期、HTTPS双证书问题

6.1 一张常见问题排查表

我把这些年遇到过的高频坑整理成一个速查表,按症状、可能原因、处理方式进行说明。

症状可能原因处理方式
前端加密后,后端解密乱码前端字符集与后端字符集不一致,Base64编码未统一统一使用UTF-8,确认是否用Base64传输密文,Java端使用Base64.getDecoder()后再解密
登录接口仍然能通过HTTP访问Nginx只对部分路径做了HTTP跳转,或者未覆盖所有端口检查所有server块和监听端口,对80端口统一做301跳转
证书过期导致Java客户端调用报错证书到期未自动续期,或JDK信任库未更新更新证书链,jdk信任库中加入根证书;配置自动化续期
HTTPS握手失败,客户端提示证书无效证书链不完整,中间证书没有下发在Nginx中配置完整证书链,一般把中间证书和服务器证书合并为一个crt文件
密文长度超限,RSA解密报错待加密数据超出RSA单次加密上限改用混合加密方案,或拆分字段分别加密
登录后日志里出现完整密码拦截器或AOP打印了完整请求体日志打印前进行敏感字段脱敏,或直接排除password字段
请求数据包没有加密,但地址栏显示https页面本身是HTTPS,但接口地址写成了HTTP全局搜索前端代码中的硬编码ip/http地址,改为相对路径或统一维护API配置

6.2 一条高效的验收路径

安全整改做完后,要有一个可重复执行的验收流程,而不是只在发布前临时检查。我自己的验收清单长这样。

第一步,用抓包工具检查登录接口请求,确认请求URL是HTTPS,请求体中的password字段不是明文。如果做了应用层加密,这里应该是一段Base64密文。

第二步,重复提交一次相同的请求报文,确认服务端返回异常或拒绝处理,验证防重放逻辑生效。

第三步,查看服务端日志文件,确认登录成功和失败的日志中都不包含密码原始值,也不包含可以还原出密码的密文信息。

第四步,访问HTTP版本的登录链接,确认自动跳转到HTTPS。直接用curl验证:

curl -I http://api.example.com/login

如果响应是301或302并带有Location: https://...,说明跳转规则生效。如果返回200,说明还有HTTP入口在裸奔。

第五步,检查数据库用户表的密码字段,确认存储的是BCrypt或同类哈希摘要,而不是明文或可逆加密的密文。

这一套流程走完,再上线就放心多了。

7. 最后补一刀:安全整改的边界和长期机制

最后我再补充一点个人经验。很多安全整改不是一次性动作,而是持续对抗的过程。明明传输加密做完了,半年后某个新同事加入,为了调试方便,临时写了一个HTTP测试接口,又把密码明文打印出来了。这种问题是无法通过一次性修复根除的,只能靠机制约束。

比较好的做法是把安全要求嵌入到研发流程里。代码评审清单中增加安全项,凡是涉及用户名、密码、手机号、身份证号等敏感字段的改动,必须经过安全评审。自动化测试中加入抓包校验用例,每次构建自动检查登录请求是否携带敏感字段明文。发布前安全巡检必须包含传输加密、存储加密、日志脱敏这三个基本项。

还有一个小工具层面的建议:在Java项目里启动依赖扫描,重点检查是否引用了已经存在漏洞的加密相关依赖库。老旧的加密库可能存在算法实现缺陷,及时升级能挡掉一大波已知攻击。

我在实际项目里最深的体会是,明文传输这个问题看起来人尽皆知,但它每次都换着花样出现。有的是因为赶工期忽略了,有的是因为前端拿不到HTTPS的测试环境,干脆用HTTP联调,结果没切成HTTPS就上线了。无论哪种情况,最后都要靠一套完整的链路设计去兜底。链路层加密是地基,应用层加密是保险,日志脱敏是收尾,重放防御是补漏,四层配合才能做到传输不裸奔、存储不裸奔、日志不裸奔。希望这篇文章能把这条整改路径讲透,帮你少走一些我走过的弯路。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询