SpringSecurity-密码存储方式

密码存储历史:

最初,密码都以明文的方式进行存储,如用户设置的密码是123,那么落库的密码就是123。然而,恶意用户利用诸如sql注入或其他手段,获取到了数据库中的明文密码,此时用户在当前系统的隐私以及权限遭到泄露,更糟糕的是,如若用户有多系统共用一个密码的习惯,那么危害会更大(例如本人,qq、微信等许多账号都用一个密码)。

开发人员开始利用单向散列的方式存储密码,也可以叫做消息摘要算法,通过此算法计算后得到的值叫做散列,此算法有很多实现,例如SHA256、MD5、HMAC等。此方式的特点:

  1. 单向性。原密码通过计算,散列出所谓的“加密“密码,但并不能由加密后的密码反推出原明文密码。
  2. 无论输入的密码多长,都会散列输出为定长。通常为128~256位之间,越长越安全。
    注意,单向散列并不是一种加密算法,真正的加密算法,是可以通过相应算法生成密文,又可以通过相应算法反身生成原文,是双向的,通常被分为两大类:对称式和非对称式。对称式的方式,加密和解密用的是一个密钥。非对称式,加密用的密钥叫公钥,解密用的密钥叫私钥,公钥和私钥都是密钥。

使用这种方式,我们落库的密码为明文密码的散列值,当需要登录认证时,就将用户输入的明文密码用相同的散列算法进行散列,与数据库中所存散列进行比较。

通过这种方式,使用户密码变得安全了,就算数据库中的密码泄露出去,也只是一个个的散列值,恶意用户使用散列值登录系统,并不会登陆成功。
但是没有银弹,看似安全了,依旧有被破解的可能。黑客可以对每一个密码散列进行穷举计算,如果破解大量密码,就会消耗大量时间,所以他们做了一张字典表。把所有简单密码的散列一次性算出存储其中,此方式需要大量的存储空间,但换回了时间。穷举字典之所以只计算存储简单的密码,是因为密码过于复杂的系统,要穷举出所有散列,那么空间的代价是巨大的,甚至可能没有上限,所以他们对穷举字典进行改进,做出一张叫彩虹表的东西。彩虹表不对所有密码进行穷举计算存储,只是存储一些特定的散列值,每个散列都能反推出一条链的明文信息,这样的话,存储空间变少了,虽然在需要破解时还会进行计算,但消耗的时间总是好过最初的暴力散列破解(彩虹表很复杂,可以查阅相关专业资料)。

为了降低彩虹表的有效性,开发人员可以在原密码中加入一串随机字符,称作,每次散列,都是散列盐和密码拼接后的字符串,这样,就算是散列值泄露,通过彩虹表找出原值,也只是和盐组合后的原值而已,仍然不会登录成功,当然了,这也只是降低了彩虹表的有效性。

由于当代计算机的运算速度越来越快,对输入的消息进行散列也越来越快,黑客们的计算速度也越来越快,此时传统的单向散列算法可能不够安全了,我们可以使用自适应的单向散列算法,它可以有意的占用大量的CPU、内存,使散列过程更耗时,这样就等于降低了恶意用户计算机的速度。我们可以指定让这个算法的散列过程有多耗时,根据您系统的吞吐量以及访问情况,酌情设置,建议是1秒,这样既可以对恶意用户造成打击,又不会给系统造成压力。

SpringSecurity对密码的处理:

SpringSecurity5.3版本以后,官方推荐使用由自适应单向散列算法实现的编码器对密码进行散列。框架内置的编码器如下:

 * bcrypt - {@link BCryptPasswordEncoder} (Also used for encoding)
 * argon2 - {@link Argon2PasswordEncoder}
 * pbkdf2 - {@link Pbkdf2PasswordEncoder}
 * scrypt - {@link SCryptPasswordEncoder}
 ---------------------------------------------------------------
 * ldap - {@link org.springframework.security.crypto.password.LdapShaPasswordEncoder}
 * MD4 - {@link org.springframework.security.crypto.password.Md4PasswordEncoder}
 * MD5 - {@code new MessageDigestPasswordEncoder("MD5")}
 * noop - {@link org.springframework.security.crypto.password.NoOpPasswordEncoder}
 * SHA-1 - {@code new MessageDigestPasswordEncoder("SHA-1")}
 * SHA-256 - {@code new MessageDigestPasswordEncoder("SHA-256")}
 * sha256 - {@link org.springframework.security.crypto.password.StandardPasswordEncoder}

