Spring-Security-Core源码分析之BCryptPasswordEncoder

时间:2024-04-06 21:00:25

背景

在授权的管理方面,Spring security 不愧是一个成熟的框架,本身有spring这个大的生态支持,再加上近几年Spring Boot的大力支持,现在Spring security在授权方面的入门门槛高的问题,也被解决了。这些问题被解决了,那么咱们是不是可以深入的了解一下spring security的一些设计。下面从spring security推荐的一个密码加密工具类来作为一个引导,会在将来一步一步的深入。

接口PasswordEncoder

接口设计的简单明了,一个用来加密密码,另一个用来匹配是否与原密码相等,接口本身在org.springframework.security.crypto.password包下,crypto是为了提供通用的加密和哈希算法的,

Spring-Security-Core源码分析之BCryptPasswordEncoder

Bcrypt

bcrypt是一个由Niels Provos以及David Mazières根据Blowfish加密算法所设计的密码散列函数,于1999年在USENIX中展示。实现中bcrypt会使用一个加盐的流程以防御彩虹表攻击,同时bcrypt还是适应性函数,它可以借由增加迭代之次数来抵御日益增进的计算机运算能力透过暴力法**。

在Bcrypt之前大部分的加密密码的操作有Md4,Md5等等的hash摘要算法,来处理密码的单向加密,但是随着彩虹表之类的工具库存在和hash碰撞的,用md5之类的处理密码的加密并不是一个非常安全的操作,Bcrypt是一个更好的加密算法,它加密的时候,同一个密码加密后的值不同,大概原理是,在hash时加入salt值来处理,让hash后的结果都不同,还有为了方便数据库不更改,这个算法可以保证同一个不需要再单独保存salt值,因为他在加密后会把salt值和hash之后的值都存在加密后的字符串中。那么接下来我们开始介绍java中的实现

BCryptPasswordEncoder

spring securit中已經提供了默认的一个bcrypt的密码加密工具类,本身该类提供了三个构造方法,无参数的,有一个参数的(参数为密码强度),有两个参数的(密码强度,随机因子)。

Spring-Security-Core源码分析之BCryptPasswordEncoder
无参构造方法,如下
无参构造方法会传一个-1到一个有参数的

	public BCryptPasswordEncoder() {
		this(-1);
	}

一个参数的,可以设置密码强度

	/**
	 * @param strength the log rounds to use, between 4 and 31
	 */
	public BCryptPasswordEncoder(int strength) {
		this(strength, null);
	}

可以设置密码强度和安全随机因子。并且密码强度本身是有最大值和最小值的限制的。分别是4到31

/**
	 * @param strength the log rounds to use, between 4 and 31
	 * @param random the secure random instance to use
	 *
	 */
	public BCryptPasswordEncoder(int strength, SecureRandom random) {
		//BCrypt.MIN_LOG_ROUNDS = 4 and BCrypt.MAX_LOG_ROUNDS = 31
		if (strength != -1 && (strength < BCrypt.MIN_LOG_ROUNDS || strength > BCrypt.MAX_LOG_ROUNDS)) {
			throw new IllegalArgumentException("Bad strength");
		}
		this.strength = strength;
		this.random = random;
	}

这里抛出一个问题?为什么密码的最大强度和最小强度要大于等于4或者小于等于31
介绍为构造方法后,我们说一下,一般情况下我们使用默认的无参构造方法就可以,简单操作方便

加密方法本身只需要传入原始密码即可,传入后,原始密码根据,如果强度小于0时,salt的值为默认生成,这个遂成随机盐的方法咱们在下一步仔细看,这个方法中并没有太多的具体操作,具体实现都在BCrypt类中。

	public String encode(CharSequence rawPassword) {
		String salt;
		if (strength > 0) {
			if (random != null) {
				salt = BCrypt.gensalt(strength, random);
			}
			else {
				salt = BCrypt.gensalt(strength);
			}
		}
		else {
			salt = BCrypt.gensalt();
		}
		return BCrypt.hashpw(rawPassword.toString(), salt);
	}

bcrypt的密码本身是有规律的,在判断密码是否相等时候,通过一个正则表达式来判断是否匹配,如果不匹配,直接返回,用来节省资源,如果匹配的话,调用BCrypt中的checkpw方法来验证。

	public boolean matches(CharSequence rawPassword, String encodedPassword) {
		if (encodedPassword == null || encodedPassword.length() == 0) {
			logger.warn("Empty encoded password");
			return false;
		}

		if (!BCRYPT_PATTERN.matcher(encodedPassword).matches()) {
			logger.warn("Encoded password does not look like BCrypt");
			return false;
		}

		return BCrypt.checkpw(rawPassword.toString(), encodedPassword);
	}

BCrypt

属性
Spring-Security-Core源码分析之BCryptPasswordEncoder
分别解释一下各个属性的作用

