鉴权模块-密码的散列存储

在涉及到密码存储问题上,应该加密 / 生成密码摘要存储,而不是存储明文密码。比如之前的 600w csdn 账号泄露对用户可能造成很大损失,因此应加密 / 生成不可逆的摘要方式存储。

散列算法

散列算法一般用于生成数据的摘要信息,是一种不可逆的算法,一般适合存储密码之类的数据,常见的散列算法如 MD5、SHA 等。一般进行散列时最好提供一个 salt(盐)

比如加密密码 “admin”,产生的散列值是 “21232f297a57a5a743894a0e4a801fc3”,可以到一些 md5 解密网站很容易的通过散列值得到密码 “admin”,即如果直接对密码进行散列相对来说破解更容易,此时我们可以加一些只有系统知道的干扰数据,如用户名和 ID(即盐);这样散列的对象是 “密码 + 用户名 +ID”,这样生成的散列值相对来说更难破解。

Shiro 还提供了通用的散列支持:

1
2
3
4
String str = "hello";
String salt = "123";
//内部使用MessageDigest
String simpleHash = new SimpleHash("SHA-1", str, salt).toString();

通过调用 SimpleHash 时指定散列算法,其内部使用了 Java 的 MessageDigest 实现。

为了方便使用,Shiro 提供了 HashService,默认提供了 DefaultHashService 实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
DefaultHashService hashService = new DefaultHashService(); //默认算法SHA-512
hashService.setHashAlgorithmName("SHA-512");
hashService.setPrivateSalt(new SimpleByteSource("123")); //私盐,默认无
hashService.setGeneratePublicSalt(true);//是否生成公盐,默认false
hashService.setRandomNumberGenerator(new SecureRandomNumberGenerator());//用于生成公盐。默认就这个
hashService.setHashIterations(1); //生成Hash值的迭代次数
HashRequest request = new HashRequest.Builder()
.setAlgorithmName("MD5")
.setSource(ByteSource.Util.bytes("hello"))
.setSalt(ByteSource.Util.bytes("123"))
.setIterations(2)
.build();
String hex = hashService.computeHash(request).toHex();

SecureRandomNumberGenerator 用于生成一个随机数:

1
2
3
SecureRandomNumberGenerator randomNumberGenerator = new SecureRandomNumberGenerator();
randomNumberGenerator.setSeed("123".getBytes());
String hex = randomNumberGenerator.nextBytes().toHex();

加密 / 解密

Shiro 还提供对称式加密 / 解密算法的支持,如 AES、Blowfish 等;当前还没有提供对非对称加密 / 解密算法支持,未来版本可能提供。

AES 算法实现:

1
2
3
4
5
6
7
8
9
10
11
AesCipherService aesCipherService = new AesCipherService();
aesCipherService.setKeySize(128); //设置key长度
//生成key
Key key = aesCipherService.generateNewKey();
String text = "hello";
//加密
String encrptText = aesCipherService.encrypt(text.getBytes(), key.getEncoded()).toHex();
//解密
String text2 =
new String(aesCipherService.decrypt(Hex.decode(encrptText),key.getEncoded()).getBytes());
Assert.assertEquals(text, text2);

PasswordService/CredentialsMatcher简单介绍

Shiro 提供了 PasswordService 及 CredentialsMatcher 用于提供加密密码及验证密码服务。

1
2
3
4
5
6
7
8
9
public interface PasswordService {
//输入明文密码得到密文密码
String encryptPassword(Object plaintextPassword) throws IllegalArgumentException;
}

public interface CredentialsMatcher {
//匹配用户输入的token的凭证(未加密)与系统提供的凭证(已加密)
boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info);
}

Shiro 默认提供了 PasswordService的实现 DefaultPasswordServiceCredentialsMatcher 的实现 PasswordMatcherHashedCredentialsMatcher(更强大)。

DefaultPasswordService 配合 PasswordMatcher 实现简单的密码加密与验证服务

1、定义 Realm(com.github.zhangkaitao.shiro.chapter5.hash.realm.MyRealm)

