【组件】Shiro设置PHP的password_hash验证

今天处理了个登录问题,有个PHP生成的用户数据,要用JAVA项目去登录,JAVA用的是shiro做的权限。

看了下PHP是用password_hash()做的密码hash和验证,这个java里没有找到合适的组件集成。

BCrypt类库

这里找了个替代组件BCrypt类库

1
2
3
4
5
<dependency>
<groupId>at.favre.lib</groupId>
<artifactId>bcrypt</artifactId>
<version>0.8.0</version>
</dependency>

hash方式:

1
2
3
4
5
String pwd="ceshi123";
String hash = BCrypt.with(BCrypt.Version.VERSION_2Y).hashToString(5, pwd.toCharArray());
System.out.println(hash);
String h102y = BCrypt.with(BCrypt.Version.VERSION_2Y).hashToString(10, pwd.toCharArray());
System.out.println(h102y);

这里注意一下,每次的hash结果不一样。

因为结果里包含了version和cost等,验证时要用BCrypt指定的验证方法,不能直接比hash结果。

验证方式:

1
2
3
4
String hash="$2y$10$ozLf.I8c6WnqJ.3hSPhn7OGYALCRi9pWv0cFgQeLPlbk08OZn.DfO";
String pwd="ceshi123";
BCrypt.Result res = BCrypt.verifyer().verify(pwd.toCharArray(), hash);
System.out.println(res.verified);

shiro登录验证

首先要把用户信息查出来:

1
2
3
4
5
6
7
8
9
10
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String userLoginName= (String) token.getPrincipal();
UserEntity user = userDao.queryShopUserByLoginName(userLoginName);
if(user == null){
log.info("商家登录失败,username:"+userLoginName);
throw new AuthenticationException("帐号密码错误");
}
SimpleAuthenticationInfo sainfo=new SimpleAuthenticationInfo(user,user.getPassWord(), getName());
return sainfo;
}

验证密码:

1
2
3
4
5
6
7
8
9
10
11
protected void assertCredentialsMatch(AuthenticationToken authcToken,
AuthenticationInfo info) throws AuthenticationException {
UserEntity user=(UserEntity) info.getPrincipals().getPrimaryPrincipal();
UsernamePasswordToken token=(UsernamePasswordToken) authcToken;
BCrypt.Result res = BCrypt.verifyer().verify(token.getPassword(), user.getPassWord());
if(res.verified){
return;
}
String msg = "Submitted credentials for token [" + token + "] did not match the expected credentials.";
throw new IncorrectCredentialsException(msg);
}

这里验证通过直接返回,没有验证通过要抛出异常。

password_hash

1
string password_hash ( string $password , integer $algo [, array $options ])

它有三个参数:密码、哈希算法、选项。前两项为必须的。 让我们使用password_hash()简单的创建一个哈希密码: 复制代码 代码如下:

1
2
3
$pwd = "123456";
$hash = password_hash($pwd, PASSWORD_DEFAULT);
echo $hash;

上例输出结果类似:

1
$2y$10$4kAu4FNGuolmRmSSHgKEMe3DbG5pm3diikFkiAKNh.Sf1tPbB4uo2

并且刷新页面该哈希值也会不断的变化。

这里便有了一个疑问,同样的值同意的算法不同的值如何验证如何实现。 上述的方法支持三种算法

PASSWORD_DEFAULT - 使用 bcrypt 算法 (PHP 5.5.0 默认)。 注意,该常量会随着 PHP 加入更新更高强度的算法而改变。所以,使用此常量生成结果的长度将在未来有变化。 因此,数据库里储存结果的列可超过60个字符(最好是255个字符)。

PASSWORD_BCRYPT - 使用 CRYPT_BLOWFISH 算法创建散列。 这会产生兼容使用 “$2y$” 的 crypt()。 结果将会是 60 个字符的字符串, 或者在失败时返回 FALSE。

PASSWORD_ARGON2I - 使用 Argon2 散列算法创建散列。

PASSWORD_BCRYPT 支持的选项:

salt(string) - 手动提供散列密码的盐值(salt)。这将避免自动生成盐值(salt)。 省略此值后,password_hash() 为每个密码散列自动生成随机的盐值。

盐值(salt)选项从 PHP 7.0.0 开始被废弃(deprecated)了。 现在最好选择简单的使用默认产生的盐值。

cost (integer) - 代表算法使用的 cost。crypt() 页面上有 cost 值的例子。 省略时,默认值是 10。这个 cost个不错的底线,但也许可以根据自己硬件的情况,加大这个值。

我要处理的密码是 因为 password_hash 使用的是 crypt 算法, 因此参与计算 hash值的:

算法(就像身份证开头能知道省份一样, 由盐值的格式决定), cost(默认10) 和 盐值 是在$hash中可以直接看出来的!

所以说, Laravel 中bcrypt的盐值是PHP自动随机生成的字符, 虽然同一个密码每次计算的hash不一样.

但是通过 $hash 和 密码, 却可以验证密码的正确性!

具体来说, 比如这个

1
2
3
4
$hash = password_hash('password',PASSWORD_BCRYPT,['cost' => 10]);
echo $hash;
// 比如我这次算的是
// $hash = '$2y$10$DyAJOutGjURG9xyKgAaCtOm4K1yezvgNkxHf6PhuLYBCENk61bePm';

那么我们从这个 crypt的hash值中可以看到, 因为以$2y$开头, 所以它的算法是 CRYPT_BLOWFISH .

同时 CRYPT_BLOWFISH 算法盐值格式规定是 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
以$2y$开头 + 一个两位cost参数 + $ + 22位随机字符("./0-9A-Za-z")

$hash(CRYPT_BLOWFISH是固定60位) = 盐值 + 31位单向加密后的值
参见: https://secure.php.net/manual/en/function.crypt.php

验证密码

if (password_verify('password', $hash)) {
echo '密码正确.';
} else {
echo '密码错误!';
}

// 原理是:

if ($hash === crypt('password', '$2y$10$DyAJOutGjURG9xyKgAaCtO')) {
echo '密码正确.';
} else {
echo '密码错误!';
}