GENSALT_DEFAULT_LOG2_ROUNDS 默认值是10
BCRYPT_SALT_LEN 默认盐值长度16
BLOWFISH_NUM_ROUNDS 布鲁斯轮询参数为16
P_orig 这个是啥呢?
S_orig
bf_crypt_ciphertext bcrypt的矢量值
base64_code base64代码表中包含的字符
index_64 base64代码表中的索引
MIN_LOG_ROUNDS 最小循环值为4
MAX_LOG_ROUNDS 最大循环值为31
Spring-Security-Core源码分析之BCryptPasswordEncoder
生成盐值,调用默认的盐值,默认值为10

	/**
	 * Generate a salt for use with the BCrypt.hashpw() method, selecting a reasonable
	 * default for the number of hashing rounds to apply
	 * @return an encoded salt value
	 */
	public static String gensalt() {
		//GENSALT_DEFAULT_LOG2_ROUNDS = 10
		return gensalt(GENSALT_DEFAULT_LOG2_ROUNDS);
	}

	/**
	 * Generate a salt for use with the BCrypt.hashpw() method
	 * @param log_rounds the log2 of the number of rounds of hashing to apply - the work
	 * factor therefore increases as 2**log_rounds. Minimum 4, maximum 31.
	 * @return an encoded salt value
	 */
	public static String gensalt(int log_rounds) {
		return gensalt(log_rounds, new SecureRandom());
	}

SecureRandom VS Random

好,这里看见用了SecureRandom,强随机数生成器工具,这里简单的介绍一下

所以在需要频繁生成随机数,或者安全要求较高的时候,不要使用Random,因为其生成的值其实是可以预测的。

  • SecureRandom类提供加密的强随机数生成器 (RNG)
  • 当然,它的许多实现都是伪随机数生成器 (PRNG) 形式,这意味着它们将使用确定的算法根据实际的随机种子生成伪随机序列
  • 也有其他实现可以生成实际的随机数
  • 还有另一些实现则可能结合使用这两项技术

SecureRandom和Random都是,也是如果种子一样,产生的随机数也一样: 因为种子确定,随机数算法也确定,因此输出是确定的。

只是说,SecureRandom类收集了一些随机事件,比如鼠标点击,键盘点击等等,SecureRandom 使用这些随机事件作为种子。这意味着,种子是不可预测的,而不像Random默认使用系统当前时间的毫秒数作为种子,有规律可寻。

继续查看生成随机盐值

gensalt

/**
	 * Generate a salt for use with the BCrypt.hashpw() method
	 * @param log_rounds the log2 of the number of rounds of hashing to apply - the work
	 * factor therefore increases as 2**log_rounds. Minimum 4, maximum 31.
	 * @param random an instance of SecureRandom to use
	 * @return an encoded salt value
	 */
	public static String gensalt(int log_rounds, SecureRandom random) {
		if (log_rounds < MIN_LOG_ROUNDS || log_rounds > MAX_LOG_ROUNDS) {
			throw new IllegalArgumentException("Bad number of rounds");
		}
		StringBuilder rs = new StringBuilder();
		byte rnd[] = new byte[BCRYPT_SALT_LEN];
		// 把随机结果返回到指定长度byte数组中
		random.nextBytes(rnd);

		rs.append("$2a$");
		if (log_rounds < 10) {
			rs.append("0");
		}
		rs.append(log_rounds);
		rs.append("$");
		encode_base64(rnd, rnd.length, rs);
		return rs.toString();
	}

通过上面的方法可以看出,盐值构成有$2a$开始,中间跟定的是轮询长度,后面是$ 然后把生成的随机数转换为Base64,最后把结果输出,下面咱们看看,转换Base64的具体操作。

	/**
	 * 使用bcrypt修改的base64编码方案对字节数组进行编码。请注意,这与标准MIME-base64编码不兼容。
	 *
	 * @param d the byte array to encode
	 * @param len the number of bytes to encode
	 * @param rs the destination buffer for the base64-encoded string
	 * @exception IllegalArgumentException if the length is invalid
	 */
	static void encode_base64(byte d[], int len, StringBuilder rs)
			throws IllegalArgumentException {
		int off = 0;
		int c1, c2;

		if (len <= 0 || len > d.length) {
			throw new IllegalArgumentException("Invalid len");
		}

		while (off < len) {
			// 从随机字节数组中取出数据与 0xff 做与位运算的值作为c1,为什么这么做?同学们思考一下 
			// 1. 只是为了取得低八位
			// 2. 保证补码的一致性
			c1 = d[off++] & 0xff;
			// 把base64数组中的值取出来,下标索引的计算方式为 c1 >> 2 右位移两次,并且与0x3f做与位运算,感兴趣的同学也思考一下为什么这里需要向右2个位移运算然后3f做与操作?
			// 1. 向右位移两位相当于之前数字的4/1
			// 2. 与0x3f做与操作相当于给64取余
			// 这么做的结果之一是效率高,主要是根据把随机的值转换为base64数组中的下标索引
			
			rs.append(base64_code[(c1 >> 2) & 0x3f]);
			// 这步操作呢?
			c1 = (c1 & 0x03) << 4;
			// 符合条件结束本次操作
			if (off >= len) {
				rs.append(base64_code[c1 & 0x3f]);
				break;
			}
			// 已经有c1了,同学们考虑考虑为什么这里还需要c2,代码核心内容同c1
			c2 = d[off++] & 0xff;
			// 计算c1 的值或等 是取低四位
			c1 |= (c2 >> 4) & 0x0f;
			// 取余后获取base64字符串
			rs.append(base64_code[c1 & 0x3f]);
			// 取低四位,并且是之前的2倍
			c1 = (c2 & 0x0f) << 2;
			// 如果符合条件则退出
			if (off >= len) {
				rs.append(base64_code[c1 & 0x3f]);
				break;
			}
			// 继续操作
			c2 = d[off++] & 0xff;
			c1 |= (c2 >> 6) & 0x03;
			rs.append(base64_code[c1 & 0x3f]);
			rs.append(base64_code[c2 & 0x3f]);
		}
	}

介于篇幅本片先到此结束。下一篇会更详细的进行密码的加密操作