👋 你好,欢迎来到我的博客!我是【菜鸟不学编程】
   我是一个正在奋斗中的职场码农,步入职场多年,正在从“小码农”慢慢成长为有深度、有思考的技术人。在这条不断进阶的路上,我决定记录下自己的学习与成长过程,也希望通过博客结识更多志同道合的朋友。
  
  🛠️ 主要方向包括 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/PKCS5PaddingECB 模式会泄露结构,很多安全事故都从这儿开始
  • 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)数字签名在工程里到底干嘛?

加密解决的是“别人看不懂”,签名解决的是两件事:

  1. 这东西是不是你发的(身份)
  2. 中途有没有被改过(完整性)

你可以把它理解为:

  • 加密像“上锁”
  • 签名像“盖章 + 防伪线”

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
🧵 本文原创,转载请注明出处。

更多推荐