你以为 Java 安全只是“加密一下就行”?那为啥线上还是会被打穿呢?
👋 你好,欢迎来到我的博客!我是【菜鸟不学编程】
我是一个正在奋斗中的职场码农,步入职场多年,正在从“小码农”慢慢成长为有深度、有思考的技术人。在这条不断进阶的路上,我决定记录下自己的学习与成长过程,也希望通过博客结识更多志同道合的朋友。
🛠️ 主要方向包括 Java 基础、Spring 全家桶、数据库优化、项目实战等,也会分享一些踩坑经历与面试复盘,希望能为还在迷茫中的你提供一些参考。
💡 我相信:写作是一种思考的过程,分享是一种进步的方式。
如果你和我一样热爱技术、热爱成长,欢迎关注我,一起交流进步!
全文目录:
I. 安全管理器:SecurityManager 和权限检查
1)SecurityManager 是什么?(以及为什么你现在很少见到它)
SecurityManager 的思路很直白:在做“敏感操作”前先问一句:你有没有权限?
比如:读文件、开网络连接、退出 JVM、反射访问某些东西……这些都可以走一遍 checkXXX。Oracle 的文档也明确了它通过一堆 check... 方法来进行操作前的安全检查。
但现实是:它越来越“不合时宜”。OpenJDK 的 JEP 411 明确指出:Security Manager 起源很早、维护成本高,已经不再是现代 Java 安全的主要手段,并且已被弃用并为未来移除做准备。
说人话:SecurityManager 这套“JVM 内部沙箱”机制,曾经辉煌过,但时代变了。现在更常见的隔离方式是:容器、进程级隔离、权限最小化、操作系统层面的控制等,而不是在 JVM 里靠策略文件缝缝补补。
2)权限检查的样子(了解就好,别强依赖)
典型写法大概这样(示意):
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
sm.checkRead("/etc/passwd"); // 敏感文件读权限检查
}
但我建议你把它当成“历史课”:
- 维护老系统可能会遇到
- 新系统别把它当核心安全方案(因为它已经走向退场,JEP 411 已说明将为移除做准备)
II. 加密 API:Cipher、KeyGenerator 和 SecretKey
加密这块最容易出现一个“非常要命的误区”:
“我用 AES 加密了,所以就安全了。”
不不不,AES 只是算法名,真正决定你是否安全的,是:模式、填充、随机数、密钥管理、完整性校验。
1)Cipher:加密的发动机
Cipher 是 JCE 的核心类,你通过 Cipher.getInstance("算法/模式/填充") 来拿到实现。
最常见(也最容易写错)的选择对比:
- ❌
AES/ECB/PKCS5Padding:ECB 模式会泄露结构,很多安全事故都从这儿开始 - ✅
AES/GCM/NoPadding:带认证(完整性校验),更适合文件/消息加密
2)KeyGenerator & SecretKey:密钥怎么来?
如果是“随机密钥”(比如系统内部通信),可以用 KeyGenerator:
KeyGenerator kg = KeyGenerator.getInstance("AES");
kg.init(256); // 需要 JCE 支持;现代 JDK 基本默认支持
SecretKey key = kg.generateKey();
如果是“用户口令”(比如你输入一个密码去加密文件),就别直接拿密码当 AES key——那等于把门钥匙用蜡做。更稳的做法是 PBKDF2 把口令拉伸成密钥(后面项目会给完整代码)。
III. 数字签名:Signature 和证书处理
1)数字签名在工程里到底干嘛?
加密解决的是“别人看不懂”,签名解决的是两件事:
- 这东西是不是你发的(身份)
- 中途有没有被改过(完整性)
你可以把它理解为:
- 加密像“上锁”
- 签名像“盖章 + 防伪线”
2)Signature 的基本用法(以 RSA 为例)
(示意,重点在流程)
Signature sig = Signature.getInstance("SHA256withRSA");
sig.initSign(privateKey);
sig.update(data);
byte[] signature = sig.sign();
// 验证
Signature verify = Signature.getInstance("SHA256withRSA");
verify.initVerify(publicKey);
verify.update(data);
boolean ok = verify.verify(signature);
3)证书处理:X.509 证书是“公钥+身份信息”的打包件
X509Certificate 文档解释了证书由 CA 签发、用于 TLS、代码签名等,是信任链的一部分。
而 CertificateFactory 则用于从编码数据里解析出证书对象(例如从 .cer / PEM 解析)。
一个常见的“落地用法”是:你拿到对方的证书 → 提取公钥 → 验签,或者用于 TLS 的证书链验证。
IV. SSL/TLS:SSLSocket 和 HttpsURLConnection
这块我说句很“现实但刺耳”的话:
你所有的签名/加密设计,如果传输层还在 HTTP 明文跑,很多时候就白忙活。
1)HttpsURLConnection:最朴素的 HTTPS 请求
URL url = new URL("https://example.com/api");
HttpsURLConnection conn = (HttpsURLConnection) url.openConnection();
conn.setRequestMethod("GET");
int code = conn.getResponseCode();
它会使用 JVM 的信任库(trust store)来验证服务端证书。
2)SSLSocket:更底层的 TLS 通信
你可以用 SSLSocketFactory 创建 SSLSocket 来做自定义的 TLS 连接(比如自定义信任、双向认证 mTLS 等)。
工程建议:
- 普通业务请求:优先用成熟 HTTP 客户端(如 Java 11+
HttpClient)- 要玩 mTLS/自定义信任策略:再下探到 SSLContext/SSLSocket 这一层
- 不要为了“看起来很懂”就乱改证书校验,把 hostname 校验关了那种操作,真的属于“自毁长城”。
V. 常见漏洞:SQL 注入和 XSS 防范
安全不是只有“加密算法选对了”,更多事故是业务层的洞。
1)SQL 注入:别拼字符串,真的别
❌ 错误示例(别学):
String sql = "SELECT * FROM users WHERE name = '" + name + "'";
✅ 正确姿势:PreparedStatement 参数化
PreparedStatement ps = conn.prepareStatement(
"SELECT * FROM users WHERE name = ?"
);
ps.setString(1, name);
ResultSet rs = ps.executeQuery();
心里话:SQL 注入这种洞,2026 年了还在出,真的不是黑客多强,是我们太容易图省事。
2)XSS:输出前要么转义,要么做安全渲染
XSS 的核心不是“你接收了用户输入”,而是:
你把用户输入当 HTML/JS 执行了。
防范策略(按优先级):
- 默认对输出做 HTML 转义(最稳)
- 前端框架(React/Vue)默认会帮你转义,除非你使用类似
dangerouslySetInnerHTML - 如果必须渲染富文本:使用可信的 sanitizer 白名单策略(别自己写正则糊弄)
VI. 项目:实现一个安全的文件加密工具(AES-GCM + PBKDF2)
好了,重头戏来了。我们做一个“能真的用”的小工具:
- 输入:文件 + 口令(password)
- 输出:加密文件(包含 salt、iv、密文、认证标签)
- 解密时:同样口令还原文件
- 算法选择:AES/GCM/NoPadding(同时保证机密性 + 完整性)
- 口令派生:PBKDF2WithHmacSHA256(抵抗暴力破解,比直接用 password 强太多)
先讲清楚设计,不然代码你抄走也容易用错:
- salt:每次随机生成,防止彩虹表
- iv(nonce):每次随机生成,GCM 必须唯一
- 迭代次数 iterations:让暴力破解更慢
- GCM tag:自动做完整性校验,改一位都会解密失败
文件格式(我们自己定义一个简单的)
MAGIC(4) = "JENC"
VERSION(1) = 1
SALT_LEN(1)
SALT(...)
IV_LEN(1)
IV(...)
ITERATIONS(4) // int
CIPHERTEXT(...)
代码:FileCryptoTool.java(可直接运行)
运行示例:
- 加密:
java FileCryptoTool enc input.txt output.jenc "your-password"- 解密:
java FileCryptoTool dec output.jenc output.txt "your-password"
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.*;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.security.SecureRandom;
import java.util.Arrays;
public class FileCryptoTool {
private static final byte[] MAGIC = "JENC".getBytes(StandardCharsets.US_ASCII);
private static final byte VERSION = 1;
// 建议值:salt 16 bytes,GCM iv 12 bytes
private static final int SALT_LEN = 16;
private static final int IV_LEN = 12;
// PBKDF2 参数:按你项目性能调,越大越抗爆破但越慢
private static final int ITERATIONS = 120_000;
private static final int KEY_BITS = 256;
// GCM tag 长度(bits)
private static final int TAG_BITS = 128;
private static final SecureRandom RNG = new SecureRandom();
public static void main(String[] args) throws Exception {
if (args.length != 4) {
System.err.println("""
用法:
加密: java FileCryptoTool enc <inFile> <outFile> <password>
解密: java FileCryptoTool dec <inFile> <outFile> <password>
""");
System.exit(1);
}
String mode = args[0];
File in = new File(args[1]);
File out = new File(args[2]);
char[] password = args[3].toCharArray();
try {
if ("enc".equalsIgnoreCase(mode)) {
encryptFile(in, out, password);
System.out.println("加密完成 ✅ -> " + out.getAbsolutePath());
} else if ("dec".equalsIgnoreCase(mode)) {
decryptFile(in, out, password);
System.out.println("解密完成 ✅ -> " + out.getAbsolutePath());
} else {
throw new IllegalArgumentException("mode 只能是 enc 或 dec");
}
} finally {
// 尽量擦除口令(降低内存残留风险)
Arrays.fill(password, '\0');
}
}
public static void encryptFile(File in, File out, char[] password) throws IOException, GeneralSecurityException {
byte[] salt = randomBytes(SALT_LEN);
byte[] iv = randomBytes(IV_LEN);
SecretKey key = deriveKey(password, salt, ITERATIONS, KEY_BITS);
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
cipher.init(Cipher.ENCRYPT_MODE, key, new GCMParameterSpec(TAG_BITS, iv));
// 可选:把一些“元信息”作为 AAD 绑定进去(会参与认证)
cipher.updateAAD(in.getName().getBytes(StandardCharsets.UTF_8));
try (InputStream fis = new BufferedInputStream(new FileInputStream(in));
OutputStream fos = new BufferedOutputStream(new FileOutputStream(out))) {
writeHeader(fos, salt, iv, ITERATIONS);
// 流式加密
byte[] buf = new byte[64 * 1024];
int n;
while ((n = fis.read(buf)) != -1) {
byte[] enc = cipher.update(buf, 0, n);
if (enc != null && enc.length > 0) fos.write(enc);
}
byte[] finalBytes = cipher.doFinal();
fos.write(finalBytes);
}
}
public static void decryptFile(File in, File out, char[] password) throws IOException, GeneralSecurityException {
try (InputStream fis = new BufferedInputStream(new FileInputStream(in));
OutputStream fos = new BufferedOutputStream(new FileOutputStream(out))) {
Header h = readHeader(fis);
SecretKey key = deriveKey(password, h.salt, h.iterations, KEY_BITS);
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
cipher.init(Cipher.DECRYPT_MODE, key, new GCMParameterSpec(TAG_BITS, h.iv));
// 必须与加密时一致,否则认证失败
cipher.updateAAD(out.getName().getBytes(StandardCharsets.UTF_8));
byte[] buf = new byte[64 * 1024];
int n;
while ((n = fis.read(buf)) != -1) {
byte[] dec = cipher.update(buf, 0, n);
if (dec != null && dec.length > 0) fos.write(dec);
}
// 如果密文被篡改 / 密码错误,这里会抛 AEADBadTagException
byte[] finalBytes = cipher.doFinal();
fos.write(finalBytes);
}
}
private static SecretKey deriveKey(char[] password, byte[] salt, int iterations, int keyBits)
throws GeneralSecurityException {
PBEKeySpec spec = new PBEKeySpec(password, salt, iterations, keyBits);
SecretKeyFactory skf = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
byte[] keyBytes = skf.generateSecret(spec).getEncoded();
return new SecretKeySpec(keyBytes, "AES");
}
private static byte[] randomBytes(int len) {
byte[] b = new byte[len];
RNG.nextBytes(b);
return b;
}
private static void writeHeader(OutputStream out, byte[] salt, byte[] iv, int iterations) throws IOException {
out.write(MAGIC);
out.write(VERSION);
out.write((byte) salt.length);
out.write(salt);
out.write((byte) iv.length);
out.write(iv);
// iterations 用 4 字节 int 写入(大端)
out.write(ByteBuffer.allocate(4).putInt(iterations).array());
}
private static Header readHeader(InputStream in) throws IOException {
byte[] magic = in.readNBytes(4);
if (magic.length != 4 || !Arrays.equals(magic, MAGIC)) {
throw new IOException("文件格式不正确:MAGIC 不匹配");
}
int version = in.read();
if (version != VERSION) {
throw new IOException("不支持的版本: " + version);
}
int saltLen = in.read();
if (saltLen <= 0 || saltLen > 64) throw new IOException("saltLen 异常: " + saltLen);
byte[] salt = in.readNBytes(saltLen);
int ivLen = in.read();
if (ivLen <= 0 || ivLen > 32) throw new IOException("ivLen 异常: " + ivLen);
byte[] iv = in.readNBytes(ivLen);
byte[] it = in.readNBytes(4);
if (it.length != 4) throw new IOException("iterations 缺失");
int iterations = ByteBuffer.wrap(it).getInt();
return new Header(salt, iv, iterations);
}
private record Header(byte[] salt, byte[] iv, int iterations) {}
}
这个小工具“安全点”在哪?(别只看能跑)
- 用了
AES/GCM/NoPadding:除了加密,还能做完整性校验,密文被改会直接解密失败(不悄悄给你吐假数据) - 用 PBKDF2 从口令派生密钥:比“password 直接当 key”强很多(抗爆破更现实)
- salt/iv 每次随机:避免“相同输入 → 相同密文”的尴尬(这尴尬一旦出现,攻击者笑得很大声)
- header 里写了 iterations:以后你要升级迭代次数,也能兼容老文件
仍然要提醒的“工程真相”
-
口令强度依然重要:你密码是
12345678,PBKDF2 也救不了你 -
真上生产最好加:
- 失败次数限制(防暴力尝试)
- 口令输入不落盘/不进日志
- 更严格的文件权限控制
- 需要共享密钥时,改用密钥管理系统(KMS)或公钥加密封装密钥
收个尾:Java 安全的“正确心态”
安全不是“加密 API 用上了就算完”,它更像一套习惯:默认不信任输入、默认最小权限、默认可审计、默认可回滚。
你可能会嫌这些麻烦——我懂,真懂。但我也见过太多项目:功能跑得飞起,安全欠账一堆,最后不是被薅数据,就是被勒索,或者更常见:被迫停机自查。那滋味,真不浪漫。
📝 写在最后
如果你觉得这篇文章对你有帮助,或者有任何想法、建议,欢迎在评论区留言交流!你的每一个点赞 👍、收藏 ⭐、关注 ❤️,都是我持续更新的最大动力!
我是一个在代码世界里不断摸索的小码农,愿我们都能在成长的路上越走越远,越学越强!
感谢你的阅读,我们下篇文章再见~👋
✍️ 作者:某个被流“治愈”过的 Java 老兵
📅 日期:2026-01-07
🧵 本文原创,转载请注明出处。
更多推荐



所有评论(0)