淺談Shiro框架中的加密算法,以及校驗

在涉及到密碼存儲問題上,應該加密/生成密碼摘要存儲,而不是存儲明文密碼。爲何要加密:網絡安全問題是一個很大的隱患,用戶數據泄露事件層出不窮,好比12306帳號泄露。java

 

Shiro提供了base64和16進制字符串編碼/解碼的API支持,方便一些編碼解碼操做,想了解本身百度API操做用法。mysql

 

看一張圖,瞭解Shiro提供的加密算法:算法

 

 

 

本文重點講shiro提供的第二種:不可逆加密。spring

        散列算法通常用於生成數據的摘要信息,是一種不可逆的算法,通常適合存儲密碼之類的數據,常見的散列算法如MD五、SHA等。通常進行散列時最好提供一salt(鹽),好比加密密碼「admin」,產生的散列值是「21232f297a57a5a743894a0e4a801fc3」,能夠到一些md5解密網站很容易的經過散列值獲得密碼「admin」,即若是直接對密碼進行散列相對來講破解更容易,此時咱們能夠加一些只有系統知道的干擾數據,如用戶名和ID(即鹽);這樣散列的對象是「密碼+用戶名+ID」,這樣生成的散列值相對來講更難破解。sql

 

常見的算法有:MD5,SHA算法:數據庫

        MD5算法是1991年發佈的一項數字簽名加密算法,它當時解決了MD4算法的安全性缺陷,成爲應用很是普遍的一種算法。做爲Hash函數的一個應用實例。apache

        SHA誕生於1993年,全稱是安全散列算法(Secure Hash Algorithm),由美國國家安全局(NSA)設計,以後被美國標準與技術研究院(NIST)收錄到美國的聯邦信息處理標準(FIPS)中,成爲美國國家標準,SHA(後來被稱做SHA-0)於1995被SHA-1(RFC3174)替代。SHA-1生成長度爲160bit的摘要信息串,雖然以後又出現了SHA-22四、SHA-25六、SHA-384和SHA-512等被統稱爲「SHA-2」的系列算法,但仍以SHA-1爲主流。安全

 

數據庫User設計:網絡

  1. CREATE TABLE `sys_users` (
  2. `id` bigint(20) NOT NULL AUTO_INCREMENT,
  3. `username` varchar(100) DEFAULT NULL,
  4. `password` varchar(100) DEFAULT NULL,
  5. `salt` varchar(100) DEFAULT NULL,
  6. `locked` tinyint(1) DEFAULT '0',
  7. PRIMARY KEY (`id`),
  8. UNIQUE KEY `idx_sys_users_username` (`username`)
  9. ) ENGINE=InnoDB AUTO_INCREMENT=94 DEFAULT CHARSET=utf8;
  10.  
  11.  
  12. 說明:id主鍵字段
  13. username 登陸的用戶名
  14. passowrd 登陸的密碼
  15. salt 鹽
  16. locked 鎖定 默認爲0(false)表示沒有鎖


