JDK Security 底层分析

Security相关实现

基本使用

ini 复制代码
// 使用默认的随机数生成策略
SecureRandom secureRandom = new SecureRandom();
int i = secureRandom.nextInt();

Security 相关类加载过程:

java.security.Security#initialize: 加载lib\security\java.security文件, 会将相关的属性放入props 变量中。

java.security -->

ini 复制代码
security.provider.1=sun.security.provider.Sun
security.provider.2=sun.security.rsa.SunRsaSign
security.provider.3=sun.security.ec.SunEC

在Providers的静态代码块中会进行初始化:

ini 复制代码
static {
        providerList = ProviderList.EMPTY;
    	// ProviderList: 保存所有的Provider,可以自定义向其添加
        //      configs:会依次读取security.provider.x 相关属性,生成一个一个的ProviderConfig
        //      userList: get() 会执行getProvider(), 获取configs 中的名称反射创建Provider实例(如:sun.security.provider.Sun)
        providerList = ProviderList.fromSecurityProperties();
        jarVerificationProviders = new String[]{"sun.security.provider.Sun", "sun.security.rsa.SunRsaSign", "sun.security.ec.SunEC", "sun.security.provider.VerificationProvider"};
    }

实例化相关的Provider

每一个具体Provider都继承了 Provider类 (Provider类继承 Properties), 这里分析下sun.security.provider.Sun 初始化过程

typescript 复制代码
 public Sun() {
        // 设置当前Provider 状态信息
        super("SUN", 1.8d, INFO);
        // 初始化 map
        SunEntries.putEntries(this);
    }
// map 即当前Provider 实例
static void putEntries(Map<Object, Object> map) {
    if (nativeAvailable && useNativePRNG) {
        map.put("SecureRandom.NativePRNG",
                "sun.security.provider.NativePRNG");
    }
    map.put("SecureRandom.SHA1PRNG",
            "sun.security.provider.SecureRandom");
    ....

put 的时候会执行Provider#put,添加到Provider#legacyStrings的map集合 entry (key,val), key=type.algr, value = implClass

在获取对应算法Instance 实例的时候, 先获取Service 对象,然后再实例化对应的实例

会首先执行:sun.security.jca.ProviderList.getService, 从所有Provider 中查找相关算法实现 执行: Provider#getServices , 会加载legacyStrings 初始化生成 Service对象 (type, algorithm, className), 放入Provider#legacyMap,下一次直接通过legacyMap获取对应的Service

ini 复制代码
// 内部会查找type 为 SecureRandom、算法为SHA1PRNG(默认) 的service进行初始化
SecureRandom secureRandom = new SecureRandom();

// 注册BouncyCastle Provider: 一个三方提供的加密库
Security.addProvider(new BouncyCastleProvider());
// 在所有Provider 中查找 type 为 MessageDigest、 算法为RipeMD160的Service 进行实例化
MessageDigest md = MessageDigest.getInstance("RipeMD160");

// 获取SunMSCAPI 中的SecureRandom 实现,算法为Windows-PRNG 的Service。 (windows 平台特有的实现: CryptGenRandom)
Provider.Service service = Security.getProvider("SunMSCAPI").getService("SecureRandom", "Windows-PRNG");
// 通过Service构造SecureRandom 内部对象。
SecureRandomSpi secureRandomSpi = (SecureRandomSpi) service.newInstance(null);

// 内部调用跟上面相同,先获取到Service,然后创建对象
SecureRandom instance = SecureRandom.getInstance("Windows-PRNG", "SunMSCAPI");
SecureRandom instance1 = SecureRandom.getInstance("SHA1PRNG", "SUN");

在JDK 中提供的算法实现中,大多数内部都有一个XXXSpi类来实现核心逻辑,如:KeyPairGenerator--> KeyPairGeneratorSpi, MessageDigest--> MessageDigestSpi, KeyFactory--> KeyFactorySpi

随机数:

Random: 默认线性生成,容易被预测。 内部采用CAS更新种子,多线程会带来性能问题 ThreadLocalRandom: 继承Random,存储种子变为每个线程存储,性能高。 同样不符合标准安全协议。 SecureRandom: 继承Random, 采用强加密算法。 每次生成随机数都会加同步锁 windows: sun.security.provider.SecureRandom,内部实际上使用SHA1算法处理(JDK17 采用DRBG)。 linux: 默认使用NativePRNG, 采用混杂模式。

Linux下的SecureRandom

核心类:sun.security.provider.NativePRNG

linux 自定义种子生成器: 添加vm参数 java.security.egd=

  • file:/dev/random 阻塞模式, sun.security.provider.NativePRNG$Blocking
  • file:/dev/urandom 非阻塞模式, sun.security.provider.NativePRNG$NonBlocking

如果没有指定java.security.egd, 则使用java.security文件中的默认设置: securerandom.source=file:/dev/random 作为seed 生成器

Linux平台使用SecureRandom时, 默认混杂模式: 种子由参数决定(默认/dev/random), buffer由 /dev/urandom ,生成过程如下:

  1. 使用SecureRandom的默认策略(SHA1)生成随机数
  2. nextIn(/dev/urandom) 文件描述符生成buffer。(buffer有剩余,且距离上次不到100 ms 则不会生成)
  3. 将1生成的与2生成的进行异或

因此Linux默认情况下使用SecureRandom,只要不调用生成种子方法,就不会使用/dev/random, 不会造成阻塞。

在linux 中也可以使用上面文件描述符直接生成随机数,如:

typescript 复制代码
# 生成13为指定字符随机数
[root@k8s-node1 dev]# tr -dc A-Za-z0-9 </dev/random | head -c 13 ; echo ''
kkHMXCbpSGxoX

EGD机制

在使用过程中经常看到加参数:-Djava.security.egd=file:/dev/./urandom, 而不是直接加/dev/urandom。

这是为了解决在早期的JDK中 会尝试通过一个套接字(socket)连接到 /dev/urandom,而不是直接使用文件,这会导致性能下降。 在中间加入一个./ 即可绕过EGD机制,从而直接使用文件。

现代JDK 中这种EGD机制早已经被废弃,因此也不需要加入./了。

相关推荐
华仔啊几秒前
Spring 配置混乱?搞懂这两个核心组件,问题真能少一半
java·后端·spring
喂完待续12 分钟前
【序列晋升】45 Spring Data Elasticsearch 实战:3 个核心方案破解索引管理与复杂查询痛点,告别低效开发
java·后端·spring·big data·spring data·序列晋升
郑重其事,鹏程万里15 分钟前
commons-exec
java
龙茶清欢16 分钟前
具有实际开发参考意义的 MyBatis-Plus BaseEntity 基类示例
java·spring boot·spring cloud·mybatis
神龙斗士24019 分钟前
Java 数组的定义与使用
java·开发语言·数据结构·算法
计算机学姐20 分钟前
基于微信小程序的扶贫助农系统【2026最新】
java·vue.js·spring boot·mysql·微信小程序·小程序·mybatis
白露与泡影20 分钟前
2025互联网大厂高频Java面试真题解析
java·开发语言·面试
forever銳21 分钟前
java中如何保证接口幂等性
java·后端
柯南二号23 分钟前
【Java后端】MyBatis 和 MyBatis-Plus (MP) 的区别
java·数据库·tomcat
C++chaofan27 分钟前
游标查询在对话历史场景下的独特优势
java·前端·javascript·数据库·spring boot