其中BCryptPasswordEncoderArgon2PasswordEncoderPbkdf2PasswordEncoderSCryptPasswordEncoder
均是由自适应单向散列算法实现的密码编码器,其他几种编码器被认为不在安全,但是考虑到有些老系统的依赖性,所以SpringSecurity并
没有将他们移除,但均被标注为过时状态,不建议使用。

为了增加对密码散列的扩展性以及适应性,SpringSecurity5.3以后还加入了一个名为DelegatingPasswordEncoder的编码器,它持有所有的密码编码器,扩展性以及适应性极强,可以轻松完成各种密码系统的过渡。所以,就算我们丢弃掉SpringSecurity框架内置的验证过滤器自己实现一个,也推荐采用此编码器进行密码的加密以及校验。

DelegatingPasswordEncoder的工作原理

DelegatingPasswordEncoder是一个密码编码器,它的功能是当我们保存用户密码的时候,对密码进行散列。当我们登录系统的时候,对密码进行验证。但是,真正做散列和比较的其实是上边我们提到的某个编码器,这更像是一种外观模式。

它默认被框架通过PasswordEncoderFactories初始化,并以HashMap的方式持有所有类型的编码器,同时指定了一个默认加密的编码器BCryptPasswordEncoder,当我们调用DelegatingPasswordEncoder加密的时候,就调用这个默认加密的编码器散列加密。

每一个编码器都有一个存入HashMap的键,也叫id,就像我上边列出的一样,比如BCryptPasswordEncoder的id就是bcrypt
当某个真正干活的编码器对密码进行散列后,它会把干活的编码器id用{}拼接到散列后的密码。例如:DelegatingPasswordEncoder的默认干活的编码器是BCryptPasswordEncoder,如果我们不做调整,那么密码散列后的值就是$2a$10$ka.goPOwDmQSPQnTx0ROWu./3oebqdPYaTKp7MlI796IBt51PasCO,最后返回的会是{bcrypt }$2a$10$ka.goPOwDmQSPQnTx0ROWu./3oebqdPYaTKp7MlI796IBt51PasCO,在登陆系统需要进行比对的时候,需要从散列后的密码中根据{id}分析出用的哪个编码器,然后对用户当前键入的密码用此编码器进行散列,最后比较。

以下是PasswordEncoderFactories源码,用来创建一个DelegatingPasswordEncoder,它很简单:

public class PasswordEncoderFactories {

	/**
	 * Creates a {@link DelegatingPasswordEncoder} with default mappings. Additional
	 * mappings may be added and the encoding will be updated to conform with best
	 * practices. However, due to the nature of {@link DelegatingPasswordEncoder} the
	 * updates should not impact users. The mappings current are:
	 *
	 * <ul>
	 * <li>bcrypt - {@link BCryptPasswordEncoder} (Also used for encoding)</li>
	 * <li>ldap - {@link org.springframework.security.crypto.password.LdapShaPasswordEncoder}</li>
	 * <li>MD4 - {@link org.springframework.security.crypto.password.Md4PasswordEncoder}</li>
	 * <li>MD5 - {@code new MessageDigestPasswordEncoder("MD5")}</li>
	 * <li>noop - {@link org.springframework.security.crypto.password.NoOpPasswordEncoder}</li>
	 * <li>pbkdf2 - {@link Pbkdf2PasswordEncoder}</li>
	 * <li>scrypt - {@link SCryptPasswordEncoder}</li>
	 * <li>SHA-1 - {@code new MessageDigestPasswordEncoder("SHA-1")}</li>
	 * <li>SHA-256 - {@code new MessageDigestPasswordEncoder("SHA-256")}</li>
	 * <li>sha256 - {@link org.springframework.security.crypto.password.StandardPasswordEncoder}</li>
	 * <li>argon2 - {@link Argon2PasswordEncoder}</li>
	 * </ul>
	 *
	 * @return the {@link PasswordEncoder} to use
	 */
	@SuppressWarnings("deprecation")
	public static PasswordEncoder createDelegatingPasswordEncoder() {
		String encodingId = "bcrypt";
		Map<String, PasswordEncoder> encoders = new HashMap<>();
		encoders.put(encodingId, new BCryptPasswordEncoder());
		encoders.put("ldap", new org.springframework.security.crypto.password.LdapShaPasswordEncoder());
		encoders.put("MD4", new org.springframework.security.crypto.password.Md4PasswordEncoder());
		encoders.put("MD5", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("MD5"));
		encoders.put("noop", org.springframework.security.crypto.password.NoOpPasswordEncoder.getInstance());
		encoders.put("pbkdf2", new Pbkdf2PasswordEncoder());
		encoders.put("scrypt", new SCryptPasswordEncoder());
		encoders.put("SHA-1", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("SHA-1"));
		encoders.put("SHA-256", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("SHA-256"));
		encoders.put("sha256", new org.springframework.security.crypto.password.StandardPasswordEncoder());
		encoders.put("argon2", new Argon2PasswordEncoder());

		return new DelegatingPasswordEncoder(encodingId, encoders);
	}

