浅谈Shiro框架中的加密算法,以及校验

在涉及到密码存储问题上,应该加密/生成密码摘要存储,而不是存储明文密码。为什么要加密:网络安全问题是一个很大的隐患,用户数据泄露事件层出不穷,比如12306账号泄露。

 

Shiro提供了base64和16进制字符串编码/解码的API支持,方便一些编码解码操作,想了解自己百度API操作用法。

 

看一张图,了解Shiro提供的加密算法:

 

 

 

本文重点讲shiro提供的第二种:不可逆加密。

        散列算法一般用于生成数据的摘要信息,是一种不可逆的算法,一般适合存储密码之类的数据,常见的散列算法如MD5、SHA等。一般进行散列时最好提供一salt(盐),比如加密密码“admin”,产生的散列值是“21232f297a57a5a743894a0e4a801fc3”,可以到一些md5解密网站很容易的通过散列值得到密码“admin”,即如果直接对密码进行散列相对来说破解更容易,此时我们可以加一些只有系统知道的干扰数据,如用户名和ID(即盐);这样散列的对象是“密码+用户名+ID”,这样生成的散列值相对来说更难破解。

 

常见的算法有:MD5,SHA算法:

        MD5算法是1991年发布的一项数字签名加密算法,它当时解决了MD4算法的安全性缺陷,成为应用非常广泛的一种算法。作为Hash函数的一个应用实例。

        SHA诞生于1993年,全称是安全散列算法(Secure Hash Algorithm),由美国国家安全局(NSA)设计,之后被美国标准与技术研究院(NIST)收录到美国的联邦信息处理标准(FIPS)中,成为美国国家标准,SHA(后来被称作SHA-0)于1995被SHA-1(RFC3174)替代。SHA-1生成长度为160bit的摘要信息串,虽然之后又出现了SHA-224、SHA-256、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:

 

  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中,进入证书凭证类中进行校验。

posted @ 2019-09-04 17:35  那些年的代码  阅读(1470)  评论(0编辑  收藏  举报