用戶表User:app

 

  1. package com.lgy.model;
  2.  
  3. import org.springframework.util.CollectionUtils;
  4. import org.springframework.util.StringUtils;
  5.  
  6. import java.io.Serializable;
  7. import java.util.ArrayList;
  8. import java.util.List;
  9.  
  10. public class User implements Serializable {
  11. private static final long serialVersionUID = -651040446077267878L;
  12.  
  13. private Long id; //編號
  14. private Long organizationId; //所屬公司
  15. private String username; //用戶名
  16. private String password; //密碼
  17. private String salt; //加密密碼的鹽
  18. private List<Long> roleIds; //擁有的角色列表
  19. private Boolean locked = Boolean.FALSE;
  20.  
  21. public User() {
  22. }
  23.  
  24. public User(String username, String password) {
  25. this.username = username;
  26. this.password = password;
  27. }
  28.  
  29. public Long getId() {
  30. return id;
  31. }
  32.  
  33. public void setId(Long id) {
  34. this.id = id;
  35. }
  36.  
  37. public Long getOrganizationId() {
  38. return organizationId;
  39. }
  40.  
  41. public void setOrganizationId(Long organizationId) {
  42. this.organizationId = organizationId;
  43. }
  44.  
  45. public String getUsername() {
  46. return username;
  47. }
  48.  
  49. public void setUsername(String username) {
  50. this.username = username;
  51. }
  52.  
  53. public String getPassword() {
  54. return password;
  55. }
  56.  
  57. public void setPassword(String password) {
  58. this.password = password;
  59. }
  60.  
  61. public String getSalt() {
  62. return salt;
  63. }
  64.  
  65. public void setSalt(String salt) {
  66. this.salt = salt;
  67. }
  68.  
  69. //證書憑證
  70. public String getCredentialsSalt() {
  71. return username + salt;
  72. }
  73.  
  74. public List<Long> getRoleIds() {
  75. if(roleIds == null) {
  76. roleIds = new ArrayList< Long>();
  77. }
  78. return roleIds;
  79. }
  80.  
  81. public void setRoleIds(List<Long> roleIds) {
  82. this.roleIds = roleIds;
  83. }
  84.  
  85.  
  86. public String getRoleIdsStr() {
  87. if(CollectionUtils.isEmpty(roleIds)) {
  88. return "";
  89. }
  90. StringBuilder s = new StringBuilder();
  91. for(Long roleId : roleIds) {
  92. s.append(roleId);
  93. s.append( ",");
  94. }
  95. return s.toString();
  96. }
  97.  
  98. public void setRoleIdsStr(String roleIdsStr) {
  99. if(StringUtils.isEmpty(roleIdsStr)) {
  100. return;
  101. }
  102. String[] roleIdStrs = roleIdsStr.split( ",");
  103. for(String roleIdStr : roleIdStrs) {
  104. if(StringUtils.isEmpty(roleIdStr)) {
  105. continue;
  106. }
  107. getRoleIds().add( Long.valueOf(roleIdStr));
  108. }
  109. }
  110.  
  111. public Boolean getLocked() {
  112. return locked;
  113. }
  114.  
  115. public void setLocked(Boolean locked) {
  116. this.locked = locked;
  117. }
  118.  
  119. @Override
  120. public boolean equals(Object o) {
  121. if (this == o) return true;
  122. if (o == null || getClass() != o.getClass()) return false;
  123.  
  124. User user = (User) o;
  125.  
  126. if (id != null ? !id.equals(user.id) : user.id != null) return false;
  127.  
  128. return true;
  129. }
  130.  
  131. @Override
  132. public int hashCode() {
  133. return id != null ? id.hashCode() : 0;
  134. }
  135.  
  136. @Override
  137. public String toString() {
  138. return "User{" +
  139. "id=" + id +
  140. ", organizationId=" + organizationId +
  141. ", username='" + username + '\'' +
  142. ", password='" + password + '\'' +
  143. ", salt='" + salt + '\'' +
  144. ", roleIds=" + roleIds +
  145. ", locked=" + locked +
  146. '}';
  147. }
  148. }


-------------------------------------------------------------------------------------------加密----------------------------------------------

正如前面散列算法的說法:加密採用的是MD5或者SHA算法和salt鹽結合產生不可逆的加密。

什麼是鹽?

      拋開鹽不說: 

     例如用戶名admin        密碼123,經過md5加密密碼獲得新的密碼值爲21232f297a57a5a743894a0e4a801fc3,這樣經過數字字典很容易就知道md5加密後的密碼爲123.

     若加入一些系統已經知道的干擾數據,這些干擾的數據就是鹽。則密碼就是由  sale(鹽) + 經過鹽生成的密碼組成,這樣同一個密碼加密生成的密碼是各不相同的達到不可逆加密。

 

 

對密碼進行鹽加密的工具:

這個是jdbc.properties配置文件,裏面有shiro加密中須要配的算法名稱和迭代次數。算法名稱能夠爲md5,sha-1,sha-256.

若填的算法名稱不是加密算法如aaa,則會報錯:Caused by: java.security.NoSuchAlgorithmException: abc MessageDigest not available

  1. #dataSource configure
  2. connection.url=jdbc:mysql: //localhost:3306/shiro-demo
  3. connection.username=root
  4. connection.password=
  5.  
  6. #druid datasource
  7. druid.initialSize= 10
  8. druid.minIdle= 10
  9. druid.maxActive= 50
  10. druid.maxWait= 60000
  11. druid.timeBetweenEvictionRunsMillis= 60000
  12. druid.minEvictableIdleTimeMillis= 300000
  13. druid.validationQuery=SELECT 'x'
  14. druid.testWhileIdle= true
  15. druid.testOnBorrow= false
  16. druid.testOnReturn= false
  17. druid.poolPreparedStatements= true
  18. druid.maxPoolPreparedStatementPerConnectionSize= 20
  19. druid.filters=wall,stat
  20.  
  21. #shiro
  22. password.algorithmName=sha -1
  23. password.hashIterations= 2


密碼加密工具類:

  1. package com.lgy.service;
  2.  
  3. import org.apache.shiro.crypto.RandomNumberGenerator;
  4. import org.apache.shiro.crypto.SecureRandomNumberGenerator;
  5. import org.apache.shiro.crypto.hash.SimpleHash;
  6. import org.apache.shiro.util.ByteSource;
  7. import org.springframework.beans.factory.annotation.Value;
  8. import org.springframework.stereotype.Service;
  9.  
  10. import com.lgy.model.User;
  11.  
  12. @Service
  13. public class PasswordHelper {
  14.  
  15. private RandomNumberGenerator randomNumberGenerator = new SecureRandomNumberGenerator();
  16.  
  17. @Value("${password.algorithmName}")
  18. private String algorithmName;
  19. @Value("${password.hashIterations}")
  20. private int hashIterations;
  21.  
  22. public void encryptPassword(User user) {
  23.  
  24. user.setSalt(randomNumberGenerator.nextBytes().toHex());
  25.  
  26. String newPassword = new SimpleHash(
  27. algorithmName, //加密算法
  28. user.getPassword(), //密碼
  29. ByteSource.Util.bytes(user.getCredentialsSalt()), //salt鹽 username + salt
  30. hashIterations //迭代次數
  31. ).toHex();
  32.  
  33. user.setPassword(newPassword);
  34. }
  35. }

 