	private PasswordEncoderFactories() {}
}

以下是DelegatingPasswordEncoder源码,结构也很简单:

public class DelegatingPasswordEncoder implements PasswordEncoder {
	private static final String PREFIX = "{";
	private static final String SUFFIX = "}";
	private final String idForEncode;
	private final PasswordEncoder passwordEncoderForEncode;
	private final Map<String, PasswordEncoder> idToPasswordEncoder;
	private PasswordEncoder defaultPasswordEncoderForMatches = new UnmappedIdPasswordEncoder();

	/**
	 * Creates a new instance
	 * @param idForEncode the id used to lookup which {@link PasswordEncoder} should be
	 * used for {@link #encode(CharSequence)}
	 * @param idToPasswordEncoder a Map of id to {@link PasswordEncoder} used to determine
	 * which {@link PasswordEncoder} should be used for {@link #matches(CharSequence, String)}
	 */
	public DelegatingPasswordEncoder(String idForEncode,
		Map<String, PasswordEncoder> idToPasswordEncoder) {
		if (idForEncode == null) {
			throw new IllegalArgumentException("idForEncode cannot be null");
		}
		if (!idToPasswordEncoder.containsKey(idForEncode)) {
			throw new IllegalArgumentException("idForEncode " + idForEncode + "is not found in idToPasswordEncoder " + idToPasswordEncoder);
		}
		for (String id : idToPasswordEncoder.keySet()) {
			if (id == null) {
				continue;
			}
			if (id.contains(PREFIX)) {
				throw new IllegalArgumentException("id " + id + " cannot contain " + PREFIX);
			}
			if (id.contains(SUFFIX)) {
				throw new IllegalArgumentException("id " + id + " cannot contain " + SUFFIX);
			}
		}
		this.idForEncode = idForEncode;
		this.passwordEncoderForEncode = idToPasswordEncoder.get(idForEncode);
		this.idToPasswordEncoder = new HashMap<>(idToPasswordEncoder);
	}

	/**
	 * Sets the {@link PasswordEncoder} to delegate to for
	 * {@link #matches(CharSequence, String)} if the id is not mapped to a
	 * {@link PasswordEncoder}.
	 *
	 * <p>
	   The encodedPassword provided will be the full password
	 * passed in including the {"id"} portion.* For example, if the password of
	 * "{notmapped}foobar" was used, the "id" would be "notmapped" and the encodedPassword
	 * passed into the {@link PasswordEncoder} would be "{notmapped}foobar".
	 * </p>
	 * @param defaultPasswordEncoderForMatches the encoder to use. The default is to
	 * throw an {@link IllegalArgumentException}
	 */
	public void setDefaultPasswordEncoderForMatches(
		PasswordEncoder defaultPasswordEncoderForMatches) {
		if (defaultPasswordEncoderForMatches == null) {
			throw new IllegalArgumentException("defaultPasswordEncoderForMatches cannot be null");
		}
		this.defaultPasswordEncoderForMatches = defaultPasswordEncoderForMatches;
	}

