
1. 项目概述为什么数据库连接信息需要加密在任何一个基于Spring Boot和MyBatisPlus的后端项目中application.yml或application.properties文件里的数据库配置比如spring.datasource.username和spring.datasource.password几乎就是整个应用的“命门”。这些信息以明文形式躺在配置文件里就像把自家大门的钥匙挂在门把手上。无论是代码不慎上传到公开的Git仓库还是服务器配置文件权限管理不当都可能导致敏感信息泄露进而引发数据被拖库、篡改甚至删除的灾难性后果。因此对数据库用户名和密码进行加密存储是从开发阶段就应建立的基本安全防线。MyBatisPlus作为一个强大的MyBatis增强工具它本身并未直接提供配置文件加密的功能。但这恰恰是Spring框架灵活性的体现——我们可以通过实现或定制Spring提供的特定接口在配置加载的“源头”或“中途”对加密的字符串进行解密。常见的做法是对password这类字段进行加密例如使用AES、DES或国密算法SM4等在配置文件中存储密文程序启动时再动态解密。这不仅仅是“把明文变密文”那么简单它涉及到Spring Boot的启动流程、配置属性加载机制以及如何无缝集成加解密工具。接下来我将拆解几种主流且实用的实现方案并分享在实际企业级项目中落地时的核心细节和避坑经验。2. 核心方案选型与设计思路拆解面对“加密数据库配置”这个需求技术选型路径大致可以分为三类每类都有其适用场景和优缺点。选择哪种取决于项目的安全等级要求、运维复杂度以及团队的技术栈。2.1 方案一自定义DataSource实现最灵活、最推荐这是最经典和可控的方案。核心思路是我们不去动Spring Boot自动配置DataSource的默认行为而是“偷梁换柱”在DataSource被创建出来之后、真正被使用之前将其内部的连接属性用户名、密码替换成解密后的值。为什么推荐这个方案侵入性低它完全遵循Spring的依赖注入和Bean生命周期管理没有破坏Spring Boot自动配置的魔法。你只是提供了一个“增强版”的DataSourceBean。灵活性高不仅可以处理密码理论上可以处理DataSource的任何配置属性。加解密算法可以自由选择如Jasypt、自定义AES工具类等密钥管理方式也可以灵活设计如从环境变量、启动参数或专门的密钥服务获取。兼容性好无论你使用HikariCP、Druid还是其他连接池这个方案都适用因为最终你提供的就是一个标准的javax.sql.DataSource实现。设计流程图概念应用启动Spring Boot读取application.yml其中password值为密文如ENC(XXXXXX)。在配置类中我们声明一个Bean方法来创建DataSource。在该方法内部我们先从Environment中获取原始的、加密的配置值。调用我们自己的解密服务对密文进行解密得到明文密码。使用解密后的明文密码结合其他配置如url, username手动构造一个DataSource实例例如HikariDataSource并返回。Spring容器将使用我们这个手动创建的、内含解密后密码的DataSourceBean替代掉原本可能由自动配置创建的Bean。2.2 方案二使用EnvironmentPostProcessor接口更底层这个方案介入的时机更早。EnvironmentPostProcessor允许你在Spring的Environment对象它包含了所有配置属性完全准备好、但尚未用于创建Bean之前对里面的属性值进行修改。我们可以在这里遍历配置属性识别出需要解密的项例如所有以ENC(开头和)结尾的值然后进行解密并替换回Environment中。这个方案的优缺点是什么优点修改的是配置源数据后续所有Bean不仅仅是DataSource在读取配置时拿到的直接就是解密后的明文。理论上更彻底。缺点实现稍复杂需要实现一个EnvironmentPostProcessor并在META-INF/spring.factories中注册对新手不够友好。可能影响其他组件如果你有其他组件也依赖这些加密属性且它们不期望在Environment中看到明文可能会造成意料之外的影响虽然这种情况较少。调试不便属性在非常早的阶段就被修改在排查配置问题时需要明确知道这个后处理器的存在。2.3 方案三集成第三方库如Jasypt最快速Jasypt是一个成熟的Java加密库其jasypt-spring-boot-starter提供了与Spring Boot无缝集成的能力。你只需要在配置文件中将密文包裹在ENC()中例如password: ENC(密文字符串)然后在启动参数或配置中指定加密密码秘钥它就能自动解密。为什么它快但需要谨慎选择优点几乎零编码依赖配置即可完成快速原型验证和小型项目的不错选择。缺点与风险密钥管理解密密钥jasypt.encryptor.password本身又成了新的秘密。如果把它写在application.yml里等于没加密。通常需要放在环境变量(-Djasypt.encryptor.passwordxxx)或启动参数中这要求运维体系必须支持。算法与版本不同版本Jasypt的默认算法可能有差异需注意兼容性。对于有明确国密算法要求的项目可能需要定制。黑盒化它封装了细节当出现解密失败或集成问题时排查深度可能不够依赖社区解决。实操心得对于中大型、对安全有严肃要求的项目我强烈推荐方案一自定义DataSource。它实现了“关注点分离”加解密逻辑是我们自己写的清晰可控密钥管理可以独立设计比如从公司的配置中心获取而且它只影响DataSource这一个Bean不会无意中改动其他配置风险边界清晰。下文也将以方案一为主线进行详细实现。3. 基于自定义DataSource的完整实现详解我们将一步步实现一个生产可用的、支持加密密码的DataSource配置方案。假设我们使用HikariCP作为连接池并采用AES算法进行加解密。3.1 第一步引入核心依赖首先在pom.xml中确保有以下依赖。MyBatisPlus的starter已经包含了MyBatis和Spring Boot数据源自动配置我们只需要额外引入一个加解密工具库这里以Apache Commons Codec和Hutool的加密工具为例你也可以用Java自带的javax.crypto或Bouncy Castle。dependencies !-- Spring Boot Starter -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency !-- MyBatisPlus Starter -- dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.6/version !-- 请使用最新稳定版 -- /dependency !-- 数据库驱动 -- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope /dependency !-- 加解密工具 (Hutool功能丰富且易用) -- dependency groupIdcn.hutool/groupId artifactIdhutool-all/artifactId version5.8.25/version /dependency /dependencies3.2 第二步准备加解密工具类我们创建一个EncryptUtils类封装AES加密和解密方法。密钥AES_KEY的管理是安全的核心绝对不要硬编码在代码中通常应从环境变量、启动参数或安全的配置中心读取。import cn.hutool.core.util.CharsetUtil; import cn.hutool.crypto.Mode; import cn.hutool.crypto.Padding; import cn.hutool.crypto.symmetric.AES; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import javax.annotation.PostConstruct; import java.util.Base64; Component public class EncryptUtils { /** * AES密钥。此处从配置读取生产环境应从更安全的地方获取如环境变量。 * 示例在application.yml中配置 encrypt.aes-key: your-32byte-aes-key */ Value(${encrypt.aes-key:}) private String aesKeyStr; private AES aes; /** * 初始化AES实例。采用AES/CBC/PKCS5Padding模式。 * 密钥长度需为16/24/32字节对应AES-128/192/256。 */ PostConstruct public void init() { if (aesKeyStr null || aesKeyStr.length() 16) { throw new IllegalArgumentException(AES密钥未正确配置或长度不足。请检查encrypt.aes-key属性。); } // 确保密钥长度符合要求这里简单截取前32字节更严谨的做法是校验长度。 byte[] key aesKeyStr.substring(0, 32).getBytes(CharsetUtil.CHARSET_UTF_8); // 使用固定的IV初始化向量实际生产环境可以考虑将IV也动态配置或管理。 // 注意固定IV会降低安全性但对于配置解密场景通常可接受。更高要求可将IV与密文一起存储。 byte[] iv 1234567890123456.getBytes(CharsetUtil.CHARSET_UTF_8); this.aes new AES(Mode.CBC, Padding.PKCS5Padding, key, iv); } /** * 解密方法 * param encryptedBase64 经过Base64编码的AES密文 * return 解密后的明文 */ public String decrypt(String encryptedBase64) { if (encryptedBase64 null || encryptedBase64.isEmpty()) { return null; } try { // 先进行Base64解码再进行AES解密 byte[] encryptedBytes Base64.getDecoder().decode(encryptedBase64); byte[] decryptedBytes aes.decrypt(encryptedBytes); return new String(decryptedBytes, CharsetUtil.CHARSET_UTF_8); } catch (Exception e) { throw new RuntimeException(解密失败请检查密文格式或密钥是否正确, e); } } /** * 加密方法用于生成密文可在本地运行生成加密后的密码填入配置文件 * param plainText 明文 * return Base64编码后的AES密文 */ public String encrypt(String plainText) { byte[] encryptedBytes aes.encrypt(plainText); return Base64.getEncoder().encodeToString(encryptedBytes); } }注意事项密钥管理上述代码从encrypt.aes-key配置项读取密钥。在生产环境中这个配置项本身不应该出现在application.yml中正确的做法是在测试/开发环境的配置文件中放置一个测试密钥而在生产环境的启动命令中通过-Dencrypt.aes-keyxxx或环境变量ENCRYPT_AES_KEY传入。例如在K8s的Deployment YAML中将其作为Secret注入到环境变量。IV初始化向量示例使用了固定IV。在CBC模式下固定IV会导致相同的明文生成相同的密文在某些场景下可能泄露信息模式。对于数据库密码加密这个风险通常可控。如果要求更高可以采用GCM等认证加密模式或者将IV随机生成后与密文一起存储如IV:密文格式在解密时拆分。异常处理解密失败时应抛出明确的运行时异常让应用在启动阶段就快速失败避免使用错误的密码连接数据库。3.3 第三步编写加密后的配置文件在application.yml中我们将密码替换为通过上面EncryptUtils.encrypt()方法生成的密文。同时存放AES密钥注意这里仅用于演示生产环境不应在此写真实密钥。# 应用配置 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/your_database?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai username: root # 用户名也可以加密方法同理 password: 5f4dsc3v2a1f7g9h0j2k8l3q6w4e7r1t # 这里是AES加密后再Base64的密文对应明文是your_password hikari: connection-timeout: 30000 maximum-pool-size: 20 minimum-idle: 5 # 加密配置 (生产环境此处的aes-key应为空通过外部传入) encrypt: aes-key: this-is-a-32byte-aes-key-test-1234567 # 32字节的AES密钥 # MyBatisPlus配置 mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 开启SQL日志调试用 global-config: db-config: id-type: auto3.4 第四步核心——创建自定义DataSource配置类这是最关键的一步。我们将创建一个Configuration类在其中提供一个DataSourceBean。这个Bean在创建时会主动从Environment中获取加密的密码调用EncryptUtils解密然后用解密后的密码构建HikariDataSource。import com.zaxxer.hikari.HikariConfig; import com.zaxxer.hikari.HikariDataSource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.env.Environment; import javax.sql.DataSource; Configuration public class EncryptedDataSourceConfig { Autowired private Environment env; // 用于获取原始配置 Autowired private EncryptUtils encryptUtils; // 我们的加解密工具 Bean public DataSource dataSource() { // 1. 从环境变量中读取原始配置此时password还是密文 String url env.getProperty(spring.datasource.url); String username env.getProperty(spring.datasource.username); String encryptedPassword env.getProperty(spring.datasource.password); // 密文密码 String driverClassName env.getProperty(spring.datasource.driver-class-name); // 2. 解密密码 String decryptedPassword encryptUtils.decrypt(encryptedPassword); if (decryptedPassword null) { throw new IllegalStateException(数据库密码解密失败请检查加密配置或密文。); } // 3. 手动创建HikariConfig并设置属性 HikariConfig config new HikariConfig(); config.setJdbcUrl(url); config.setUsername(username); config.setPassword(decryptedPassword); // 这里设置的是解密后的明文 config.setDriverClassName(driverClassName); // 4. 可选设置HikariCP连接池的其他属性可以从env中读取也可以硬编码 config.setConnectionTimeout(env.getProperty(spring.datasource.hikari.connection-timeout, Long.class, 30000L)); config.setMaximumPoolSize(env.getProperty(spring.datasource.hikari.maximum-pool-size, Integer.class, 20)); config.setMinimumIdle(env.getProperty(spring.datasource.hikari.minimum-idle, Integer.class, 5)); config.setConnectionTestQuery(SELECT 1); // MySQL健康检查语句 // 5. 创建并返回DataSource return new HikariDataSource(config); } }这个配置类是如何工作的Spring Boot启动加载application.yml所有属性进入Environment。Spring容器初始化扫描到EncryptedDataSourceConfig是一个配置类。执行dataSource()方法。此时Environment中的spring.datasource.password值仍是密文。方法内调用encryptUtils.decrypt()利用从外部环境变量或启动参数获取的密钥对密文进行解密得到明文密码。使用解密后的明文密码连同其他配置构建一个HikariDataSource实例。这个DataSourceBean被注册到Spring容器。由于我们手动提供了DataSourceBeanSpring Boot的DataSourceAutoConfiguration会自动退让不再自动配置数据源。MyBatisPlus的SqlSessionFactory等组件在初始化时会从容器中获取我们这个已经包含了正确密码的DataSourceBean从而建立正常的数据库连接。实操心得这里有一个非常重要的细节必须清空或排除Spring Boot默认的数据源自动配置吗在我们的写法中不需要。因为Spring Boot的自动配置遵循“约定优于配置”和“条件化装配”原则。当我们显式地在Configuration类中定义了一个DataSource类型的Bean时DataSourceAutoConfiguration会检测到容器中已存在DataSourceBean从而跳过自身的自动配置逻辑。这是一种优雅的“覆盖”无需使用Exclude注解。4. 密钥安全管理与运维实践实现了代码层面的加解密后整个安全链条中最脆弱的一环就变成了密钥本身的管理。如果密钥泄露加密形同虚设。下面分享几种在生产环境中管理密钥的常见模式。4.1 模式一通过系统环境变量传递推荐用于容器化部署这是目前容器化部署Docker, K8s中最常见和推荐的方式。密钥不写入任何代码或配置文件仅在容器启动时注入。操作步骤在application.yml中将encrypt.aes-key设置为空或一个占位符表示它必须由外部提供。encrypt: aes-key: ${ENCRYPT_AES_KEY:} # 优先从环境变量ENCRYPT_AES_KEY读取如果没有则为空在Dockerfile中不需要包含密钥。在启动容器时通过-e参数注入环境变量docker run -d -e ENCRYPT_AES_KEYyour-32byte-secure-aes-key-here your-app-image在K8s中通过Secret对象创建环境变量apiVersion: v1 kind: Secret metadata: name: app-secret type: Opaque data: encrypt-aes-key: base64编码后的密钥 # 注意K8s Secret的value需要base64编码 --- apiVersion: apps/v1 kind: Deployment spec: template: spec: containers: - name: app env: - name: ENCRYPT_AES_KEY valueFrom: secretKeyRef: name: app-secret key: encrypt-aes-key优点密钥与镜像完全分离符合12-Factor应用原则。运维人员可以在不接触代码的情况下管理密钥。缺点需要运维流程支持并且要确保拥有环境变量访问权限的人越少越好。4.2 模式二通过启动参数JVM参数传递类似于环境变量但作用域在JVM内。操作步骤同样在application.yml中使用占位符encrypt.aes-key: ${aes.key:}启动应用时使用-D参数java -jar your-app.jar -Daes.keyyour-32byte-secure-aes-key-here或者在JAVA_OPTS中设置。优点简单直接。缺点在进程列表如ps aux中可能看到明文参数存在泄露风险。通常需要结合操作系统权限严格控制。4.3 模式三从集中式配置中心获取适用于微服务架构在微服务架构中常使用Nacos、Apollo、Consul等作为配置中心。可以将加密密钥存放在配置中心应用启动时拉取。操作步骤在配置中心创建一个配置项例如encrypt.aes-key。在应用的bootstrap.ymlSpring Cloud项目中配置配置中心的地址。移除本地application.yml中的encrypt.aes-key配置。EncryptUtils类中的Value注解会自动从配置中心拉取该值。优点密钥集中管理安全审计和轮换方便与微服务架构天然契合。缺点引入了配置中心的依赖和复杂度。注意事项无论采用哪种模式密钥的生成和首次分发都必须通过安全的流程。建议使用强随机数生成器生成足够长度的密钥AES-256需32字节并仅通过安全的通道分发给授权的运维人员或系统。定期轮换密钥也是一个好习惯但需要协调应用重启或支持动态更新实现会更复杂。5. 常见问题排查与实战技巧在实际部署和运行中你可能会遇到以下问题。这里我整理了排查思路和解决方法。5.1 应用启动失败报错Failed to decrypt password或InvalidKeyException问题现象应用启动时在创建DataSourceBean阶段抛出解密相关的异常。排查步骤检查密钥一致性这是最常见的原因。确保用于解密的密钥与当初加密密码时使用的密钥完全一致包括大小写、空格和特殊字符。一个技巧是在安全的环境下写一个临时的主方法用当前获取到的密钥去尝试解密配置文件中的密文看是否能得到预期的明文。检查密文格式确保配置文件中存储的密文是纯Base64字符串没有多余的空白字符、换行或ENC()等包装除非你自定义了格式。可以使用在线的Base64解码工具验证密文是否是有效的Base64编码。检查算法/模式/填充确保加密和解密时使用的算法AES、工作模式如CBC、填充方式如PKCS5Padding以及IV如果使用完全一致。不同库的默认值可能不同。检查依赖冲突如果使用了Hutool、BouncyCastle等库检查是否存在多个版本冲突这可能导致加密解密行为不一致。使用mvn dependency:tree命令排查。5.2 连接池初始化成功但获取连接时报错Access denied for user问题现象应用能启动DataSourceBean创建成功但在执行第一条SQL时抛出认证失败异常。排查步骤确认解密结果在EncryptUtils.decrypt()方法返回后立即打印或日志记录解密后的密码注意生产环境切勿日志记录真实密码可记录密码长度或首尾字符进行模糊比对。确认解密出的密码就是你认为的那个密码。检查用户名和URL密码正确但认证失败也可能是用户名或数据库地址错了。确认url中的数据库名、username是否正确。数据库权限使用解密得到的密码通过MySQL客户端如mysql -u username -p手动连接数据库验证该用户是否有从应用服务器IP地址访问指定数据库的权限。5.3 在IDE中运行正常但打包成Jar后运行失败问题现象在开发环境IntelliJ IDEA或Eclipse中启动一切正常但使用mvn package打包后通过java -jar运行报错。排查步骤检查配置文件激活确保打包后正确的application.yml或application-prod.yml被包含在jar包的BOOT-INF/classes目录下并且其内容尤其是密文没有在构建过程中被意外修改或过滤。检查环境变量/参数传递在IDE中你可能在运行配置里设置了环境变量或VM参数。打包后运行需要通过命令行重新传递这些参数。例如java -jar -Daes.keyxxx your-app.jar。检查依赖打包确保加解密相关的依赖如hutool-all已被正确打包进BOOT-INF/lib目录。可以解压jar包查看。5.4 如何加密现有的明文配置文件这是一个运维操作。你可以写一个简单的主程序或者使用单元测试来调用EncryptUtils.encrypt()方法。public class PasswordEncryptor { public static void main(String[] args) { // 初始化一个临时的AES实例密钥需与生产环境一致 String key this-is-your-32byte-production-aes-key; byte[] iv 1234567890123456.getBytes(); AES aes new AES(Mode.CBC, Padding.PKCS5Padding, key.getBytes(), iv); String plainPassword your_database_password; String encrypted Base64.getEncoder().encodeToString(aes.encrypt(plainPassword)); System.out.println(加密后的密码 (Base64): encrypted); // 将输出结果复制到配置文件的 password: 后面 } }重要提示执行此操作的机器和环境必须是绝对安全的并且加密完成后应立即从代码仓库、日志和终端历史中清除明文的密码和密钥。5.5 与Druid连接池集成有何不同如果你使用阿里巴巴的Druid连接池原理完全一样只是在创建DataSourceBean时略有不同。Bean public DataSource dataSource() throws Exception { // ... 获取url, username, 解密password ... // 使用DruidDataSource DruidDataSource dataSource new DruidDataSource(); dataSource.setUrl(url); dataSource.setUsername(username); dataSource.setPassword(decryptedPassword); dataSource.setDriverClassName(driverClassName); // 设置Druid特有配置 dataSource.setInitialSize(5); dataSource.setMaxActive(20); dataSource.setValidationQuery(SELECT 1); // ... 其他配置 return dataSource; }Druid还提供了内置的配置解密功能通过config.decrypt等属性但其与特定的加密方式绑定。自定义DataSource的方式更为通用和自由。6. 方案对比与扩展思考让我们回顾并对比一下文中提到的几种方案特性自定义DataSourceEnvironmentPostProcessorJasypt Starter实现复杂度中等需编写配置类较高需实现并注册接口低仅需依赖和配置灵活性高可自定义任何算法和逻辑高可修改任何配置属性中受限于库支持的功能和算法侵入性低仅影响DataSource Bean中影响整个Environment低但依赖特定starter密钥管理灵活可集成任何方式灵活可集成任何方式需适配库的约定如系统属性可维护性高代码清晰易于理解和调试中逻辑隐藏在启动早期阶段中依赖第三方库黑盒化推荐场景中大型项目对安全和可控性要求高需要深度定制配置加载流程的项目小型项目、原型验证、快速上线扩展思考动态刷新密钥在微服务架构下我们可能希望在不重启应用的情况下轮换加密密钥。这可以通过以下思路实现将EncryptUtils中的密钥从Value注入改为从ConfigurationProperties绑定到一个类并让这个类监听配置中心如Nacos的配置变更事件。当密钥变更时触发一个事件销毁旧的DataSourceBean并重新创建新的。注意这需要非常谨慎地处理因为重建DataSource会导致所有数据库连接中断可能影响正在进行的业务。通常需要配合连接池的优雅关闭和业务端的重试机制。最后一点个人体会数据库配置加密是应用安全“纵深防御”中非常基础但至关重要的一环。采用自定义DataSource的方案虽然需要多写几十行代码但它给了团队对安全流程的完全掌控力。在项目初期就引入这套机制并将其作为标准基建的一部分能有效避免后期因安全审计或漏洞扫描不过关而带来的被动和返工。记住安全无小事从最基础的连接密码加密做起培养团队的安全意识其价值远超过技术实现本身。