密碼中干擾的值是username+salt組成, salt是用RandomNumberGererator隨機生成的值。能夠自定義,也能夠不須要salt這個字段。這樣在數據庫中生成的數據有:

一樣的密碼123456,獲得的密碼值是不同的!

用戶名                                    密碼                                                              鹽值

admin c4270458aca71740949bead254d6e9fb          228723e1ecce4511f2ff3a02a1a6a57b

feng 2053ad769d326bc6b36f97aac53b72a6a        cf12465e22601b8399439e526499f5c

 

---------------------------------------------------------------------------解密-----------------------------------------------------------------

 

shiro框架的解密是經過:HashedCredentialsMatcher實現密碼驗證服務

a.首先配置本身的realm:     

  1. <!-- Realm實現 -->
  2. <bean id="userRealm" class="com.lgy.realm.UserRealm">
  3. <!-- 密碼驗證方式 -->
  4. <property name="credentialsMatcher" ref="credentialsMatcher"/>
  5. <property name="cachingEnabled" value="false"/>
  6. <!--<property name="authenticationCachingEnabled" value="true"/>-->
  7. <!--<property name="authenticationCacheName" value="authenticationCache"/>-->
  8. <!--<property name="authorizationCachingEnabled" value="true"/>-->
  9. <!--<property name="authorizationCacheName" value="authorizationCache"/>-->
  10. </bean>
  1. <!-- 憑證匹配器 -->
  2. <bean id="credentialsMatcher" class="com.lgy.credentials.RetryLimitHashedCredentialsMatcher">
  3. <constructor-arg ref="cacheManager"/>
  4. <property name="hashAlgorithmName" value="sha-1"/>
  5. <property name="hashIterations" value="2"/>
  6. <property name="storedCredentialsHexEncoded" value="true"/>
  7. </bean>


密碼驗證方式是自定義實現的,RetryLimitHashedCredentialsMatcher實現類以下:

  1. package com.lgy.credentials;
  2.  
  3. import org.apache.shiro.authc.AuthenticationInfo;
  4. import org.apache.shiro.authc.AuthenticationToken;
  5. import org.apache.shiro.authc.ExcessiveAttemptsException;
  6. import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
  7. import org.apache.shiro.cache.Cache;
  8. import org.apache.shiro.cache.CacheManager;
  9.  
  10. import java.util.concurrent.atomic.AtomicInteger;
  11. public class RetryLimitHashedCredentialsMatcher extends HashedCredentialsMatcher {
  12.  
  13. private Cache<String, AtomicInteger> passwordRetryCache;
  14.  
  15. public RetryLimitHashedCredentialsMatcher(CacheManager cacheManager) {
  16. passwordRetryCache = cacheManager.getCache( "passwordRetryCache");
  17. }
  18.  
  19. @Override
  20. public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
  21. String username = (String)token.getPrincipal();
  22. //retry count + 1
  23. AtomicInteger retryCount = passwordRetryCache.get(username);
  24. if(retryCount == null) {
  25. retryCount = new AtomicInteger(0);
  26. passwordRetryCache.put(username, retryCount);
  27. }
  28. if(retryCount.incrementAndGet() > 5) {
  29. //if retry count > 5 throw
  30. throw new ExcessiveAttemptsException();
  31. }
  32.  
  33. boolean matches = super.doCredentialsMatch(token, info);
  34. if(matches) {
  35. //clear retry count
  36. passwordRetryCache.remove(username);
  37. }
  38. return matches;
  39. }
  40. }


這裏要注意認證憑證中的2個參數值的設置要與加密時的一致,分別是算法名稱)和迭代次數.

userRealm類以下:

  1. @Override
  2. protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
  3. String username = (String)token.getPrincipal();
  4. User user = userService.findByUsername(username);
  5. if(user == null) {
  6. throw new UnknownAccountException();//沒找到賬號
  7. }
  8. if(Boolean.TRUE.equals(user.getLocked())) {
  9. throw new LockedAccountException(); //賬號鎖定
  10. }
  11. //交給AuthenticatingRealm使用CredentialsMatcher進行密碼匹配,若是以爲人家的很差能夠自定義實現
  12. SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
  13. user.getUsername(), //用戶名
  14. user.getPassword(), //密碼
  15. ByteSource.Util.bytes(user.getCredentialsSalt()), //salt=username+salt
  16. getName() //realm name
  17. );
  18. return authenticationInfo;
  19. }

經過SimpleAuthenticationInfo將鹽值以及用戶名和密碼信息封裝到AuthenticationInfo中,進入證書憑證類中進行校驗。

相關文章
相關標籤/搜索