1
2
3
4
5
6
7
8
9
10
11
12
13
public class MyRealm extends AuthorizingRealm {
private PasswordService passwordService;
public void setPasswordService(PasswordService passwordService) {
this.passwordService = passwordService;
}
//省略doGetAuthorizationInfo,具体看代码
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token)
throws AuthenticationException {
return
new SimpleAuthenticationInfo("wu", passwordService.encryptPassword("123"),getName());
}
}

这里为了演示方便,直接注入一个 passwordService 来加密密码,实际使用时需要在 Service 层使用 passwordService 加密密码并存到数据库。

HashedCredentialsMatcher 实现密码验证服务

Shiro 提供了 CredentialsMatcher 的散列实现 HashedCredentialsMatcher,和PasswordMatcher

不同的是,它只用于密码验证,且可以提供自己的盐,而不是随机生成盐,且生成密码散列值的算法需要自己

写,因为能提供自己的盐。

HashedCredentialsMatcher需要进行全局配置,加密算法、迭代次数、私盐等

项目中的实现

PasswordHelper 类用于在UserService中对密码生成数字签名,代替PasswordService

PasswordHelper 指定了hash算法,迭代次数,私盐

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Component
public class PasswordHelper {
// 公盐生成器
private final RandomNumberGenerator randomNumberGenerator = new SecureRandomNumberGenerator();
public final static String PRIVATE_SALT = ByteSource.Util.bytes("jungomama").toHex(); //私盐
private static final String algorithmName = "md5";
private static final int hashIterations = 2;

/**
* 加密密码 在添加用户、充值密码的时候调用
* @param user
*/
public void encryptPassword(User user) {
user.setPublicSalt(randomNumberGenerator.nextBytes().toHex()); // 设置公盐
String newPassword = new SimpleHash(
algorithmName,
user.getPassword(),
ByteSource.Util.bytes(user.getPublicSalt() + PRIVATE_SALT),
hashIterations).toHex();
user.setPassword(newPassword);
}

自定义类RetryLimitHashedCredentialsMatcher继承了HashedCredentialsMatcher,扩展除了重试次数限制的功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class RetryLimitHashedCredentialsMatcher extends HashedCredentialsMatcher {
private RedisTemplate<String,Object> redisService;
@Autowired
public void setRedisService(RedisTemplate<String, Object> redisService) {
this.redisService = redisService;
}

/**
* 缓存无需删除,设置1小时失效。
* @param token
* @param info
* @return
*/
@Override
public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
if (super.doCredentialsMatch(token, info)) {
return true;
} else {
String username = (String) token.getPrincipal();
AtomicInteger retryCount = (AtomicInteger) redisService.opsForValue().get(username);
if (retryCount == null) {
redisService.opsForValue().set(username, new AtomicInteger(0));
}
assert retryCount != null;
if (retryCount.incrementAndGet() > 5) {
throw new ExcessiveAttemptsException();
} else {
redisService.opsForValue().set(username,retryCount);
}
return false;
}
}
}

在ShiroConfig中要进行bean的配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Bean("retryLimitHashedCredentialsMatcher")
public RetryLimitHashedCredentialsMatcher retryLimitHashedCredentialsMatcher(){
RetryLimitHashedCredentialsMatcher matcher = new RetryLimitHashedCredentialsMatcher();
matcher.setHashAlgorithmName("MD5");
matcher.setHashIterations(2);
return matcher;
}
/**
* 将自定义的校验规格放入Realm
* @param matcher
* @return
*/
@Bean("sqlDatabaseRealm")
public SqlDatabaseRealm sqlDatabaseRealm(
@Qualifier("retryLimitHashedCredentialsMatcher") RetryLimitHashedCredentialsMatcher matcher){
SqlDatabaseRealm sqlDatabaseRealm = new SqlDatabaseRealm();
//信息放入缓存
sqlDatabaseRealm.setCacheManager(new MemoryConstrainedCacheManager());
sqlDatabaseRealm.setCredentialsMatcher(matcher);
return sqlDatabaseRealm;
}

以上就实现了密码签名存储、hash密码验证、重试次数限制的功能