目錄
一、什麼是憑證比較器
在第一章,shiro整體架構圖上可以看到,shiro爲我們提供了密碼學工具,和哈希憑據匹配器 HashedCredentialsMatcher 類,下圖可以看到此類繼承了編解碼支持器 CodecSupport 類和實現了 CredentialsMatcher 接口,實現了接口中 boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) 方法,這個方法就可以替我們實現密碼比較,他會在調用自定義realm的身份認證方法後來執行!
二、爲什麼要加密加鹽
- 之前的例子中,我們在自定義realm的 doGetAuthenticationInfo 方法中採用了自己加密和對比密碼,不同則拋出 IncorrectCredentialsException 異常的方式對比密碼,但是在繼續深入學習shiro的過程中,發現了shiro已經爲我們提供了自動密碼比對工具,我們只要安裝上去即可使用。之後會講到他的執行原理和使用方法。
- 那麼爲什麼要對數據加密呢?這篇文章可以告訴你答案2018年國內外信息泄露案例彙編,現在數據庫加密基本上是使用MD5加密方式,MD5加密過的密文只能被暴力破解,我們不僅僅是要防範外部黑客的攻擊,還需要放置內部維護人員猜出密碼,加密過後,即使數據庫被泄露出去,但是暴露出去的人員的密碼均是加密過的,這樣即使是暴力破解,也給予了時間挽救。
- 爲什麼要加鹽呢,鹽就是讓你的密碼更加的安全,更加的難以破解,在你的密碼中加入特定的字段,然後在進行加密,這樣即使密碼相同的情況下,只要鹽不相同,數據庫儲存的字段也不會相同,大大減少了很多用戶使用同一密碼,數據庫泄漏一下破解很多用戶賬號的可能!
三、如何生成密文
shiro已經爲我們提供了密碼學工具,SimpleHash 類,我們只需要調用這個類的方法即可快速完成密碼加密,查看關係圖,猜測此類還提供sha加密方式(後認證過),
String hashAlgorithmName = "MD5";//加密方式
Object crdentials = "admin";//密碼原值
ByteSource salt = ByteSource.Util.bytes("admin");//以賬號作爲鹽值
int hashIterations = 1024;//加密次數
Object result = new SimpleHash(hashAlgorithmName,crdentials,salt,hashIterations);
System.out.println("admin:"+result);
//輸出
>> admin:df655ad8d3229f3269fad2a8bab59b6c
用戶註冊時,可以使用此方法生成密文
四、瞭解shiro自帶憑證比較器
我們跟蹤登錄流程發現用戶調用login接口後,shiro框架會執行下列流程:
下面爲 HashedCredentialsMatcher 哈希憑據匹配器 的繼承關係
4.1提供的的比較憑證的方法部分代碼
//固有屬性
private String hashAlgorithm; // 加密算法的名稱
private int hashIterations; // 配置加密的次數
private boolean hashSalted;//是否加鹽
private boolean storedCredentialsHexEncoded;// 是否存儲爲16進制
//構造
public HashedCredentialsMatcher() {
this.hashAlgorithm = null;
this.hashSalted = false;
this.hashIterations = 1;
this.storedCredentialsHexEncoded = true; //false means Base64-encoded
}
//設置憑證加密次數
public void setHashIterations(int hashIterations) {
if (hashIterations < 1) {//在此限制了最低加密次數爲1次
this.hashIterations = 1;
} else {
this.hashIterations = hashIterations;
}
}
...............
//實現對比憑證方法,token中包含用戶輸入信息,info則是realm中用戶認證方法中返回的dbuser信息
public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
Object tokenCredentials = getCredentials(token);
Object accountCredentials = getCredentials(info);
return equals(tokenCredentials, accountCredentials);
}
五、使用shiro自帶憑證比較器
- 需要在shiroconfig配置類中新增憑證比較器
@Bean
public SimpleCredentialsMatcher CredentialsMatcher() {
HashedCredentialsMatcher hct = new HashedCredentialsMatcher();
// 加密算法的名稱
hct.setHashAlgorithmName("MD5");
// 配置加密的次數
hct.setHashIterations(1024);
// 是否存儲爲16進制
hct.setStoredCredentialsHexEncoded(true);
return hct;
}
- 把憑證比較器放入自定義realm中,如果沒有此步驟shiro不會進行憑證驗證
/**
* 自定義身份認證 realm;
* <p>
* 必須寫這個類,並加上 @Bean 註解,目的是注入 MyShiroRealm, 否則會影響 MyShiroRealm類 中其他類的依賴注入
*/
@Bean
public MyShiroRealm myShiroRealm() {
MyShiroRealm myShiroRealm = new MyShiroRealm();
// 設置憑證比較器
myShiroRealm.setCredentialsMatcher(CredentialsMatcher());
return myShiroRealm;
}
- 修改用戶信息,模擬用戶註冊生成的密碼,更新至業務層
我們採用guest用戶來測試,使用用戶名guest作爲鹽,把生成的密碼放入service中,
String hashAlgorithmName = "MD5";//加密方式
Object crdentials = "guest";//密碼原值
ByteSource salt = ByteSource.Util.bytes("guest");//以賬號作爲鹽值
int hashIterations = 1024;//加密次數
Object result = new SimpleHash(hashAlgorithmName,crdentials,salt,hashIterations);
System.out.println("admin:"+result);
>>admin:565dd969076eef0ac3f9d49aa61e9489
- 更新UserServiceImpl
@Override
public User findByUsername(String username) {
User user = new User();
user.setUsername(username);
Set<String> roleList = new HashSet<>();
Set<String> permissionsList = new HashSet<>();
switch (username) {
case "admin":
roleList.add("admin");
user.setPassword("admin");
permissionsList.add("user:add");
permissionsList.add("user:delete");
break;
case "consumer":
roleList.add("consumer");
user.setPassword("consumer");
permissionsList.add("consumer:modify");
break;
default:
roleList.add("guest");
user.setPassword("565dd969076eef0ac3f9d49aa61e9489");
break;
}
user.setRole(roleList);
user.setPermission(permissionsList);
return user;
}
- 把自定義realm中的判斷密碼部分代碼刪除,此時,我們可以把這個方法理解爲提供給主框架真實用戶信息,作爲憑證對比對象
/**
* 身份驗證
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
System.out.println("進入自定義登錄驗證方法!");
if (userService == null) {
userService = (UserService) SpringBeanFactoryUtil.getBeanByName("userServiceImpl");
}
UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) token;
String username = usernamePasswordToken.getUsername();// 用戶輸入用戶名
User user = userService.findByUsername(username);// 根據用戶輸入用戶名查詢該用戶
if (user == null) {
throw new UnknownAccountException();// 用戶不存在
}
String password = user.getPassword();// 數據庫獲取的密碼
// 主要的(用戶名,也可以是用戶對象(最好不放對象)),資格證書(數據庫獲取的密碼),區域名稱(當前realm名稱)
SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(username, password, getName());
//加鹽,對比的時候會使用該參數對用戶輸入的密碼按照密碼比較器指定規則加鹽,加密,再去對比數據庫密文
simpleAuthenticationInfo.setCredentialsSalt(ByteSource.Util.bytes(username));
return simpleAuthenticationInfo;
}
5.1登錄測試,可以登錄
- 此時我們斷點到shiro的憑證對比器中查看,可以看到shiro內部已經使用 hashProvidedCredentials 方法對輸入的密碼進行了加密
- 部分源碼
//HashedCredentialsMatcher類 密碼加密
protected Object hashProvidedCredentials(AuthenticationToken token, AuthenticationInfo info) {
Object salt = null;
if (info instanceof SaltedAuthenticationInfo) {
salt = ((SaltedAuthenticationInfo) info).getCredentialsSalt();
} else {
//retain 1.0 backwards compatibility:
if (isHashSalted()) {
salt = getSalt(token);
}
}
return hashProvidedCredentials(token.getCredentials(), salt, getHashIterations());
}
@Override //HashedCredentialsMatcher類
public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
Object tokenHashedCredentials = hashProvidedCredentials(token, info);
Object accountCredentials = getCredentials(info);
return equals(tokenHashedCredentials, accountCredentials);
}
//密碼對比方法SimpleCredentialsMatcher類
protected boolean equals(Object tokenCredentials, Object accountCredentials) {
.......
if (isByteSource(tokenCredentials) && isByteSource(accountCredentials)) {
....
byte[] tokenBytes = toBytes(tokenCredentials);
byte[] accountBytes = toBytes(accountCredentials);
return MessageDigest.isEqual(tokenBytes, accountBytes);
} else {
return accountCredentials.equals(tokenCredentials);
}
}
六、如何使用自定義密碼比較器
- 在理解了shiro自帶的憑證比較器後,自定義一個自己的憑證比較器思路應該也十分清晰了,我們只需要創建 MyCredentialsMatcher 類繼承 SimpleCredentialsMatcher 類,重寫其中的 doCredentialsMatch 方法即可
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authc.credential.SimpleCredentialsMatcher;
import org.apache.shiro.crypto.hash.SimpleHash;
public class MyCredentialsMatcher extends SimpleCredentialsMatcher {
/**
* 重寫密碼驗證器
*
* @param token 用戶輸入的信息
* @param info 數據庫查詢到的信息
*/
@Override
public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
System.out.println("進入自定義密碼比較器");
UsernamePasswordToken upt = (UsernamePasswordToken) token;
String inputName = upt.getUsername();// 用戶輸入的用戶名
String inputPwd = new String(upt.getPassword());// 用戶輸入的密碼
String dbPassword = (String) info.getCredentials();// 數據庫查詢得到的加密後的密碼
// 對用戶輸入密碼進行加密(加密方式,用戶輸入密碼,鹽值(用戶名),加密次數)
String encryptionPwd = new SimpleHash("MD5", inputPwd, inputName, 1024).toString();// 加密後的密碼
return equals(encryptionPwd, dbPassword);
}
}
- 第二步,我們需要把注入realm的憑證比較器替換爲我們自己的憑證比較器
@Bean
public SimpleCredentialsMatcher CredentialsMatcher() {
MyCredentialsMatcher hct = new MyCredentialsMatcher();//自定義憑證比較器
//HashedCredentialsMatcher hct = new HashedCredentialsMatcher();//系統提供憑證比較器
// 加密算法的名稱
hct.setHashAlgorithmName("MD5");
// 配置加密的次數
hct.setHashIterations(1024);
// 是否存儲爲16進制
hct.setStoredCredentialsHexEncoded(true);
return hct;
}
6.1 測試
控制檯打印出了執行了自定義密碼