diff --git a/src/programming/java/工具箱/Spring Boot JAR 瘦身与加密.md b/src/programming/java/工具箱/Spring Boot JAR 瘦身与加密.md new file mode 100644 index 0000000..e16e128 --- /dev/null +++ b/src/programming/java/工具箱/Spring Boot JAR 瘦身与加密.md @@ -0,0 +1,1225 @@ +--- +icon: bi:arrows-expand +date: 2025-05-13 +category: + - 实用工具 + - JAVA + - SpringBoot + - JAR +tag: + - JAR包瘦身 +title: Spring Boot JAR 瘦身与加密 +--- + +Spring Boot JAR 瘦身与加密:构建安全高效的部署方案 + +# Spring Boot JAR 瘦身与加密:构建安全高效的部署方案 + +在 Spring Boot 应用程序部署过程中,我们常常面临两个主要挑战: + +1. **JAR 包体积过大**:Spring Boot 应用打包时会将所有依赖一起打包,导致最终 JAR 文件臃肿 +2. **代码安全性问题**:部署到客户环境或公开场合的 JAR 包可能被反编译,造成核心业务逻辑泄露 + +为了解决这些问题,本文将介绍一套完整的解决方案,包括 JAR 包瘦身和 JAR 包加密两部分,以及配套的自定义类加载器,实现高效安全的 Spring Boot 应用部署。 + +## 整体方案设计 + +该方案由两个主要项目组成: + +1. **spring-boot-jar-slim-encrypt**:用于将 Spring Boot 应用 JAR 包瘦身和加密 +2. **spring-boot-custom-classloader**:用于加载第三方JAR + +### 工作流程 + +``` +┌────────────────────┐ +│ 原始Spring Boot │ +│ JAR包 │ +└──────────┬─────────┘ + │ + ▼ +┌────────────────────┐ ┌────────────────────┐ +│ spring-boot-jar- │ │ │ +│ slim-encrypt工具 ├───►│ 提取依赖到libs目录 │ +└──────────┬─────────┘ └────────────────────┘ + │ + ▼ +┌────────────────────┐ +│ 瘦身后的JAR包 │ +└──────────┬─────────┘ + │ + ▼ +┌────────────────────┐ +│ XJar加密处理 │ +└──────────┬─────────┘ + │ + ▼ +┌────────────────────┐ +│ 加密后的JAR包 │ +│ (.xjar) │ +└──────────┬─────────┘ + │ + │ 部署 + ▼ +┌─────────────────────────────────────────┐ +│ 运行时环境 │ +│ ┌─────────────────┐ ┌───────────────┐ │ +│ │ 加密JAR (.xjar) │ │ 提取的依赖库 │ │ +│ └────────┬────────┘ └───────┬───────┘ │ +│ │ │ │ +│ │ │ │ +│ ▼ ▼ │ +│ ┌─────────────────────────────────┐ │ +│ │ PlainTextClassLoader │ │ +│ │ (自定义类加载器) │ │ +│ └─────────────┬─────────────┬─────┘ │ +│ │ │ │ +│ ▼ ▼ │ +│ ┌─────────────────┐ ┌───────────────┐ │ +│ │ 解密JAR内容 │ │ 加载外部依赖 │ │ +│ └─────────────────┘ └───────────────┘ │ +│ │ +└─────────────────────────────────────────┘ +``` + +1. 使用 spring-boot-jar-slim-encrypt 工具提取原始 JAR 包中的依赖 +2. 将依赖库单独存储在 libs 目录 +3. 对精简后的 JAR 包进行加密 +4. 使用自定义类加载器加载外部依赖 +5. 使用xJar进行解密 + +## 项目一:spring-boot-custom-classloader + +这是一个自定义类加载器项目,它实现了类加载器: + +### 1. JarClassLoader 接口 + +```java +package com.mangmang; + +import java.io.File; + +public interface JarClassLoader { + String JAR_EXTENSION = ".jar"; + + /** + * 从指定目录加载所有JAR文件 + * + * @param jarDir 包含要加载的JAR文件的目录路径 + * @throws IllegalArgumentException 如果jarDir为null或不存在 + */ + default void loadJar(String jarDir) { + if (jarDir == null || jarDir.trim().isEmpty()) { + throw new IllegalArgumentException("JAR目录路径不能为空"); + } + + File directory = new File(jarDir); + if (!directory.exists() || !directory.isDirectory()) { + throw new IllegalArgumentException("指定路径不是有效目录: " + jarDir); + } + + File[] jarFiles = directory.listFiles(this::isJarFile); + if (jarFiles == null) { + return; + } + + for (File jarFile : jarFiles) { + System.out.println("加载 》" + jarFile.getName()); + scanJarFile(jarFile); + } + } + + /** + * 递归扫描文件或目录以查找JAR文件 + * + * @param file 要扫描的文件或目录 + * @throws IllegalArgumentException 如果file为null + */ + default void scanJarFile(File file) { + if (file == null) { + throw new IllegalArgumentException("文件不能为null"); + } + + if (!file.exists()) { + return; + } + + if (isJarFile(file)) { + addJARFile(file); + } else if (file.isDirectory()) { + File[] files = file.listFiles(); + if (files != null) { + for (File f : files) { + scanJarFile(f); + } + } + } + } + + /** + * 检查文件是否为JAR文件 + */ + default boolean isJarFile(File file) { + return file.isFile() && file.getName().endsWith(JAR_EXTENSION); + } + + /** + * 将JAR文件添加到类加载器 + * + * @param jar 要添加的JAR文件 + * @throws IllegalArgumentException 如果jar为null或不是有效的JAR文件 + */ + void addJARFile(File jar); +} +``` + +这是一个接口,定义了 JAR 文件加载的核心方法: + +- `loadJar(String jarDir)`: 从指定目录加载所有 JAR 文件 +- `scanJarFile(File file)`: 递归扫描文件或目录查找 JAR 文件 +- `isJarFile(File file)`: 检查文件是否为 JAR 文件 +- `addJARFile(File jar)`: 将 JAR 文件添加到类加载器 + +### 2. PlainTextClassLoader 实现 + +```java +package com.mangmang; + +import org.springframework.boot.context.event.ApplicationStartingEvent; +import org.springframework.context.ApplicationListener; +import org.springframework.lang.NonNull; + +import java.io.File; +import java.lang.management.ManagementFactory; +import java.lang.reflect.Method; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +/** + * 明文类加载器 + *

+ * 该类实现了自定义类加载器接口和Spring应用启动事件监听器接口。 + * 主要功能是在应用启动时,从JVM启动参数中查找指定路径的JAR文件并加载。 + * 通过反射机制将JAR文件动态添加到当前线程的类加载器中,实现运行时加载额外的类库。 + *

+ *

+ * 使用方法: + * 1. 在JVM启动参数中添加 -Dexternal.jars.path=你的JAR文件目录路径 + * 2. 系统将自动加载该目录下所有的JAR文件 + *

+ *

+ * 示例: + * java -Dexternal.jars.path.path=/path/to/jars -jar your-application.jar + *

