关键词:Hash加密算法、Security中的PasswordManager
Hash与加密
密码安全
用户的密码,怎么才能够保证安全?我认为最完美的方法就是确保该密码只有用户自己知道。
在系统中,用户信息一般都是存储在数据库中,其中就包括账号、密码等信息。对于密码的存储方式,一般有两种
-
明文存储
即用户输入的是什么密码,就存储什么密码。 -
Hash存储
Hash存储的意思是:对用户输入的密码按照Hash算法得到Hash值,然后将Hash值存到数据库中。
为什么说是 "Hash存储",而不是 "加密存储"?因为这本来就不算是加密解密的过程,而且很容易对人造成误解。Hash算法是将目标文本转换成具有相同长度的、不可逆的杂凑字符串(或叫做消息摘要),而加密(Encrypt)是将目标文本转换成具有不同长度的、可逆的密文。
有些系统安全意识不够,直接存储明文,这是绝对不可取的,就算不考虑因为系统异常等因素导致的密码泄露,拥有数据库最高权限的人,就一定能看到所有用户的密码,这显然是不可取的。所以,主要考虑的是Hash存储。
转换算法目前主流的就是哈希算法,也叫译摘要算法,是一种散列算法。哈希算法是不可逆的,这里的不可逆有两层含义,一是“给定一个哈希结果R,没有方法将R转换成原目标文本S”,二是“给定哈希结果R,即使知道一段文本S的哈希结果为R,也不能断言当初的目标文本就是S”(这里涉及到Hash碰撞)。
为什么需要单向的算法?回到之前的那句话:“最完美的方法就是确保该密码只有用户自己知道”。单向的,就意味着对于每一个固定的明文,经过Hash算法转换后可以得到固定的Hash值,但是根据Hash值,却无法得到明文。数据库最高管理员可以看到不同用户密码对应的Hash值,但因为Hash算法是不可逆的,所以,他也无法知道用户的文明,没有明文,就无法登录系统进行危险操作。
但是仔细想想,上面的方案还是会有一些问题。我们知道,根据固定的明文,按照一定的Hash算法可以得到固定的Hash值。用户设置密码的时候,又基本上不会设置泰国负载的密码,那就有可能通过穷举法来实现破解,将常见密码的的Hash值全部算出来,然后拿用户的Hash值一一匹对,虽然,根据不同的明文生成的 hash 值可能相同(Hash碰撞),但这只会让破解更简单。
盐值
为了解决上面的问题,hash 方案迎来的第一个改造是对引入一个“随机的因子”来掺杂进明文中进行 hash 计算,这样的随机因子通常被称之为盐 (salt)。salt 一般是用户相关的,每个用户持有各自的 salt。此时两个用户的密码即使相同,由于 salt 的影响,存储在数据库中的密码也是不同的。
但现在的计算机能力越来越强,虽然破解 salted hash 比较麻烦,却并非不可行。一些新型的单向 hash 算法被研究了出来。其中就包括:Bcrypt,PBKDF2,Scrypt,Argon2。
Hash算法
MD5和SHA。SHA又包括 SHA-1 和 SHA-2(SHA-224、SHA-256、SHA-384、SHA-512) 和SHA-3。
MD5是输入不定长度信息,输出固定长度128-bits的算法。经过程序流程,生成四个32位数据,最后联合起来成为一个128-bits散列。基本方式为,求余、取余、调整长度、与链接变量进行循环运算。得出结果。
SHA-1在许多安全协议中广为使用,包括TLS和SSL、PGP、SSH、S/MIME和IPsec,曾被视为是MD5(更早之前被广为使用的散列函数)的后继者。
SHA-2它的算法跟SHA-1基本上仍然相似;因此有些人开始发展其他替代的散列算法。
由于对MD5出现成功的破解,以及对SHA-0和SHA-1出现理论上破解的方法,NIST感觉需要一个与之前算法不同的,可替换的加密杂凑算法,也就是现在的SHA-3。
PasswordManager
在spring security新版本中,获取明文的Hash值通过org.springframework.security.crypto.password.PasswordEncoder接口,该接口有三个实现:
NoOpPasswordEncoder不多说了,啥也不做按原文本处理,相当于不加密。
StandardPasswordEncoder 1024次迭代的SHA-256散列哈希加密实现,并使用一个随机8字节的salt。
BCryptPasswordEncoder 使用BCrypt的强散列哈希加密实现,并可以由客户端指定加密的强度strength,强度越高安全性自然就越高,默认为10.
Hap中使用的是StandardPasswordEncoder,但官方其实推荐使用BCryptPasswordEncoder。
* If you are developing a new system,
* {@link org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder} is a better
* choice both in terms of security and interoperability with other languages.
Hap中的自定义PasswordManager如下,其实就是调用StandardPasswordEncoder:
<bean id="passwordManager" class="com.hand.hap.security.PasswordManager">
<property name="siteWideSecret" value="Zxa1pO6S6uvBMlY"/>
</bean>
package com.hand.hap.security;
import java.util.Arrays;
import java.util.List;
import com.hand.hap.mybatis.util.StringUtil;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.crypto.password.StandardPasswordEncoder;
import com.hand.hap.message.profile.SystemConfigListener;
/**
* @author njq.niu@hand-china.com
* @author xiangyu.qi@hand-china.com
* @date 2016/1/31
* @date 2016/10/10
*/
public class PasswordManager implements PasswordEncoder, InitializingBean, SystemConfigListener {
public static final String PASSWORD_COMPLEXITY_NO_LIMIT = "NO_LIMIT";
public static final String PASSWORD_COMPLEXITY_DIGITS_AND_LETTERS = "DIGITS_AND_LETTERS";
public static final String PASSWORD_COMPLEXITY_DIGITS_AND_CASE_LETTERS = "DIGITS_AND_CASE_LETTERS";
private PasswordEncoder delegate;
private String siteWideSecret = "my-secret-key";
private String defaultPassword = "123456";
/**
* 密码失效时间 默认0 不失效
*/
private Integer passwordInvalidTime = 0;
/**
* 密码长度
*/
private Integer passwordMinLength = 8;
/**
* 密码复杂度
*/
private String passwordComplexity = "no_limit";
public Integer getPasswordInvalidTime() {
return passwordInvalidTime;
}
public Integer getPasswordMinLength() {
return passwordMinLength;
}
public String getPasswordComplexity() {
return passwordComplexity;
}
public String getDefaultPassword() {
return defaultPassword;
}
public String getSiteWideSecret() {
return siteWideSecret;
}
public void setSiteWideSecret(String siteWideSecret) {
this.siteWideSecret = siteWideSecret;
}
@Override
public void afterPropertiesSet() throws Exception {
delegate = new StandardPasswordEncoder(siteWideSecret);
}
@Override
public String encode(CharSequence rawPassword) {
return delegate.encode(rawPassword);
}
@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
if (StringUtil.isEmpty(encodedPassword)) {
return false;
}
return delegate.matches(rawPassword, encodedPassword);
}
@Override
public List<String> getAcceptedProfiles() {
return Arrays.asList("DEFAULT_PASSWORD", "PASSWORD_INVALID_TIME", "PASSWORD_MIN_LENGTH", "PASSWORD_COMPLEXITY");
}
@Override
public void updateProfile(String profileName, String profileValue) {
if ("PASSWORD_INVALID_TIME".equalsIgnoreCase(profileName)) {
this.passwordInvalidTime = Integer.parseInt(profileValue);
} else if ("PASSWORD_MIN_LENGTH".equalsIgnoreCase(profileName)) {
this.passwordMinLength = Integer.parseInt(profileValue);
} else if ("PASSWORD_COMPLEXITY".equalsIgnoreCase(profileName)) {
this.passwordComplexity = profileValue;
} else if ("DEFAULT_PASSWORD".equalsIgnoreCase(profileName)) {
this.defaultPassword = profileValue;
}
}
}
主要有两个方法
/**
* Encode the raw password. Generally, a good encoding algorithm applies a SHA-1 or
* greater hash combined with an 8-byte or greater randomly generated salt.
*/
String encode(CharSequence rawPassword);
/**
* Verify the encoded password obtained from storage matches the submitted raw
* password after it too is encoded. Returns true if the passwords match, false if
* they do not. The stored password itself is never decoded.
*
* @param rawPassword the raw password to encode and match
* @param encodedPassword the encoded password from storage to compare with
* @return true if the raw password, after encoding, matches the encoded password from
* storage
*/
boolean matches(CharSequence rawPassword, String encodedPassword);
encode 方法是將明文生成Hash值得,参数就是明文。matches使用来匹对密码是否正确的,第一个参数是明文,第二个参数是之前生成得Hash值。
encode过程
StandardPasswordEncoder两个共有构造函数
secret是秘钥,可以没有。在Hap中,配置文件中给了一个秘钥:Zxa1pO6S6uvBMlY
<bean id="passwordManager" class="com.hand.hap.security.PasswordManager">
<property name="siteWideSecret" value="Zxa1pO6S6uvBMlY"/>
</bean>
调用encode方法的时候,基于SHA-256算法 明文+秘钥+8位随机盐值 生成 hash值,生成的hash是80个十六进制的字符串,其中就包括了盐值和秘钥。
matches过程
encode的过程中,会用随机生成一个盐值,让然后用盐值+明文+秘钥的组合字符串生成hash值。那有没有想过,当用户登录的时候,怎么验证密码的正确性呢?可以确定的是,验证密码的时候不会去校验明文,因为数据库里存的是hash值,所以系统只能校验hash值。只要用户输入的明文经过hash转换后得到的hash值和数据库里的hash值一样,就说明密码正确。在上面的encode中已经提到了,盐值也在生成的hash值中。
public boolean matches(CharSequence rawPassword, String encodedPassword) {
byte[] digested = decode(encodedPassword);
byte[] salt = subArray(digested, 0, saltGenerator.getKeyLength());
return matches(digested, digest(rawPassword, salt));
}
subArray(digested, 0, saltGenerator.getKeyLength()); 就是根据 hash值找到盐值。