	@Override
	public String encode(CharSequence rawPassword) {
		return PREFIX + this.idForEncode + SUFFIX + this.passwordEncoderForEncode.encode(rawPassword);
	}

	@Override
	public boolean matches(CharSequence rawPassword, String prefixEncodedPassword) {
		if (rawPassword == null && prefixEncodedPassword == null) {
			return true;
		}
		String id = extractId(prefixEncodedPassword);
		PasswordEncoder delegate = this.idToPasswordEncoder.get(id);
		if (delegate == null) {
			return this.defaultPasswordEncoderForMatches
				.matches(rawPassword, prefixEncodedPassword);
		}
		String encodedPassword = extractEncodedPassword(prefixEncodedPassword);
		return delegate.matches(rawPassword, encodedPassword);
	}

	private String extractId(String prefixEncodedPassword) {
		if (prefixEncodedPassword == null) {
			return null;
		}
		int start = prefixEncodedPassword.indexOf(PREFIX);
		if (start != 0) {
			return null;
		}
		int end = prefixEncodedPassword.indexOf(SUFFIX, start);
		if (end < 0) {
			return null;
		}
		return prefixEncodedPassword.substring(start + 1, end);
	}

	@Override
	public boolean upgradeEncoding(String prefixEncodedPassword) {
		String id = extractId(prefixEncodedPassword);
		if (!this.idForEncode.equalsIgnoreCase(id)) {
			return true;
		}
		else {
			String encodedPassword = extractEncodedPassword(prefixEncodedPassword);
			return this.idToPasswordEncoder.get(id).upgradeEncoding(encodedPassword);
		}
	}

	private String extractEncodedPassword(String prefixEncodedPassword) {
		int start = prefixEncodedPassword.indexOf(SUFFIX);
		return prefixEncodedPassword.substring(start + 1);
	}

	/**
	 * Default {@link PasswordEncoder} that throws an exception that a id could
	 */
	private class UnmappedIdPasswordEncoder implements PasswordEncoder {

		@Override
		public String encode(CharSequence rawPassword) {
			throw new UnsupportedOperationException("encode is not supported");
		}

		@Override
		public boolean matches(CharSequence rawPassword,
			String prefixEncodedPassword) {
			String id = extractId(prefixEncodedPassword);
			throw new IllegalArgumentException("There is no PasswordEncoder mapped for the id \"" + id + "\"");
		}
	}
}

感兴趣的可以自己打开源码看一看这两个类,以及上边提到的各种密码编码器,后续SpringSecurity源码,我会深入讲解他们的用处。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Spring Security OAuth2提供了一个基于数据库的认证和授权方案,可以使用SQL数据库来存储和管理用户信息和授权信息。 在Spring Security OAuth2中,可以使用JDBC来配置和管理数据库连接。需要配置数据源和相关的JDBC驱动,以便连接到SQL数据库。可以使用Spring Boot提供的自动配置功能来简化配置过程。 一般来说,需要创建一个具有特定表结构的SQL数据库来存储用户和授权信息。Spring Security OAuth2提供了一组默认的表结构,可以通过执行相应的SQL脚本来创建这些表。这些表包括:oauth_client_details、oauth_access_token、oauth_refresh_token等。 在配置文件中,需要指定数据库连接信息,如数据库URL、用户名、密码等。可以使用类似以下的配置来配置数据库连接: ``` spring.datasource.url=jdbc:mysql://localhost:3306/oauth2 spring.datasource.username=username spring.datasource.password=password spring.datasource.driver-class-name=com.mysql.jdbc.Driver ``` 同时,还需要配置相应的JDBC连接池和数据源,可以使用Spring Boot提供的自动配置功能,如下所示: ``` spring.datasource.tomcat.initial-size=5 spring.datasource.tomcat.max-idle=5 spring.datasource.tomcat.max-active=20 ``` 完成上述配置后,Spring Security OAuth2就可以使用SQL数据库来存储和管理用户信息和授权信息了。可以通过相关的API来进行用户认证和授权操作,例如获取访问令牌、刷新令牌等。 以上是关于Spring Security OAuth2在使用SQL数据库进行认证和授权的基本介绍,具体的配置和使用方法可以根据实际需求进行进一步调整和扩展。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值