+ */ +public class PlainTextClassLoader implements JarClassLoader, ApplicationListener { + + private final String findPath = "external.jars.path"; // 查找路径的键名 + private final ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); // 获取当前线程的类加载器 + private final List jarFiles = new ArrayList<>(); // 存储已加载的JAR文件列表 + + /** + * 构造函数 + *

+ * 在初始化时执行以下操作: + * 1. 设置当前线程的类加载器 + * 2. 输出启动日志信息 + * 3. 从JVM启动参数中检索包含"external.jars.path.path"的参数 + * 4. 提取路径值并调用loadJar方法加载指定目录下的JAR文件 + *

+ */ + public PlainTextClassLoader() { + // 设置当前线程的类加载器 + Thread.currentThread().setContextClassLoader(classLoader); + + // 打印启动信息 + System.out.println("启动自定义明文类加载器"); + + // 查找并加载外部JAR文件 + loadExternalJarsFromSystemProperties(); + } + + /** + * 从系统属性中查找并加载外部JAR文件 + */ + private void loadExternalJarsFromSystemProperties() { + List inputArguments = ManagementFactory.getRuntimeMXBean().getInputArguments(); + + // 查找包含指定路径参数的启动参数 + inputArguments.stream() + .filter(arg -> arg.contains(findPath)) + .map(this::extractPathFromArgument) + .filter(Objects::nonNull) + .forEach(this::loadJar); + } + + /** + * 从JVM参数中提取路径值 + * + * @param argument JVM启动参数 + * @return 提取的路径值,如果提取失败则返回null + */ + private String extractPathFromArgument(String argument) { + String prefix = "-D" + findPath + "="; + if (argument.startsWith(prefix)) { + String path = argument.replace(prefix, ""); + if (!path.isEmpty()) { + return path; + } + } + return null; + } + + /** + * 处理应用程序启动事件 + *

+ * 当Spring应用启动时会触发此方法。 + * 目前该方法为空实现,可以在此添加应用启动时需要执行的代码。 + *

+ * + * @param event Spring应用启动事件对象 + */ + + @Override + public void onApplicationEvent(@NonNull ApplicationStartingEvent event) { + // 应用程序启动事件的处理方法,目前为空 + } + + /** + * 将JAR文件添加到类加载器 + *

+ * 通过反射机制调用URLClassLoader的addURL方法,将指定的JAR文件URL添加到当前类加载器。 + * 添加成功后,JAR文件中的类可以被当前JVM加载和使用。 + * 同时将已加载的JAR文件记录到jarFiles列表中。 + *

+ * + * @param jar 要添加到类加载器的JAR文件对象 + * @throws RuntimeException 如果添加过程中发生任何异常,将抛出RuntimeException + */ + @Override + public void addJARFile(File jar) { + if (jar == null) { + throw new IllegalArgumentException("JAR文件不能为null"); + } + + try { + addUrlToClassLoader(jar); + jarFiles.add(jar); + System.out.println(jarFiles); + } catch (Exception e) { + throw new RuntimeException("添加JAR文件到类加载器失败: " + jar.getName(), e); + } + } + + /** + * 通过反射将JAR文件URL添加到类加载器 + * + * @param jar 要添加的JAR文件 + * @throws Exception 如果反射操作失败 + */ + private void addUrlToClassLoader(File jar) throws Exception { + Method addUrlMethod = URLClassLoader.class.getDeclaredMethod("addURL", URL.class); + if (!addUrlMethod.isAccessible()) { + addUrlMethod.setAccessible(true); + } + URL jarUrl = jar.toURI().toURL(); + addUrlMethod.invoke(classLoader, jarUrl); + } +``` + +这是一个明文类加载器,实现了 `JarClassLoader` 接口和 Spring 的 `ApplicationListener` 接口,用于在 Spring Boot 应用启动时加载外部 JAR 文件: + +主要特点: + +- 在 Spring Boot 应用启动时自动执行 +- 通过 JVM 参数 `-Dexternal.jars.path=你的JAR文件目录路径` 指定外部 JAR 文件目录 +- 使用反射机制将 JAR 文件 URL 添加到当前线程的类加载器中 + +使用示例: + +```bash +java -Dexternal.jars.path=/path/to/jars -jar your-application.jar +``` + +### 3. MANIFEAT.MF + +>src/main/resources/META-INF/spring.factories + +```text +org.springframework.context.ApplicationListener=\ +com.mangmang.PlainTextClassLoader +``` + +### 4. pom.xml + +```xml + + 4.0.0 + + spring-boot-custom-classloader + jar + + spring-boot-custom-classloader + http://maven.apache.org + + + UTF-8 + + + + + org.springframework.boot + spring-boot-starter + 2.6.7 + + + + +``` + +## 项目二:spring-boot-jar-slim-encrypt + +这个工具用于压缩和加密 Spring Boot JAR 文件,主要包含以下两个核心类: + +### 1. JarUtil 工具类 + +```java +package com.mangmang; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.io.FileUtil; +import io.xjar.XCryptos; +import io.xjar.XEncryption; +import org.apache.commons.compress.utils.IOUtils; +import org.apache.commons.compress.utils.Sets; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.util.Enumeration; +import java.util.HashSet; +import java.util.Set; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.jar.JarOutputStream; + +public class JarUtil { + /** + * 表示UTF-8字符编码的常量。 + * 该变量用于在各种文件操作中强制使用UTF-8编码, + * 确保在整个应用程序中一致地处理文本数据。 + */ + private static final String UTF_8 = "UTF-8"; + /** + * 常量BOOT_INF_LIB表示JAR文件中通常存储依赖库的默认目录路径。 + * 该路径主要用于在压缩或排除等操作中识别和处理库文件。 + */ + private static final String BOOT_INF_LIB = "BOOT-INF/lib"; + /** + * 用于标识JAR(Java归档)文件的文件扩展名。 + * 该常量表示JAR文件的标准扩展名,通常用于 + * 文件过滤、命名或在目录或归档中识别JAR文件的操作。 + */ + private static final String JAR_EXTENSION = ".jar"; + /** + * 定义在管理JAR过程中生成的需求文件的后缀, + * 特别是在处理依赖项或排除项时使用。 + * 该字符串用作特定的文件名模式,用于保存与特定服务 + * 相关的排除依赖项或其他需求的列表。 + * 默认值为"-requirements.txt"。 + */ + private static final String REQUIREMENTS_SUFFIX = "-requirements.txt"; + /** + * 预定义的、不可修改的特定jar文件名集合,被视为 + * "安全"或"始终包含"的文件。这些jar文件通常在 + * 处理或压缩操作中免于排除过滤。 + * 该集合包含以下jar标识符: + * - "spring" + * - "logback-core" + * - "tomcat" + * 该变量用于根据jar文件名决定是否包含特定jar文件的操作中。 + * 它作为应用程序关键或必要jar的白名单。 + */ + private static final Set WHITE_LIST_JARS = Sets.newHashSet("spring", "logback-core", "tomcat"); + + /** + * 通过排除和包含指定的条目来压缩给定的源JAR文件,并将结果写入目标JAR文件。 + * 它处理源JAR的条目,应用排除和包含规则,还可以将某些条目提取到指定的目录中。 + * 在此过程中创建一个临时文件,成功完成后将其重命名为目标JAR文件。 + * + * @param serviceName 正在处理的服务名称,主要用于日志记录和创建其他相关文件。 + * @param sourceJar 要压缩的源JAR文件。 + * @param includes 指定应保留哪些条目的包含模式集合。可能会自动添加额外的默认包含项。 + * @param exclusions 指定应排除哪些条目的排除模式集合。 + * @param targetJar 将写入压缩JAR的文件。 + * @param libDir 某些被排除的条目可能被提取到的目录(如适用)。 + */ + public static void compress(String serviceName, File sourceJar, Set includes, Set exclusions, File targetJar, String libDir) { + includes.addAll(WHITE_LIST_JARS); + File tempJar = new File(targetJar.getAbsolutePath() + ".tmp"); + Set excludedJars = new HashSet<>(); + + if (processJarEntries(sourceJar, tempJar, includes, exclusions, libDir, excludedJars)) { + finalizeCompression(serviceName, targetJar, tempJar, excludedJars, libDir); + } else { + boolean delete = tempJar.delete(); + System.out.println("删除临时文件:{" + delete + "}"); + } + } + +/** + * 处理源JAR文件中的条目以生成临时JAR文件, + * 同时根据包含和排除规则过滤条目。如果需要, + * 还会将指定的JAR条目提取到库目录中。 + * + * @param sourceJar 要处理的源JAR文件 + * @param tempJar 要创建的临时JAR文件 + * @param includes 定义要包含的条目的模式集合 + * @param exclusions 定义要排除的条目的模式集合 + * @param libDir 特定JAR应该被提取到的目录,如果不需要提取则为null + * @param excludedJars 用于存储被排除的JAR条目名称的集合 + * @return 如果处理成功完成则返回true,否则返回false + */ + private static boolean processJarEntries(File sourceJar, File tempJar, Set includes, + Set exclusions, String libDir, Set excludedJars) { + try (JarFile jar = new JarFile(sourceJar); + JarOutputStream tempJarStream = new JarOutputStream(Files.newOutputStream(tempJar.toPath()))) { + for (Enumeration entries = jar.entries(); entries.hasMoreElements(); ) { + JarEntry entry = entries.nextElement(); + String entryName = entry.getName(); + + if (shouldExcludeEntry(entryName, includes, exclusions)) { + if (libDir != null && !libDir.isEmpty()) { + extractJarToLib(jar, entry, libDir, excludedJars); + } + continue; + } + + copyEntryToJar(jar, entry, tempJarStream); + } + return true; + } catch (Exception ex) { + System.out.println("处理异常:" + ex.getMessage()); + return false; + } + } + + /** + * 根据预定义的标准确定是否应排除特定的jar条目。 + * 该方法评估条目是否属于BOOT-INF/lib目录,是否具有".jar"扩展名, + * 以及是否不满足由includes和exclusions集合定义的包含/排除条件。 + * + * @param entryName 要检查的jar条目名称 + * @param includes jar包含条件的集合 + * @param exclusions jar排除条件的集合 + * @return 如果应排除该条目则返回true,否则返回false + */ + private static boolean shouldExcludeEntry(String entryName, Set includes, Set exclusions) { + if (!entryName.startsWith(BOOT_INF_LIB)) { + return false; + } + String jarName = entryName.substring(entryName.lastIndexOf("/") + 1); + return jarName.endsWith(JAR_EXTENSION) && !isWhiteJar(jarName, includes, exclusions); + } + + /** + * 从JAR文件中提取指定的JAR条目到指定的库目录。 + * 如果条目对应于JAR文件且在库目录中尚不存在, + * 则将其复制到该目录,并将其名称添加到被排除的JAR集合中。 + * + * @param jar 包含要提取的条目的JAR文件 + * @param entry 要提取的JAR条目 + * @param libDir 提取的JAR文件将被复制到的目录 + * @param excludedJars 处理过程中被排除的JAR文件名的集合 + * @throws IOException 如果在从文件系统读取或写入时发生I/O错误 + */ + private static void extractJarToLib(JarFile jar, JarEntry entry, String libDir, + Set excludedJars) throws IOException { + String jarName = entry.getName().substring(entry.getName().lastIndexOf("/") + 1); + File outputFile = new File(libDir, jarName); + + if (!outputFile.exists()) { + FileUtil.touch(outputFile); + } + + try (InputStream input = jar.getInputStream(entry); + FileOutputStream output = new FileOutputStream(outputFile)) { + IOUtils.copy(input, output); + excludedJars.add(jarName); + System.out.println("Excluding: " + outputFile.getAbsolutePath()); + } + } + + /** + * 将单个{@link JarEntry}从源{@link JarFile}复制到目标{@link JarOutputStream}。 + * + * @param jar 包含要复制的条目的源{@link JarFile} + * @param entry 要复制的{@link JarEntry} + * @param output 将写入条目的目标{@link JarOutputStream} + * @throws IOException 如果在复制过程中发生I/O错误 + */ + private static void copyEntryToJar(JarFile jar, JarEntry entry, JarOutputStream output) throws IOException { + try (InputStream input = jar.getInputStream(entry)) { + output.putNextEntry(entry); + IOUtils.copy(input, output); + } + } + + /** + * 通过处理目标和临时JAR文件完成压缩过程, + * 并可选择将排除的JAR列表写入需求文件。 + * + * @param serviceName 与压缩过程关联的服务名称 + * @param targetJar 要创建或更新的目标JAR文件 + * @param tempJar 压缩过程中使用的临时JAR文件 + * @param excludedJars 压缩过程中排除的JAR文件名的集合 + * @param libDir 存储库文件的目录 + */ + private static void finalizeCompression(String serviceName, File targetJar, File tempJar, Set excludedJars, String libDir) { + boolean deleteTarget = targetJar.delete(); + System.out.println("删除目标文件结果:" + deleteTarget); + boolean rename = tempJar.renameTo(targetJar); + System.out.println("临时文件重命名结果:" + rename); + + if (CollectionUtil.isNotEmpty(excludedJars)) { + File requirementsFile = new File(libDir, serviceName + REQUIREMENTS_SUFFIX); + FileUtil.writeLines(excludedJars, requirementsFile, UTF_8); + } + } + + /** + * 确定给定的jar文件名是否匹配任何指定的包含模式 + * 且不是排除集的一部分。 + * + * @param jarName 要检查的jar文件名 + * @param includes 表示包含模式的字符串集合;如果jar名称包含 + * 这些模式中的任何一个,则被视为匹配 + * @param exclusions 表示要排除的jar名称的字符串集合;如果jar名称 + * 存在于此集合中,则被视为排除 + * @return 如果jar名称匹配任何包含模式且不是 + * 排除集的一部分,则返回{@code true},否则返回{@code false} + */ + private static boolean isWhiteJar(String jarName, Set includes, Set exclusions) { + if (exclusions.contains(jarName)) { + return false; + } + for (String include : includes) { + if (jarName.contains(include)) { + return true; + } + } + return false; + } + + /** + * 使用提供的密码将指定的原始JAR文件加密为加密的JAR文件。 + * 支持包含和排除模式,用于选择性地加密JAR中的文件条目。 + * + * @param rawFile 要加密的输入JAR文件 + * @param xjarFile 输出的加密JAR文件 + * @param pass 用于加密的密码,如果为空则生成默认密码 + * @param includes 指定要包含在加密中的文件条目的包含模式数组 + * @param excludes 指定要从加密中排除的文件条目的排除模式数组 + * @throws Exception 如果在加密过程中发生错误 + */ + public static void encrypt(File rawFile, File xjarFile, String pass, String[] includes, String[] excludes) throws Exception { + XEncryption xe = XCryptos.encryption().from(rawFile.getAbsolutePath()); + xe.use((pass == null || pass.trim().isEmpty() || pass.startsWith("默认")) ? "0755isa" : pass); + if (includes != null) { + for (String include : includes) { + xe.include(include); + } + } + if (excludes != null) { + for (String exclude : excludes) { + xe.exclude(exclude); + } + } + xe.to(xjarFile); + } +} +``` + +这个工具类提供了两个主要功能: + +#### JAR 包压缩功能 + +`compress` 方法实现了 JAR 瘦身功能: + +- 根据包含列表和排除列表过滤 JAR 中的依赖 +- 将被排除的依赖提取到指定的库目录 +- 生成一个记录排除依赖的需求文件 +- 创建一个只包含必要依赖的精简 JAR 文件 + +#### JAR 包加密功能 + +`encrypt` 方法利用 XJar 库实现了 JAR 加密: + +- 支持指定加密密码 +- 通过包含和排除模式选择性地加密 JAR 中的内容 +- 生成加密后的 XJar 文件 + +### 2. 主应用 + +```java +package com.mangmang; + +import cn.hutool.core.io.FileUtil; +import org.dom4j.Document; +import org.dom4j.Element; +import org.dom4j.io.SAXReader; +import org.xml.sax.SAXException; + +import java.io.File; +import java.util.*; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.stream.Collectors; + +/** + * + * SpringBoot JAR文件压缩和加密工具的主类。 + *

+ * 此类提供了一个基于XJar加密技术的框架,用于压缩和加密JAR文件。 + * 它支持根据XML配置文件中的规则包含或排除特定的依赖项,并将结果保存为加密的JAR文件。 + * 程序从原始JAR文件开始,根据需要压缩它们,然后应用加密。 + *

+ */ +public class SpringBootJarSlimEncryptApplication { + + /** + * 类的Logger实例,用于记录程序执行过程中的各种级别的日志信息。 + */ + private static final Logger LOGGER = Logger.getLogger(SpringBootJarSlimEncryptApplication.class.getName()); + + /** + * 包含程序所有配置常量的内部静态类。 + * 这些常量定义了输入/输出目录、加密设置和其他程序所需的各种配置参数。 + */ + private static class Config { + /** + * 存储依赖项XML文件的目录路径。 + * 默认为"./xml/",可通过系统属性"xml.dir"覆盖。 + */ + static final String DEPENDENCY_XML_DIR = System.getProperty("xml.dir", "./config/xml/"); + + /** + * 存储原始JAR文件的目录路径。 + * 默认为"./rawJars/",可通过系统属性"raw.dir"覆盖。 + */ + static final String RAW_JAR_DIR = System.getProperty("raw.dir", "./config/rawJars/"); + + /** + * 存储压缩后JAR文件的目录路径。 + * 默认为"./compressJars/",可通过系统属性"compress.dir"覆盖。 + */ + static final String COMPRESS_JAR_DIR = System.getProperty("compress.dir", "./config/compressJars/"); + + /** + * 存储提取的库文件的目录路径。 + * 默认为"./libs/",可通过系统属性"libs.dir"覆盖。 + */ + static final String LIB_DIR = System.getProperty("libs.dir", "./config/libs/"); + + /** + * 存储加密后的XJar文件的目录路径。 + * 默认为"./xJars/",可通过系统属性"xjar.dir"覆盖。 + */ + static final String X_JAR_DIR = System.getProperty("xjar.dir", "./config/xJars/"); + + /** + * 控制是否启用压缩功能的标志。 + * 默认为true,可通过系统属性"compress.enable"覆盖。 + */ + static final boolean COMPRESS_ENABLED = Boolean.parseBoolean(System.getProperty("compress.enable", "true")); + + /** + * 定义要包含在XJar加密中的文件模式数组。 + * 这些文件将在加密过程中被加密。 + */ + static final String[] X_JAR_INCLUDES = new String[]{"/com/mangmang/**", "*.yaml", "*.yml", "mapper/**.xml"}; + + /** + * 定义要从XJar加密中排除的文件模式数组。 + * 这些文件在加密过程中将保持未加密状态。 + */ + static final String[] X_JAR_EXCLUDES = new String[]{"/com/mangmang/pinyin/**"}; + + /** + * 用于X_JAR文件加密的密码。 + */ + static final String ENCRYPTION_PASSWORD = "0755isa"; + + /** + * 包含要排除的依赖项列表的XML文件的名称。 + */ + static final String EXCLUSIONS_XML = "config/exclusions.xml"; + + /** + * 包含要包含的依赖项列表的XML文件的名称。 + */ + static final String INCLUDES_XML = "config/includes.xml"; + } + /** + * 应用程序的主入口点。 + * 4. 处理所有服务,根据需要进行压缩和加密 + *

+ * + * @param args 命令行参数,当前未使用 + */ + public static void main(String[] args) { + try { + //1. 确保所有必需的目录存在 + ensureDirectoriesExist(); + //2. 查找所有原始JAR服务 + Set serviceList = findAllRawJarServices(); + //3. 从XML配置文件加载排除和包含的JAR + Set exclusionJars = loadJarsFromXml(new File(Config.DEPENDENCY_XML_DIR + File.separator + Config.EXCLUSIONS_XML)); + Set includedJars = loadJarsFromXml(new File(Config.DEPENDENCY_XML_DIR + File.separator + Config.INCLUDES_XML)); + processAllServices(serviceList, includedJars, exclusionJars); + } catch (Exception e) { + LOGGER.log(Level.SEVERE, "处理JAR文件过程中发生错误", e); + } + } + + /** + * 处理提供的服务列表,对每个服务应用包含和排除规则,然后处理它们。 + *

+ * 对于列表中的每个服务,此方法会调用{@link #processService}方法, + * 并记录任何可能发生的错误。 + *

+ * + * @param serviceList 要处理的服务名称集合 + * @param includedJars 定义要包含的JAR的规则集合 + * @param exclusionJars 定义要排除的JAR的规则集合 + */ + private static void processAllServices(Set serviceList, Set includedJars, Set exclusionJars) { + for (String service : serviceList) { + LOGGER.info("开始处理" + service); + try { + processService(service, includedJars, exclusionJars); + } catch (Exception e) { + LOGGER.log(Level.SEVERE, "处理服务 " + service + " 时发生错误", e); + } + } + } + + /** + * 处理单个服务,应用压缩(如果启用)和加密操作。 + *

+ * 根据配置,此方法将执行以下操作之一: + * - 如果启用了压缩:压缩原始JAR文件,然后加密压缩后的JAR + * - 如果禁用了压缩:直接加密原始JAR文件 + *

+ * + * @param service 要处理的服务的名称 + * @param includedJars 要包含在压缩JAR中的JAR文件集合 + * @param exclusionJars 要从压缩JAR中排除的JAR文件集合 + * @throws Exception 如果在处理过程中发生错误 + */ + private static void processService(String service, Set includedJars, Set exclusionJars) throws Exception { + File rawJarFile = new File(Config.RAW_JAR_DIR + File.separator + service + ".jar"); + File xjarFile = new File(Config.X_JAR_DIR + File.separator + service + ".xjar"); + + if (Config.COMPRESS_ENABLED) { + File compressedJarFile = new File(Config.COMPRESS_JAR_DIR + File.separator + service + "-compress.jar"); + JarUtil.compress(service, rawJarFile, includedJars, exclusionJars, compressedJarFile, Config.LIB_DIR); + JarUtil.encrypt(compressedJarFile, xjarFile, Config.ENCRYPTION_PASSWORD, Config.X_JAR_INCLUDES, Config.X_JAR_EXCLUDES); + if (xjarFile.exists()) { + LOGGER.info("压缩并加密" + service + "成功"); + } + } else { + JarUtil.encrypt(rawJarFile, xjarFile, Config.ENCRYPTION_PASSWORD, Config.X_JAR_INCLUDES, Config.X_JAR_EXCLUDES); + if (xjarFile.exists()) { + LOGGER.info("加密" + service + "成功"); + } + } + } + + + + /** + * 查找RAW_JAR_DIR目录中的所有JAR文件,并返回不带.jar扩展名的服务名称集合。 + *

+ * 此方法扫描配置的原始JAR目录,查找所有以.jar结尾的文件, + * 然后从文件名中删除.jar扩展名以获取服务名称。 + *

+ * + * @return 原始JAR目录中找到的服务名称的集合(不带.jar扩展名) + */ + private static Set findAllRawJarServices() { + File dir = new File(Config.RAW_JAR_DIR); + File[] files = dir.listFiles(); + if (files == null) { + return Collections.emptySet(); + } + + return Arrays.stream(files) + .filter(file -> file.getName().endsWith(".jar")) + .map(file -> file.getName().replace(".jar", "")) + .collect(Collectors.toSet()); + } + + /** + * 确保所有必需的目录存在,如果不存在则创建它们。 + *

+ * 此方法检查配置中定义的所有目录,并在必要时创建它们。 + * 这些目录包括: + * - 依赖项XML目录 + * - 原始JAR目录 + * - 压缩JAR目录 + * - 库目录 + * - X_JAR目录 + *

+ */ + private static void ensureDirectoriesExist() { + String[] dirs = { + Config.DEPENDENCY_XML_DIR, + Config.RAW_JAR_DIR, + Config.COMPRESS_JAR_DIR, + Config.LIB_DIR, + Config.X_JAR_DIR + }; + + for (String dir : dirs) { + File file = new File(dir); + if (!file.exists()) { + FileUtil.mkdir(file); + } + } + } + + + /** + * 从XML文件中加载JAR依赖项列表。 + *

+ * 此方法解析指定的XML文件,查找依赖项元素,并提取artifactId和可选的version, + * 以构建JAR文件名列表。 + *

+ * + * @param xmlFile 包含依赖项列表的XML文件 + * @return 从XML文件中提取的JAR名称集合 + * @throws SAXException 如果在解析XML时发生错误 + */ + private static Set loadJarsFromXml(File xmlFile) throws SAXException { + Set jars = new HashSet<>(); + if (!xmlFile.exists()) { + return jars; + } + + SAXReader saxReader = new SAXReader(); + saxReader.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); + saxReader.setEncoding("UTF-8"); + + try { + Document document = saxReader.read(xmlFile); + Element rootElement = document.getRootElement(); + + if (!rootElement.hasContent()) { + return jars; + } + + List dependencies = rootElement.elements("dependency"); + if (dependencies.isEmpty()) { + return jars; + } + + for (Element element : dependencies) { + Element artifactId = element.element("artifactId"); + String artifactIdText = artifactId.getText(); + Element version = element.element("version"); + + String jarName; + if (Objects.nonNull(version)) { + String versionText = version.getText(); + jarName = artifactIdText + "-" + versionText + ".jar"; + } else { + jarName = artifactIdText; + } + jars.add(jarName); + } + } catch (Exception e) { + LOGGER.log(Level.WARNING, "解析XML文件 " + xmlFile.getName() + " 时发生错误", e); + } + + return jars; + } +} +``` + +这是工具的主应用类,提供了完整的命令行接口来处理 JAR 文件的压缩和加密: + +主要功能: + +- 通过配置常量定义输入/输出目录、加密设置等 +- 支持从 XML 配置文件加载要包含和排除的依赖项 +- 批量处理多个 JAR 文件 +- 支持通过系统属性覆盖默认配置 + +主要配置参数: + +- `xml.dir`: 依赖项 XML 文件目录(默认: `./config/xml/`) +- `raw.dir`: 原始 JAR 文件目录(默认: `./config/rawJars/`) +- `compress.dir`: 压缩后 JAR 文件目录(默认: `./config/compressJars/`) +- `libs.dir`: 提取的库文件目录(默认: `./config/libs/`) +- `xjar.dir`: 加密后的 XJar 文件目录(默认: `./config/xJars/`) +- `compress.enable`: 是否启用压缩功能(默认: `true`) + +### 3. pom.xml + +```xml + + 4.0.0 + + spring-boot-jar-slim-encrypt + jar + + spring-boot-jar-slim-encrypt + http://maven.apache.org + + + UTF-8 + + + + + + jitpack.io + https://jitpack.io + + + + + + + com.github.core-lib + xjar + 4.0.0 + + + org.dom4j + dom4j + 2.1.4 + + + cn.hutool + hutool-core + 5.8.25 + + + org.apache.commons + commons-lang3 + 3.0 + + + + + + + + + org.apache.maven.plugins + maven-jar-plugin + 3.2.0 + + + + + + + com.mangmang.SpringBootJarSlimEncryptApplication + + + + + + + + + + org.apache.maven.plugins + maven-assembly-plugin + 3.3.0 + + + make-assembly + + + package + + + single + + + + + + jar-with-dependencies + + + + + com.mangmang.SpringBootJarSlimEncryptApplication + + + + + + + + +``` + + + +## 使用步骤 + +### 步骤 1: 集成自定义类加载器 + +在你的 Spring Boot 项目中添加自定义类加载器依赖: + +```xml + + com.mangmang + spring-boot-custom-classloader + 1.0.0 + +``` + +### 步骤 2: 构建 Spring Boot 应用 + +正常构建你的 Spring Boot 应用: + +```bash +mvn clean package +``` + +### 步骤 3: 配置依赖排除和包含规则 + +>默认路径为根目录下的./config/xml/ + +创建两个 XML 文件以定义要排除和包含的依赖项: + +**exclusions.xml**(要排除的依赖): + +```xml + + + + + cn.hutool + hutool-all + 5.8.26 + + +``` + +**includes.xml**(要保留的依赖): + +```xml + + + + com.fasterxml.jackson.core + jackson-annotations + 2.10.3 + + +``` + +### 步骤 4: 执行 JAR 瘦身和加密工具 + +将你的 Spring Boot JAR 文件放入 `./config/rawJars/` 目录,然后运行瘦身加密工具: + +```bash +java -jar spring-boot-jar-slim-encrypt.jar +``` + +你可以通过系统属性覆盖默认配置: + +```bash +java -Dcompress.enable=true -Dlibs.dir=/custom/libs/path -jar spring-boot-jar-slim-encrypt.jar +``` + +### 步骤 5: 部署和运行 + +#### 1. 精简JAR启动指令 +```shell +java -Dexternal.jars.path.path=/path/to/jars -jar your-application.jar +``` + +#### 2. 加密JAR启动指令 +1. 需要进入xjar.dir(默认: `./config/xJars/`)对应目录 +2. 使用go build xjar.go 编译 +3. 启动win10示例 + ```powershell + .\xjar.exe java "-Dexternal.jars.path=..\libs" -jar .\spring-boot-thin-launcher-1.0-SNAPSHOT.xjar + ``` +4. linux 示例 + ```shell + ./xjar java -Xms256m -Xmx1024m -Dexternal.jars.path=./libs -jar /path/to/encrypted.jar + ``` + +## 技术原理解析 + +### JAR 瘦身原理 + +1. 扫描 Spring Boot JAR 中的 `BOOT-INF/lib` 目录 +2. 根据配置的排除和包含规则过滤依赖 +3. 将被排除的依赖提取到外部目录 +4. 创建一个不包含被排除依赖的新 JAR 文件 + +### JAR 加密原理 + +1. 使用 XJar 库实现 JAR 文件内容的加密 +2. 只加密指定的文件模式(如 Java 类文件、配置文件等) +3. 避免加密某些需要保持明文的资源 + +### 自定义类加载器原理 + +1. 在 Spring Boot 应用启动时初始化自定义类加载器 +2. 扫描指定目录下的 JAR 文件 +3. 使用反射机制将 JAR 文件 URL 添加到当前类加载器 + +## 方案优势 + +1. **减小 JAR 体积**:将大型依赖库外置,显著减小主 JAR 文件体积 +2. **提高安全性**:通过加密保护核心业务逻辑和敏感配置 +3. **灵活配置**:支持通过 XML 配置和系统属性灵活控制瘦身和加密过程 +4. **无缝集成**:与 Spring Boot 应用无缝集成,无需修改应用代码 + +## 注意事项 + +1. 确保加密密码安全保存,丢失密码将导致无法运行加密的 JAR +2. 测试瘦身后的应用,确保所有需要的依赖都能正确加载 +3. 部署时必须将提取的依赖库和加密后的 JAR 一起部署 +4. 启动应用时必须指定外部库路径参数 + +## 结论 + +通过结合 JAR 瘦身、JAR 加密和自定义类加载器,我们成功构建了一套完整的 Spring Boot 应用优化和保护方案。这不仅有效减小了部署包的体积,还提高了应用的安全性,为企业级 Spring Boot 应用的部署提供了一种实用的解决方案。 + +在实际应用中,可以根据具体需求调整配置参数,以达到最佳的平衡点。例如,可以根据应用规模和安全需求调整要排除的依赖和加密的文件模式。 + +这套工具实现了将原本臃肿的 Spring Boot 应用拆分为核心加密 JAR 和外部依赖库的方案,使得应用部署更加灵活,也为应用分发和更新提供了更多可能性。 + +------ + +*注:本方案适用于需要保护核心业务逻辑或减小部署包体积的 Spring Boot 应用。对于简单应用或开源项目,可能不必使用这么复杂的方案。* + + + + + + + + + + \ No newline at end of file diff --git a/src/programming/java/工具箱/SpringBoot打包体积优化.md b/src/programming/java/工具箱/SpringBoot打包体积优化.md deleted file mode 100644 index 4f21359..0000000 --- a/src/programming/java/工具箱/SpringBoot打包体积优化.md +++ /dev/null @@ -1,616 +0,0 @@ ---- -icon: bi:arrows-expand -date: 2025-05-13 -category: - - 实用工具 - - JAVA - - SpringBoot - - JAR -tag: - - JAR包瘦身 -title: SpringBoot打包体积优化.md ---- - -SpringBoot打包体积优化 - - -# Spring Boot JAR 瘦身与加密:构建安全高效的部署方案 - -在 Spring Boot 应用程序部署过程中,我们常常面临两个主要挑战: - -1. **JAR 包体积过大**:Spring Boot 应用打包时会将所有依赖一起打包,导致最终 JAR 文件臃肿 -2. **代码安全性问题**:部署到客户环境或公开场合的 JAR 包可能被反编译,造成核心业务逻辑泄露 - -为了解决这些问题,本文将介绍一套完整的解决方案,包括 JAR 包瘦身和 JAR 包加密两部分,以及配套的自定义类加载器,实现高效安全的 -Spring Boot 应用部署。 - -## 整体方案设计 - -该方案由两个主要项目组成: - -1. **spring-boot-jar-slim-encrypt**:用于将 Spring Boot 应用 JAR 包瘦身和加密 -2. **spring-boot-custom-classloader**:用于加载瘦身和加密后的 JAR 包 - -### 工作流程 - -``` -┌────────────────────┐ -│ 原始Spring Boot │ -│ JAR包 │ -└──────────┬─────────┘ - │ - ▼ -┌────────────────────┐ ┌────────────────────┐ -│ spring-boot-jar- │ │ │ -│ slim-encrypt工具 ├───►│ 提取依赖到libs目录 │ -└──────────┬─────────┘ └────────────────────┘ - │ - ▼ -┌────────────────────┐ -│ 瘦身后的JAR包 │ -└──────────┬─────────┘ - │ - ▼ -┌────────────────────┐ -│ XJar加密处理 │ -└──────────┬─────────┘ - │ - ▼ -┌────────────────────┐ -│ 加密后的JAR包 │ -│ (.xjar) │ -└──────────┬─────────┘ - │ - │ 部署 - ▼ -┌─────────────────────────────────────────┐ -│ 运行时环境 │ -│ ┌─────────────────┐ ┌───────────────┐ │ -│ │ 加密JAR (.xjar) │ │ 提取的依赖库 │ │ -│ └────────┬────────┘ └───────┬───────┘ │ -│ │ │ │ -│ │ │ │ -│ ▼ ▼ │ -│ ┌─────────────────────────────────┐ │ -│ │ PlainTextClassLoader │ │ -│ │ (自定义类加载器) │ │ -│ └─────────────┬─────────────┬─────┘ │ -│ │ │ │ -│ ▼ ▼ │ -│ ┌─────────────────┐ ┌───────────────┐ │ -│ │ 解密JAR内容 │ │ 加载外部依赖 │ │ -│ └─────────────────┘ └───────────────┘ │ -│ │ -└─────────────────────────────────────────┘ -``` - -1. 使用 spring-boot-jar-slim-encrypt 工具提取原始 JAR 包中的依赖 -2. 将依赖库单独存储在 libs 目录 -3. 对精简后的 JAR 包进行加密 -4. 使用自定义类加载器加载外部依赖和解密 JAR 包 - -## 项目一:spring-boot-custom-classloader - -这是一个自定义类加载器项目,它实现了两种类加载器: - -### 1. JarClassLoader 接口 - -```java -package com.mangmang; - -import java.io.File; - -public interface JarClassLoader { - String JAR_EXTENSION = ".jar"; - - /** - * 从指定目录加载所有JAR文件 - * - * @param jarDir 包含要加载的JAR文件的目录路径 - * @throws IllegalArgumentException 如果jarDir为null或不存在 - */ - default void loadJar(String jarDir) { - if (jarDir == null || jarDir.trim().isEmpty()) { - throw new IllegalArgumentException("JAR目录路径不能为空"); - } - - File directory = new File(jarDir); - if (!directory.exists() || !directory.isDirectory()) { - throw new IllegalArgumentException("指定路径不是有效目录: " + jarDir); - } - - File[] jarFiles = directory.listFiles(this::isJarFile); - if (jarFiles == null) { - return; - } - - for (File jarFile : jarFiles) { - System.out.println("加载 》" + jarFile.getName()); - scanJarFile(jarFile); - } - } - - /** - * 递归扫描文件或目录以查找JAR文件 - * - * @param file 要扫描的文件或目录 - * @throws IllegalArgumentException 如果file为null - */ - default void scanJarFile(File file) { - if (file == null) { - throw new IllegalArgumentException("文件不能为null"); - } - - if (!file.exists()) { - return; - } - - if (isJarFile(file)) { - addJARFile(file); - } else if (file.isDirectory()) { - File[] files = file.listFiles(); - if (files != null) { - for (File f : files) { - scanJarFile(f); - } - } - } - } - - /** - * 检查文件是否为JAR文件 - */ - default boolean isJarFile(File file) { - return file.isFile() && file.getName().endsWith(JAR_EXTENSION); - } - - /** - * 将JAR文件添加到类加载器 - * - * @param jar 要添加的JAR文件 - * @throws IllegalArgumentException 如果jar为null或不是有效的JAR文件 - */ - void addJARFile(File jar); -} - -``` - -这是一个接口,定义了 JAR 文件加载的核心方法: - -- `loadJar(String jarDir)`: 从指定目录加载所有 JAR 文件 -- `scanJarFile(File file)`: 递归扫描文件或目录查找 JAR 文件 -- `isJarFile(File file)`: 检查文件是否为 JAR 文件 -- `addJARFile(File jar)`: 将 JAR 文件添加到类加载器 - -### 2. PlainTextClassLoader 实现 - -```java -package com.mangmang; - -import org.springframework.boot.context.event.ApplicationStartingEvent; -import org.springframework.context.ApplicationListener; -import org.springframework.lang.NonNull; - -import java.io.File; -import java.lang.management.ManagementFactory; -import java.lang.reflect.Method; -import java.net.URL; -import java.net.URLClassLoader; -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; - -/** - * 明文类加载器 - *

- * 该类实现了自定义类加载器接口和Spring应用启动事件监听器接口。 - * 主要功能是在应用启动时,从JVM启动参数中查找指定路径的JAR文件并加载。 - * 通过反射机制将JAR文件动态添加到当前线程的类加载器中,实现运行时加载额外的类库。 - *

- *

- * 使用方法: - * 1. 在JVM启动参数中添加 -Dexternal.jars.path=你的JAR文件目录路径 - * 2. 系统将自动加载该目录下所有的JAR文件 - *

- *

- * 示例: - * java -Dexternal.jars.path.path=/path/to/jars -jar your-application.jar - *

- */ -public class PlainTextClassLoader implements JarClassLoader, ApplicationListener { - - private final String findPath = "external.jars.path"; // 查找路径的键名 - private final ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); // 获取当前线程的类加载器 - private final List jarFiles = new ArrayList<>(); // 存储已加载的JAR文件列表 - - /** - * 构造函数 - *

- * 在初始化时执行以下操作: - * 1. 设置当前线程的类加载器 - * 2. 输出启动日志信息 - * 3. 从JVM启动参数中检索包含"external.jars.path.path"的参数 - * 4. 提取路径值并调用loadJar方法加载指定目录下的JAR文件 - *

- */ - public PlainTextClassLoader() { - // 设置当前线程的类加载器 - Thread.currentThread().setContextClassLoader(classLoader); - - // 打印启动信息 - System.out.println("启动自定义明文类加载器"); - - // 查找并加载外部JAR文件 - loadExternalJarsFromSystemProperties(); - } - - /** - * 从系统属性中查找并加载外部JAR文件 - */ - private void loadExternalJarsFromSystemProperties() { - List inputArguments = ManagementFactory.getRuntimeMXBean().getInputArguments(); - - // 查找包含指定路径参数的启动参数 - inputArguments.stream() - .filter(arg -> arg.contains(findPath)) - .map(this::extractPathFromArgument) - .filter(Objects::nonNull) - .forEach(this::loadJar); - } - - /** - * 从JVM参数中提取路径值 - * - * @param argument JVM启动参数 - * @return 提取的路径值,如果提取失败则返回null - */ - private String extractPathFromArgument(String argument) { - String prefix = "-D" + findPath + "="; - if (argument.startsWith(prefix)) { - String path = argument.replace(prefix, ""); - if (!path.isEmpty()) { - return path; - } - } - return null; - } - - /** - * 处理应用程序启动事件 - *

- * 当Spring应用启动时会触发此方法。 - * 目前该方法为空实现,可以在此添加应用启动时需要执行的代码。 - *

- * - * @param event Spring应用启动事件对象 - */ - - @Override - public void onApplicationEvent(@NonNull ApplicationStartingEvent event) { - // 应用程序启动事件的处理方法,目前为空 - } - - /** - * 将JAR文件添加到类加载器 - *

- * 通过反射机制调用URLClassLoader的addURL方法,将指定的JAR文件URL添加到当前类加载器。 - * 添加成功后,JAR文件中的类可以被当前JVM加载和使用。 - * 同时将已加载的JAR文件记录到jarFiles列表中。 - *

- * - * @param jar 要添加到类加载器的JAR文件对象 - * @throws RuntimeException 如果添加过程中发生任何异常,将抛出RuntimeException - */ - @Override - public void addJARFile(File jar) { - if (jar == null) { - throw new IllegalArgumentException("JAR文件不能为null"); - } - - try { - addUrlToClassLoader(jar); - jarFiles.add(jar); - System.out.println(jarFiles); - } catch (Exception e) { - throw new RuntimeException("添加JAR文件到类加载器失败: " + jar.getName(), e); - } - } - - /** - * 通过反射将JAR文件URL添加到类加载器 - * - * @param jar 要添加的JAR文件 - * @throws Exception 如果反射操作失败 - */ - private void addUrlToClassLoader(File jar) throws Exception { - Method addUrlMethod = URLClassLoader.class.getDeclaredMethod("addURL", URL.class); - if (!addUrlMethod.isAccessible()) { - addUrlMethod.setAccessible(true); - } - URL jarUrl = jar.toURI().toURL(); - addUrlMethod.invoke(classLoader, jarUrl); - } -``` - -这是一个明文类加载器,实现了 `JarClassLoader` 接口和 Spring 的 `ApplicationListener` 接口,用于在 -Spring Boot 应用启动时加载外部 JAR 文件: - -主要特点: - -- 在 Spring Boot 应用启动时自动执行 -- 通过 JVM 参数 `-Dexternal.jars.path=你的JAR文件目录路径` 指定外部 JAR 文件目录 -- 使用反射机制将 JAR 文件 URL 添加到当前线程的类加载器中 - -使用示例: - -```bash -java -Dexternal.jars.path=/path/to/jars -jar your-application.jar -``` - -## 项目二:spring-boot-jar-slim-encrypt - -这个工具用于压缩和加密 Spring Boot JAR 文件,主要包含以下两个核心类: - -### 1. JarUtil 工具类 - -```java -// 路径:spring-boot-jar-slim-encrypt/src/main/java/com/mangmang/JarUtil.java -package com.sunri; - -import cn.hutool.core.collection.CollectionUtil; -import cn.hutool.core.io.FileUtil; -import io.xjar.XCryptos; -import io.xjar.XEncryption; -import org.apache.commons.compress.utils.IOUtils; -import org.apache.commons.compress.utils.Sets; - -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.nio.file.Files; -import java.util.Enumeration; -import java.util.HashSet; -import java.util.Set; -import java.util.jar.JarEntry; -import java.util.jar.JarFile; -import java.util.jar.JarOutputStream; - -public class JarUtil { - /** - * 表示UTF-8字符编码的常量。 - * 该变量用于在各种文件操作中强制使用UTF-8编码, - * 确保在整个应用程序中一致地处理文本数据。 - */ - private static final String UTF_8 = "UTF-8"; - /** - * 常量BOOT_INF_LIB表示JAR文件中通常存储依赖库的默认目录路径。 - * 该路径主要用于在压缩或排除等操作中识别和处理库文件。 - */ - private static final String BOOT_INF_LIB = "BOOT-INF/lib"; - /** - * 用于标识JAR(Java归档)文件的文件扩展名。 - * 该常量表示JAR文件的标准扩展名,通常用于 - * 文件过滤、命名或在目录或归档中识别JAR文件的操作。 - */ - private static final String JAR_EXTENSION = ".jar"; - /** - * 定义在管理JAR过程中生成的需求文件的后缀, - * 特别是在处理依赖项或排除项时使用。 - * 该字符串用作特定的文件名模式,用于保存与特定服务 - * 相关的排除依赖项或其他需求的列表。 - * 默认值为"-requirements.txt"。 - */ - private static final String REQUIREMENTS_SUFFIX = "-requirements.txt"; - /** - * 预定义的、不可修改的特定jar文件名集合,被视为 - * "安全"或"始终包含"的文件。这些jar文件通常在 - * 处理或压缩操作中免于排除过滤。 - * 该集合包含以下jar标识符: - * - "spring" - * - "logback-core" - * - "tomcat" - * 该变量用于根据jar文件名决定是否包含特定jar文件的操作中。 - * 它作为应用程序关键或必要jar的白名单。 - */ - private static final Set WHITE_LIST_JARS = Sets.newHashSet("spring", "logback-core", "tomcat"); - - /** - * 通过排除和包含指定的条目来压缩给定的源JAR文件,并将结果写入目标JAR文件。 - * 它处理源JAR的条目,应用排除和包含规则,还可以将某些条目提取到指定的目录中。 - * 在此过程中创建一个临时文件,成功完成后将其重命名为目标JAR文件。 - * - * @param serviceName 正在处理的服务名称,主要用于日志记录和创建其他相关文件。 - * @param sourceJar 要压缩的源JAR文件。 - * @param includes 指定应保留哪些条目的包含模式集合。可能会自动添加额外的默认包含项。 - * @param exclusions 指定应排除哪些条目的排除模式集合。 - * @param targetJar 将写入压缩JAR的文件。 - * @param libDir 某些被排除的条目可能被提取到的目录(如适用)。 - */ - public static void compress(String serviceName, File sourceJar, Set includes, Set exclusions, File targetJar, String libDir) { - includes.addAll(WHITE_LIST_JARS); - File tempJar = new File(targetJar.getAbsolutePath() + ".tmp"); - Set excludedJars = new HashSet<>(); - - if (processJarEntries(sourceJar, tempJar, includes, exclusions, libDir, excludedJars)) { - finalizeCompression(serviceName, targetJar, tempJar, excludedJars, libDir); - } else { - boolean delete = tempJar.delete(); - System.out.println("删除临时文件:{" + delete + "}"); - } - } - - /** - * 处理源JAR文件中的条目以生成临时JAR文件, - * 同时根据包含和排除规则过滤条目。如果需要, - * 还会将指定的JAR条目提取到库目录中。 - * - * @param sourceJar 要处理的源JAR文件 - * @param tempJar -``` - -这个工具类提供了两个主要功能: - -#### JAR 包压缩功能 - -`compress` 方法实现了 JAR 瘦身功能: - -- 根据包含列表和排除列表过滤 JAR 中的依赖 -- 将被排除的依赖提取到指定的库目录 -- 生成一个记录排除依赖的需求文件 -- 创建一个只包含必要依赖的精简 JAR 文件 - -#### JAR 包加密功能 - -`encrypt` 方法利用 XJar 库实现了 JAR 加密: - -- 支持指定加密密码 -- 通过包含和排除模式选择性地加密 JAR 中的内容 -- 生成加密后的 XJar 文件 - -### 2. SpringBootJarSlimEncryptApplication 主应用 - -```java -// 路径:spring-boot-jar-slim-encrypt/src/main/java/com/mangmang/SpringBootJarSlimEncryptApplication.java -``` - -这是工具的主应用类,提供了完整的命令行接口来处理 JAR 文件的压缩和加密: - -主要功能: - -- 通过配置常量定义输入/输出目录、加密设置等 -- 支持从 XML 配置文件加载要包含和排除的依赖项 -- 批量处理多个 JAR 文件 -- 支持通过系统属性覆盖默认配置 - -主要配置参数: - -- `xml.dir`: 依赖项 XML 文件目录(默认: `./config/xml/`) -- `raw.dir`: 原始 JAR 文件目录(默认: `./config/rawJars/`) -- `compress.dir`: 压缩后 JAR 文件目录(默认: `./config/compressJars/`) -- `libs.dir`: 提取的库文件目录(默认: `./config/libs/`) -- `xjar.dir`: 加密后的 XJar 文件目录(默认: `./config/xJars/`) -- `compress.enable`: 是否启用压缩功能(默认: `true`) - -## 使用步骤 - -### 步骤 1: 集成自定义类加载器 - -在你的 Spring Boot 项目中添加自定义类加载器依赖: - -```xml - - - com.mangmang - spring-boot-custom-classloader - 1.0.0 - -``` - -### 步骤 2: 构建 Spring Boot 应用 - -正常构建你的 Spring Boot 应用: - -```bash -mvn clean package -``` - -### 步骤 3: 配置依赖排除和包含规则 - -创建两个 XML 文件以定义要排除和包含的依赖项: - -**exclusions.xml**(要排除的依赖): - -```xml - - - - spring-boot-starter-web - 2.7.0 - - - -``` - -**includes.xml**(要保留的依赖): - -```xml - - - - spring-boot-custom-classloader - - - -``` - -### 步骤 4: 执行 JAR 瘦身和加密工具 - -将你的 Spring Boot JAR 文件放入 `./config/rawJars/` 目录,然后运行瘦身加密工具: - -```bash -java -jar spring-boot-jar-slim-encrypt.jar -``` - -你可以通过系统属性覆盖默认配置: - -```bash -java -Dcompress.enable=true -Dlibs.dir=/custom/libs/path -jar spring-boot-jar-slim-encrypt.jar -``` - -### 步骤 5: 部署和运行 - -部署加密后的 JAR 文件和提取的库文件: - -```bash -java -Dexternal.jars.path=/path/to/libs -jar your-application.xjar -``` - -## 技术原理解析 - -### JAR 瘦身原理 - -1. 扫描 Spring Boot JAR 中的 `BOOT-INF/lib` 目录 -2. 根据配置的排除和包含规则过滤依赖 -3. 将被排除的依赖提取到外部目录 -4. 创建一个不包含被排除依赖的新 JAR 文件 - -### JAR 加密原理 - -1. 使用 XJar 库实现 JAR 文件内容的加密 -2. 只加密指定的文件模式(如 Java 类文件、配置文件等) -3. 避免加密某些需要保持明文的资源 - -### 自定义类加载器原理 - -1. 在 Spring Boot 应用启动时初始化自定义类加载器 -2. 扫描指定目录下的 JAR 文件 -3. 使用反射机制将 JAR 文件 URL 添加到当前类加载器 - -## 方案优势 - -1. **减小 JAR 体积**:将大型依赖库外置,显著减小主 JAR 文件体积 -2. **提高安全性**:通过加密保护核心业务逻辑和敏感配置 -3. **灵活配置**:支持通过 XML 配置和系统属性灵活控制瘦身和加密过程 -4. **无缝集成**:与 Spring Boot 应用无缝集成,无需修改应用代码 - -## 注意事项 - -1. 确保加密密码安全保存,丢失密码将导致无法运行加密的 JAR -2. 测试瘦身后的应用,确保所有需要的依赖都能正确加载 -3. 部署时必须将提取的依赖库和加密后的 JAR 一起部署 -4. 启动应用时必须指定外部库路径参数 - -## 结论 - -通过结合 JAR 瘦身、JAR 加密和自定义类加载器,我们成功构建了一套完整的 Spring Boot 应用优化和保护方案。这不仅有效减小了部署包的体积,还提高了应用的安全性,为企业级 -Spring Boot 应用的部署提供了一种实用的解决方案。 - -在实际应用中,可以根据具体需求调整配置参数,以达到最佳的平衡点。例如,可以根据应用规模和安全需求调整要排除的依赖和加密的文件模式。 - -这套工具实现了将原本臃肿的 Spring Boot 应用拆分为核心加密 JAR 和外部依赖库的方案,使得应用部署更加灵活,也为应用分发和更新提供了更多可能性。 - ------- - -*注:本方案适用于需要保护核心业务逻辑或减小部署包体积的 Spring Boot -应用。对于简单应用或开源项目,可能不必使用这么复杂的方案。* \ No newline at end of file diff --git a/src/programming/java/工具箱/WSL2.md b/src/programming/java/工具箱/WSL2.md new file mode 100644 index 0000000..e6f782f --- /dev/null +++ b/src/programming/java/工具箱/WSL2.md @@ -0,0 +1,558 @@ +--- +icon: bi:arrows-expand +date: 2025-05-08 +category: + - win10 +tag: + - wsl + - http +title: WSL2 +--- + +# WSL2完全配置指南:从安装到实用工具 +Windows Subsystem for Linux (WSL2) 为Windows用户提供了无需双系统或虚拟机就能运行Linux环境的能力。本文将全面介绍WSL2的安装、配置和优化过程,包括网络设置、常用工具安装和问题排查等内容。 + +## 安装WSL2 + +1. **启用WSL功能**: 打开控制面板,启用"Windows Subsystem for Linux"和"虚拟机平台"功能,然后重启电脑。 + +2. **检查WSL版本**: + + ```powershell + wsl --status + wsl --update + ``` + +3. **设置默认为WSL2**: + + ```powershell + wsl --set-default-version 2 + ``` + +4. **查看可用的Linux发行版**: + + ```powershell + wsl --list --online + ``` + +5. **安装Linux发行版**: + + ```powershell + # 安装默认版本(Ubuntu) + wsl --install + + # 或安装指定版本 + wsl --install -d Ubuntu-24.04 + ``` + +6. **设置用户名和密码**,安装完成后系统会提示设置。 + +7. **更新系统**: + + ```bash + sudo apt update && sudo apt upgrade + ``` + +8. **验证安装**: + + ```powershell + wsl -l -v + ``` + +## 网络配置 + +### 配置DNS + +WSL2使用的DNS服务器在`/etc/resolv.conf`文件中设置,为防止每次启动重置,需要进行以下配置: + +1. 创建`/etc/wsl.conf`文件: + + ```bash + [network] + generateResolvConf = false + ``` + +2. 删除原链接文件: + + ```bash + rm /etc/resolv.conf + ``` + +3. 创建新配置: + + ```bash + vi /etc/resolv.conf + # 添加内容 + nameserver 114.114.114.114 + ``` + +4. 重启WSL: + + ```bash + exit + wsl --shutdown + wsl + ``` + +5. 测试网络: + + ```bash + ping www.baidu.com + ``` + +### 配置桥接网络 + +1. **开启Hyper-V**后执行以下命令: + + ```powershell + Get-NetAdapter + New-VMSwitch -SwitchName "VETH" -NetAdapterName "以太网" -AllowManagementOS $True + ``` + +2. 创建`.wslconfig`配置文件: + + ```powershell + cd ~ + New-Item .wslconfig + notepad .\.wslconfig + ``` + +3. 添加以下内容: + + ``` + [wsl2] + networkingMode=bridged + vmSwitch=VETH + ipv6=true + ``` + +### 配置网络代理 + +1. 关闭自动更新DNS: + + ```bash + #/etc/wsl.conf + [network] + generateResolvConf = false + ``` + +2. 添加以下脚本至`.bashrc`或`.zshrc`: + + ```bash + vi ~/.bashrc + # 添加代理配置 + export hostip=10.6.212.22 # 替换为你的代理IP + export hostport=7890 # 替换为你的代理端口 + alias proxy=' + export HTTPS_PROXY="http://${hostip}:${hostport}"; + export HTTP_PROXY="http://${hostip}:${hostport}"; + export ALL_PROXY="http://${hostip}:${hostport}"; + echo -e "Acquire::http::Proxy \"http://${hostip}:${hostport}\";" | sudo tee -a /etc/apt/apt.conf.d/proxy.conf > /dev/null; + echo -e "Acquire::https::Proxy \"http://${hostip}:${hostport}\";" | sudo tee -a /etc/apt/apt.conf.d/proxy.conf > /dev/null; + ' + alias unproxy=' + unset HTTPS_PROXY; + unset HTTP_PROXY; + unset ALL_PROXY; + sudo sed -i -e '/Acquire::http::Proxy/d' /etc/apt/apt.conf.d/proxy.conf; + sudo sed -i -e '/Acquire::https::Proxy/d' /etc/apt/apt.conf.d/proxy.conf; + ' + ``` + +3. 执行命令启用/禁用代理: + + ```bash + # 启用代理 + proxy + + # 禁用代理 + unproxy + ``` + +4. 固定DNS配置: + + ```bash + sudo rm /etc/resolv.conf + sudo bash -c 'echo "nameserver 8.8.8.8" > /etc/resolv.conf' + sudo bash -c 'echo "[network]" > /etc/wsl.conf' + sudo bash -c 'echo "generateResolvConf = false" >> /etc/wsl.conf' + sudo chattr +i /etc/resolv.conf + ``` + +5. 测试代理: + + ```bash + curl -vv google.com + ``` + +## 基础功能配置 + +### 开启SSH服务 + +1. 安装SSH服务: + + ```bash + sudo apt update && sudo apt upgrade + sudo apt-get install openssh-server + ``` + +2. 修改SSH配置: + + ```bash + sudo vi /etc/ssh/sshd_config + ``` + + 找到`PasswordAuthentication`行,确保设置为`yes` + +3. 启动并设置开机自启: + + ```bash + sudo systemctl status ssh + sudo systemctl start ssh + sudo systemctl enable ssh + ``` + +### 安装桌面环境 + +```bash +sudo apt update +sudo apt install ubuntu-desktop +# 安装远程桌面服务 +sudo apt-get install xrdp +sudo systemctl start xrdp +sudo systemctl enable xrdp +``` + +### 映射Windows目录至WSL + +```bash +# 创建挂载点 +sudo mkdir /mnt/z +# 挂载Windows目录 +sudo mount -t drvfs C:/User/xxx/Desktop/挂载文件 /mnt/z +``` + +## 系统管理 + +### 查看端口 + +查询端口占用有两种常用方法: + +1. 使用`netstat`: + + ```bash + sudo apt-get install net-tools + sudo netstat -tunlp | grep 端口号 + ``` + +2. 使用`lsof`: + + ```bash + sudo apt-get install lsof + sudo lsof -i:端口号 + ``` + +### 修改主机名 + +1. 使用`hostnamectl`命令: + + ```bash + sudo hostnamectl set-hostname 新主机名 + ``` + +2. 修改配置文件: + + ```bash + sudo vi /etc/hostname + sudo vi /etc/hosts + ``` + + 在hosts文件中将`127.0.1.1`对应的旧主机名替换为新主机名 + +3. 重启系统(可选): + + ```bash + sudo reboot + ``` + +## 开发环境配置 + +### IDEA中文乱码修复 + +1. 安装语言包: + + ```bash + sudo apt install language-pack-zh-hans + ``` + +2. 配置语言环境: + + ```bash + sudo dpkg-reconfigure locales + # 选择en_US.UTF-8和zh_CN.UTF-8,并将zh_CN.UTF-8设为默认 + ``` + +3. 安装字体工具: + + ```bash + sudo apt install fontconfig + ``` + +4. 配置Windows字体: + + ```bash + sudo vi /etc/fonts/local.conf + ``` + + 添加内容: + + ```xml + + + + /mnt/c/Windows/Fonts + + ``` + +5. 刷新字体缓存: + + ```bash + fc-cache -f -v + ``` + +6. 重启WSL: + + ```bash + wsl --shutdown + ``` + +### IDEA配置输入法 + +1. 安装fcitx输入法: + + ```bash + sudo apt install fcitx dbus-x11 im-config fcitx-sunpinyin + ``` + +2. 编辑`/etc/locale.gen`: + + ```bash + vi /etc/locale.gen + # 取消注释行:zh_CN.UTF-8 + ``` + +3. 配置环境变量: + + ```bash + vi ~/.profile + # 添加内容 + export GTK_IM_MODULE=fcitx + export QT_IM_MODULE=fcitx + export XMODIFIERS=@im=fcitx + export DefaultIMModule=fcitx + fcitx-autostart &>/dev/null + ``` + +4. 更新配置: + + ```bash + source ~/.profile + ``` + +5. 配置快捷键: + + ```bash + fcitx-config-gtk3 + ``` + +6. IDEA支持:编辑`idea.sh`启动脚本,添加: + + ```properties + export XMODIFIERS=@im=fcitx + export QT_IM_MODULE=fcitx + ``` + +### 安装Docker + +1. 更新系统包: + + ```bash + sudo apt update + ``` + +2. 安装依赖: + + ```bash + sudo apt install ca-certificates curl gnupg lsb-release + ``` + +3. 添加Docker官方GPG密钥: + + ```bash + sudo mkdir -p /etc/apt/keyrings + curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg + ``` + +4. 添加Docker APT源: + + ```bash + echo \ + "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \ + $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null + ``` + +5. 更新包列表: + + ```bash + sudo apt update + ``` + +6. 安装Docker引擎: + + ```bash + sudo apt install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin + ``` + +7. 启动并验证: + + ```bash + sudo systemctl start docker + sudo systemctl enable docker + sudo docker --version + ``` + +### 安装1Panel + +一键安装: + +```bash +curl -sSL https://resource.fit2cloud.com/1panel/package/quick_start.sh -o quick_start.sh && sudo bash quick_start.sh +``` + +查看管理员密码: + +```bash +sudo 1pctl user-info +``` + +### 安装SVN + +```bash +sudo apt update +sudo apt install subversion +svn --version # 验证安装 +``` + +## 问题记录 + +### SSH连接异常 + +症状:SSH服务启动失败,出现以下错误: + +``` +error: Bind to port 22 on 0.0.0.0 failed: Address already in use. +fatal: Missing privilege separation directory: /run/sshd +``` + +解决方法: + +1. 检查端口占用: + + ```bash + sudo lsof -i:22 + ``` + +2. 终止占用进程: + + ```bash + sudo kill + ``` + +3. 创建缺失目录: + + ```bash + sudo mkdir -p /run/sshd + sudo chmod 0755 /run/sshd + ``` + +4. 重启SSH服务: + + ```bash + sudo systemctl restart ssh + ``` + +## 常用指令 + +```powershell +# 列出可用的Linux发行版 +wsl --list --online + +# 列出已安装的发行版 +wsl --list --verbose # 或 wsl -l -v + +# 设置WSL版本 +wsl --set-version <发行版名称> <版本号> + +# 设置默认WSL版本 +wsl --set-default-version <版本号> + +# 设置默认Linux发行版 +wsl --set-default <发行版名称> + +# 运行特定发行版 +wsl --distribution <发行版名称> --user <用户名> + +# 更新WSL +wsl --update + +# 检查WSL状态 +wsl --status + +# 检查WSL版本 +wsl --version + +# 以特定用户身份运行 +wsl --user <用户名> + +# 卸载Linux发行版 +wsl --unregister <发行版名称> + +# 标识IP地址 +wsl hostname -I # 返回WSL2 IP地址 +ip route show | grep -i default | awk '{ print $3}' # 返回Windows主机IP + +# 更改默认用户 +<发行版名称> config --default-user <用户名> +``` + +## 实用软件安装 + +### 安装适用于 Linux 的 Google Chrome + +```bash +cd /tmp +wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb +sudo apt install --fix-missing ./google-chrome-stable_current_amd64.deb +``` + +启动命令:`google-chrome` + +### 安装 VLC + +```bash +sudo apt install vlc -y +``` + +启动命令:`vlc` + +### 安装 X11 应用 + +```bash +sudo apt install x11-apps -y +``` + +启动命令示例:`xcalc`、`xclock`、`xeyes` + +------ + +通过本文的配置指南,你可以构建一个功能完善的WSL2环境,满足日常开发、学习和娱乐需求。WSL2的灵活性使得Windows用户无需切换操作系统就能享受Linux的强大功能,是开发人员的理想工具。 \ No newline at end of file diff --git a/src/programming/java/工具箱/assert/sshkeys_create-8409f453e6780ca1a8db3ce33c74240b.png b/src/programming/java/工具箱/assert/sshkeys_create-8409f453e6780ca1a8db3ce33c74240b.png new file mode 100644 index 0000000..c585b23 Binary files /dev/null and b/src/programming/java/工具箱/assert/sshkeys_create-8409f453e6780ca1a8db3ce33c74240b.png differ diff --git a/src/programming/java/工具箱/assert/sshkeys_list-bff1a324894abbdc3ab8f61c49bb63d5.png b/src/programming/java/工具箱/assert/sshkeys_list-bff1a324894abbdc3ab8f61c49bb63d5.png new file mode 100644 index 0000000..8132a32 Binary files /dev/null and b/src/programming/java/工具箱/assert/sshkeys_list-bff1a324894abbdc3ab8f61c49bb63d5.png differ diff --git a/src/programming/java/工具箱/assert/sshkeys_show-a14cdfb89475debed237bfded2bd9848.png b/src/programming/java/工具箱/assert/sshkeys_show-a14cdfb89475debed237bfded2bd9848.png new file mode 100644 index 0000000..b5e9db5 Binary files /dev/null and b/src/programming/java/工具箱/assert/sshkeys_show-a14cdfb89475debed237bfded2bd9848.png differ diff --git a/src/programming/java/工具箱/gitee-ssh.md b/src/programming/java/工具箱/gitee-ssh.md new file mode 100644 index 0000000..f092375 --- /dev/null +++ b/src/programming/java/工具箱/gitee-ssh.md @@ -0,0 +1,190 @@ +--- +icon: bi:arrows-expand +date: 2025-05-08 +category: + - 实用工具 +tag: + - gitee + - 码云 +title: Gitee SSH +--- + +在Gitee上设置SSH公钥:完整指南 + + +# 在Gitee上设置SSH公钥:完整指南 + +## 引言 + +在使用Git进行代码管理时,SSH协议为开发者提供了一种安全且便捷的仓库访问方式。相比HTTPS协议,SSH协议不需要每次操作都输入用户名和密码,大大提高了开发效率。对于经常使用Gitee平台的Java开发者来说,正确配置SSH公钥是日常工作流程中的重要一环。本文将详细介绍如何在Gitee平台上生成并设置SSH公钥,帮助您实现更高效的代码管理。 + +## SSH公钥的基本概念 + +在深入了解操作步骤前,我们先来理解一下SSH公钥认证的基本原理: + +- SSH认证基于非对称加密,包含一对密钥:私钥(保存在本地)和公钥(上传至服务器) +- 私钥必须安全保管,不可泄露;公钥则可以自由分享 +- 当您使用SSH协议访问仓库时,Gitee会验证您的身份,确认您是否拥有与已注册公钥匹配的私钥 + +## 详细操作步骤 + +### 1. 生成SSH密钥对 + +首先,我们需要生成SSH密钥对。对于Windows用户,建议使用**Windows PowerShell**或**Git Bash**执行以下命令(注意:Windows的命令提示符中没有`cat`和`ls`命令)。 + +```bash +ssh-keygen -t ed25519 -C "Gitee SSH Key" +``` + +参数说明: +- `-t`:指定密钥类型,这里使用更安全的ed25519算法 +- `-C`:添加注释,便于识别密钥的用途 + +执行命令后,系统会提示您输入保存密钥的位置和密码短语。一般情况下,直接按三次回车键即可使用默认设置: + +```bash +Generating public/private ed25519 key pair. +Enter file in which to save the key (/home/git/.ssh/id_ed25519): +Enter passphrase (empty for no passphrase): +Enter same passphrase again: +Your identification has been saved in /home/git/.ssh/id_ed25519 +Your public key has been saved in /home/git/.ssh/id_ed25519.pub +``` + +这样我们就成功生成了两个文件: +- `id_ed25519`:私钥文件(保密,不要分享) +- `id_ed25519.pub`:公钥文件(需要上传到Gitee) + +### 2. 获取公钥内容 + +执行以下命令查看生成的SSH公钥内容: + +```bash +cat ~/.ssh/id_ed25519.pub +``` + +命令会输出类似以下格式的公钥内容: + +``` +ssh-ed25519 AAAA***5B Gitee SSH Key +``` + +请完整复制这段输出的内容,包括开头的`ssh-ed25519`和末尾的注释`Gitee SSH Key`。 + +### 3. 将公钥添加到Gitee账户 + +获取到公钥内容后,我们需要将其添加到Gitee账户中: + +1. 登录Gitee账户,点击右上角头像 +2. 选择「个人设置」->「安全设置」->「SSH 公钥」 +3. 点击「添加公钥」按钮 +4. 在表单中填入公钥标题(自定义,方便识别)和公钥内容 +5. 输入Gitee账户密码进行验证 +6. 点击「添加」按钮完成操作 + +![添加账户SSH公钥](./assert/sshkeys_create-8409f453e6780ca1a8db3ce33c74240b.png "添加用户SSH公钥") + +### 4. 验证SSH连接 + +添加完成后,可以通过以下命令测试SSH连接是否配置成功: + +```bash +ssh -T git@gitee.com +``` + +如果配置正确,您将看到以下输出,其中USERNAME是您的Gitee用户名: + +```bash +Hi USERNAME! You've successfully authenticated, but GITEE.COM does not provide shell access. +``` + +看到这个提示就说明SSH公钥已经成功配置,您可以开始使用SSH协议来操作Gitee上的仓库了。 + +## SSH公钥的管理 + +在Gitee平台上,您可以方便地管理已添加的SSH公钥: + +1. 浏览公钥列表:通过「个人设置」->「安全设置」->「SSH 公钥」页面查看所有已添加的SSH公钥 +2. 查看公钥详情:点击具体的公钥可以查看其详细信息 +3. 删除公钥:当某个SSH公钥不再需要时,可以直接删除它 + +![浏览SSH Key](./assert/sshkeys_list-bff1a324894abbdc3ab8f61c49bb63d5.png "浏览SSH Key") + +![查看/删除SSH Key](./assert/sshkeys_show-a14cdfb89475debed237bfded2bd9848.png "查看/删除SSH Key") + +## 账户SSH公钥vs仓库SSH公钥 + +在Gitee平台上,存在两种不同类型的SSH公钥,理解它们的区别对正确使用Git操作非常重要: + +### 账户SSH公钥 + +- 与您的Gitee账户绑定 +- 当您的账户对仓库有推送/拉取权限时,可以使用这个公钥进行对应操作 +- 验证时会显示您的用户名:`Hi USERNAME! You've successfully authenticated...` +- 适用于日常开发过程中的代码提交、拉取等操作 + +### 仓库SSH公钥(部署公钥) + +- 仅针对特定仓库,与账户无关 +- 仅具有仓库的**拉取**权限,无法进行推送操作 +- 验证时显示为匿名用户:`Hi Anonymous! You've successfully authenticated...` +- 主要用于部署环境(如生产服务器)拉取代码,避免在服务器上存储个人账户凭证 + +> 注意:如果您需要设置仓库的部署公钥,请参考Gitee帮助文档中的"添加部署公钥"章节。 + +## 实用技巧与常见问题 + +### 使用不同的SSH密钥访问不同的Git服务 + +如果您同时使用多个Git平台(如Gitee、GitHub、GitLab等),可以为每个平台生成不同的SSH密钥对,并在SSH配置文件中指定使用规则。 + +1. 为不同平台生成不同的密钥,例如: + ```bash + ssh-keygen -t ed25519 -C "Gitee" -f ~/.ssh/gitee_id_ed25519 + ssh-keygen -t ed25519 -C "GitHub" -f ~/.ssh/github_id_ed25519 + ``` + +2. 创建或编辑`~/.ssh/config`文件: + ``` + # Gitee平台 + Host gitee.com + HostName gitee.com + PreferredAuthentications publickey + IdentityFile ~/.ssh/gitee_id_ed25519 + + # GitHub平台 + Host github.com + HostName github.com + PreferredAuthentications publickey + IdentityFile ~/.ssh/github_id_ed25519 + ``` + +### SSH连接故障排查 + +如果在验证SSH连接时遇到问题,可以尝试以下步骤: + +1. 使用`-v`参数获取详细信息: + ```bash + ssh -vT git@gitee.com + ``` + +2. 检查SSH密钥权限(Unix/Linux/macOS系统): + ```bash + chmod 700 ~/.ssh + chmod 600 ~/.ssh/id_ed25519 + chmod 644 ~/.ssh/id_ed25519.pub + ``` + +3. 确认SSH代理正在运行: + ```bash + eval $(ssh-agent -s) + ssh-add ~/.ssh/id_ed25519 + ``` + +## 总结 + +正确设置和使用SSH公钥对于Java开发者在Gitee平台上进行高效的代码管理至关重要。通过本文介绍的步骤,您可以轻松生成SSH密钥对、将公钥添加到Gitee账户,并开始使用SSH协议访问您的代码仓库。这不仅提高了安全性,还简化了您的日常工作流程,避免了重复输入用户名和密码的繁琐过程。 + +对于团队开发和持续集成/持续部署(CI/CD)环境,了解账户SSH公钥和仓库部署公钥的区别尤为重要,这有助于您根据不同场景选择最合适的认证方式,确保代码安全的同时提高开发和部署效率。 + +希望本指南能帮助您在Gitee平台上更加顺畅地使用Git进行项目管理和协作开发。如果在实际操作中遇到任何问题,欢迎参考Gitee的官方帮助文档获取更多支持。 \ No newline at end of file