JavaSpringBootShiro
数据库中密码相关字段都不是明文,肯定是加密之后的,传统方式一般是使用MD5加密。
单纯使用不加盐的MD5加密方式,当两个用户的密码相同时,会发现数据库中存在相同内容的密码,这样也是不安全的。希望即便是两个人的原始密码一样,加密后的结果也不一样。
下面进行shiro密码 加密加盐配置:

1、ShiroConfig中添加密码比较器

  1. /**
  2. * 配置密码比较器
  3. * @return
  4. */
  5. @Bean("credentialsMatcher")
  6. public RetryLimitHashedCredentialsMatcher retryLimitHashedCredentialsMatcher(){
  7. RetryLimitHashedCredentialsMatcher retryLimitHashedCredentialsMatcher = new RetryLimitHashedCredentialsMatcher();
  8. retryLimitHashedCredentialsMatcher.setRedisManager(redisManager());
  9. //如果密码加密,可以打开下面配置
  10. //加密算法的名称
  11. retryLimitHashedCredentialsMatcher.setHashAlgorithmName("MD5");
  12. //配置加密的次数
  13. retryLimitHashedCredentialsMatcher.setHashIterations(2);
  14. //是否存储为16进制
  15. retryLimitHashedCredentialsMatcher.setStoredCredentialsHexEncoded(true);
  16. return retryLimitHashedCredentialsMatcher;
  17. }

2、将密码比较器配置给ShiroRealm

  1. /**
  2. * 身份认证realm; (这个需要自己写,账号密码校验;权限等)
  3. * @return
  4. */
  5. @Bean
  6. public ShiroRealm shiroRealm(){
  7. ShiroRealm shiroRealm = new ShiroRealm();
  8. shiroRealm.setCachingEnabled(true);
  9. //启用身份验证缓存,即缓存AuthenticationInfo信息,默认false
  10. shiroRealm.setAuthenticationCachingEnabled(true);
  11. //缓存AuthenticationInfo信息的缓存名称 在ehcache-shiro.xml中有对应缓存的配置
  12. shiroRealm.setAuthenticationCacheName("authenticationCache");
  13. //启用授权缓存,即缓存AuthorizationInfo信息,默认false
  14. shiroRealm.setAuthorizationCachingEnabled(true);
  15. //缓存AuthorizationInfo信息的缓存名称 在ehcache-shiro.xml中有对应缓存的配置
  16. shiroRealm.setAuthorizationCacheName("authorizationCache");
  17. //配置自定义密码比较器
  18. shiroRealm.setCredentialsMatcher(retryLimitHashedCredentialsMatcher());
  19. return shiroRealm;
  20. }

3、密码比较器RetryLimitHashedCredentialsMatcher

自定义的密码比较器,跟前面博客中逻辑没有变化,唯一变的是 继承的类从 SimpleCredentialsMatcher 变为 HashedCredentialsMatcher
在密码比较器中做了:如果用户输入密码连续错误5次,将锁定账号。
RetryLimitHashedCredentialsMatcher完整内容如下:

  1. package com.shiro.config;
  2. import java.util.concurrent.atomic.AtomicInteger;
  3. import com.springboot.test.shiro.modules.user.dao.UserMapper;
  4. import com.springboot.test.shiro.modules.user.dao.entity.User;
  5. import org.apache.log4j.Logger;
  6. import org.apache.shiro.authc.AuthenticationInfo;
  7. import org.apache.shiro.authc.AuthenticationToken;
  8. import org.apache.shiro.authc.LockedAccountException;
  9. import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
  10. import org.springframework.beans.factory.annotation.Autowired;
  11. /**
  12. * @description: 登陆次数限制
  13. */
  14. public class RetryLimitHashedCredentialsMatcher extends HashedCredentialsMatcher {
  15. private static final Logger logger = Logger.getLogger(RetryLimitHashedCredentialsMatcher.class);
  16. public static final String DEFAULT_RETRYLIMIT_CACHE_KEY_PREFIX = "shiro:cache:retrylimit:";
  17. private String keyPrefix = DEFAULT_RETRYLIMIT_CACHE_KEY_PREFIX;
  18. @Autowired
  19. private UserMapper userMapper;
  20. private RedisManager redisManager;
  21. public void setRedisManager(RedisManager redisManager) {
  22. this.redisManager = redisManager;
  23. }
  24. private String getRedisKickoutKey(String username) {
  25. return this.keyPrefix + username;
  26. }
  27. @Override
  28. public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
  29. //获取用户名
  30. String username = (String)token.getPrincipal();
  31. //获取用户登录次数
  32. AtomicInteger retryCount = (AtomicInteger)redisManager.get(getRedisKickoutKey(username));
  33. if (retryCount == null) {
  34. //如果用户没有登陆过,登陆次数加1 并放入缓存
  35. retryCount = new AtomicInteger(0);
  36. }
  37. if (retryCount.incrementAndGet() > 5) {
  38. //如果用户登陆失败次数大于5次 抛出锁定用户异常 并修改数据库字段
  39. User user = userMapper.findByUserName(username);
  40. if (user != null && "0".equals(user.getState())){
  41. //数据库字段 默认为 0 就是正常状态 所以 要改为1
  42. //修改数据库的状态字段为锁定
  43. user.setState("1");
  44. userMapper.update(user);
  45. }
  46. logger.info("锁定用户" + user.getUsername());
  47. //抛出用户锁定异常
  48. throw new LockedAccountException();
  49. }
  50. //判断用户账号和密码是否正确
  51. boolean matches = super.doCredentialsMatch(token, info);
  52. if (matches) {
  53. //如果正确,从缓存中将用户登录计数 清除
  54. redisManager.del(getRedisKickoutKey(username));
  55. }{
  56. redisManager.set(getRedisKickoutKey(username), retryCount);
  57. }
  58. return matches;
  59. }
  60. /**
  61. * 根据用户名 解锁用户
  62. * @param username
  63. * @return
  64. */
  65. public void unlockAccount(String username){
  66. User user = userMapper.findByUserName(username);
  67. if (user != null){
  68. //修改数据库的状态字段为锁定
  69. user.setState("0");
  70. userMapper.update(user);
  71. redisManager.del(getRedisKickoutKey(username));
  72. }
  73. }
  74. }

4、修改ShiroRealm中doGetAuthenticationInfo方法

  1. package com.springboot.shiro.realm;
  2. import com.springboot.test.shiro.modules.user.dao.PermissionMapper;
  3. import com.springboot.test.shiro.modules.user.dao.RoleMapper;
  4. import com.springboot.test.shiro.modules.user.dao.entity.Permission;
  5. import com.springboot.test.shiro.modules.user.dao.entity.Role;
  6. import com.springboot.test.shiro.modules.user.dao.UserMapper;
  7. import com.springboot.test.shiro.modules.user.dao.entity.User;
  8. import org.apache.shiro.SecurityUtils;
  9. import org.apache.shiro.authc.*;
  10. import org.apache.shiro.authz.AuthorizationInfo;
  11. import org.apache.shiro.authz.SimpleAuthorizationInfo;
  12. import org.apache.shiro.realm.AuthorizingRealm;
  13. import org.apache.shiro.subject.PrincipalCollection;
  14. import org.springframework.beans.factory.annotation.Autowired;
  15. import java.util.Set;
  16. /**
  17. * @description: 在Shiro中,最终是通过Realm来获取应用程序中的用户、角色及权限信息的
  18. * 在Realm中会直接从我们的数据源中获取Shiro需要的验证信息。可以说,Realm是专用于安全框架的DAO.
  19. */
  20. public class ShiroRealm extends AuthorizingRealm {
  21. @Autowired
  22. private UserMapper userMapper;
  23. @Autowired
  24. private RoleMapper roleMapper;
  25. @Autowired
  26. private PermissionMapper permissionMapper;
  27. /**
  28. * 验证用户身份
  29. * @param authenticationToken
  30. * @return
  31. * @throws AuthenticationException
  32. */
  33. @Override
  34. protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
  35. //获取用户名密码 第一种方式
  36. //String username = (String) authenticationToken.getPrincipal();
  37. //String password = new String((char[]) authenticationToken.getCredentials());
  38. //获取用户名 密码 第二种方式
  39. UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) authenticationToken;
  40. String username = usernamePasswordToken.getUsername();
  41. String password = new String(usernamePasswordToken.getPassword());
  42. //从数据库查询用户信息
  43. User user = this.userMapper.findByUserName(username);
  44. //可以在这里直接对用户名校验,或者调用 CredentialsMatcher 校验
  45. if (user == null) {
  46. throw new UnknownAccountException("用户名或密码错误!");
  47. }
  48. //这里将 密码对比 注销掉,否则 无法锁定 要将密码对比 交给 密码比较器
  49. //if (!password.equals(user.getPassword())) {
  50. // throw new IncorrectCredentialsException("用户名或密码错误!");
  51. //}
  52. if ("1".equals(user.getState())) {
  53. throw new LockedAccountException("账号已被锁定,请联系管理员!");
  54. }
  55. SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user, user.getPassword(),new MyByteSource(user.getUsername()),getName());
  56. return info;
  57. }
  58. /**
  59. * 授权用户权限
  60. * 授权的方法是在碰到<shiro:hasPermission name=''></shiro:hasPermission>标签的时候调用的
  61. * 它会去检测shiro框架中的权限(这里的permissions)是否包含有该标签的name值,如果有,里面的内容显示
  62. * 如果没有,里面的内容不予显示(这就完成了对于权限的认证.)
  63. *
  64. * shiro的权限授权是通过继承AuthorizingRealm抽象类,重载doGetAuthorizationInfo();
  65. * 当访问到页面的时候,链接配置了相应的权限或者shiro标签才会执行此方法否则不会执行
  66. * 所以如果只是简单的身份认证没有权限的控制的话,那么这个方法可以不进行实现,直接返回null即可。
  67. *
  68. * 在这个方法中主要是使用类:SimpleAuthorizationInfo 进行角色的添加和权限的添加。
  69. * authorizationInfo.addRole(role.getRole()); authorizationInfo.addStringPermission(p.getPermission());
  70. *
  71. * 当然也可以添加set集合:roles是从数据库查询的当前用户的角色,stringPermissions是从数据库查询的当前用户对应的权限
  72. * authorizationInfo.setRoles(roles); authorizationInfo.setStringPermissions(stringPermissions);
  73. *
  74. * 就是说如果在shiro配置文件中添加了filterChainDefinitionMap.put("/add", "perms[权限添加]");
  75. * 就说明访问/add这个链接必须要有“权限添加”这个权限才可以访问
  76. *
  77. * 如果在shiro配置文件中添加了filterChainDefinitionMap.put("/add", "roles[100002],perms[权限添加]");
  78. * 就说明访问/add这个链接必须要有 "权限添加" 这个权限和具有 "100002" 这个角色才可以访问
  79. * @param principalCollection
  80. * @return
  81. */
  82. @Override
  83. protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
  84. System.out.println("查询权限方法调用了!!!");
  85. //获取用户
  86. User user = (User) SecurityUtils.getSubject().getPrincipal();
  87. //获取用户角色
  88. Set<Role> roles =this.roleMapper.findRolesByUserId(user.getUid());
  89. //添加角色
  90. SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
  91. for (Role role : roles) {
  92. authorizationInfo.addRole(role.getRole());
  93. }
  94. //获取用户权限
  95. Set<Permission> permissions = this.permissionMapper.findPermissionsByRoleId(roles);
  96. //添加权限
  97. for (Permission permission:permissions) {
  98. authorizationInfo.addStringPermission(permission.getPermission());
  99. }
  100. return authorizationInfo;
  101. }
  102. /**
  103. * 重写方法,清除当前用户的的 授权缓存
  104. * @param principals
  105. */
  106. @Override
  107. public void clearCachedAuthorizationInfo(PrincipalCollection principals) {
  108. super.clearCachedAuthorizationInfo(principals);
  109. }
  110. /**
  111. * 重写方法,清除当前用户的 认证缓存
  112. * @param principals
  113. */
  114. @Override
  115. public void clearCachedAuthenticationInfo(PrincipalCollection principals) {
  116. super.clearCachedAuthenticationInfo(principals);
  117. }
  118. @Override
  119. public void clearCache(PrincipalCollection principals) {
  120. super.clearCache(principals);
  121. }
  122. /**
  123. * 自定义方法:清除所有 授权缓存
  124. */
  125. public void clearAllCachedAuthorizationInfo() {
  126. getAuthorizationCache().clear();
  127. }
  128. /**
  129. * 自定义方法:清除所有 认证缓存
  130. */
  131. public void clearAllCachedAuthenticationInfo() {
  132. getAuthenticationCache().clear();
  133. }
  134. /**
  135. * 自定义方法:清除所有的 认证缓存 和 授权缓存
  136. */
  137. public void clearAllCache() {
  138. clearAllCachedAuthenticationInfo();
  139. clearAllCachedAuthorizationInfo();
  140. }
  141. }

跟之前的 ShiroRealm 相比,唯一改变的了
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user, user.getPassword(),new MyByteSource(user.getUsername()),getName());这一行代码,添加了 加盐参数。
注意:这里使用了 MyByteSource 而不是 ByteSource.Util.bytes(user.getUsername())

5、下面是生成密码加密加盐的方法,可以在注册的时候对明文进行加密 加盐 入库

  1. package com.fcant.shiro.test;
  2. import org.apache.shiro.crypto.hash.SimpleHash;
  3. import org.apache.shiro.util.ByteSource;
  4. import org.junit.Test;
  5. /**
  6. * @description: 给 密码进行 加密加盐 盐值默认为 用户名
  7. */
  8. public class PasswordSaltTest {
  9. @Test
  10. public void test() throws Exception {
  11. System.out.println(md5("123456","admin"));
  12. }
  13. public static final String md5(String password, String salt){
  14. //加密方式
  15. String hashAlgorithmName = "MD5";
  16. //盐:为了即使相同的密码不同的盐加密后的结果也不同
  17. ByteSource byteSalt = ByteSource.Util.bytes(salt);
  18. //密码
  19. Object source = password;
  20. //加密次数
  21. int hashIterations = 2;
  22. SimpleHash result = new SimpleHash(hashAlgorithmName, source, byteSalt, hashIterations);
  23. return result.toString();
  24. }
  25. }

可能出现的问题

可能会发生这种情况,测试发现密码不对,具体原因debug都可以发现,这里直接把结果发出来:

第一种:

debug发现 传入的密码 经过加密加盐之后是对的,但是 从数据库中 获取的密码 却是明文,原因是在ShiroRealm中 doGetAuthenticationInfo方法中,最后返回的SimpleAuthenticationInfo 第二个参数 是密码,这个密码 不是从前台传过来的密码,而是从数据库中查询出来的

第二种:

debug发现 传入的密码 经过加密加盐之后是对的,但是 从数据库中 获取的密码 却是更长的一段密文,原因是在ShiroConfig中配置的RetryLimitHashedCredentialsMatcher一个属性:

  1. //是否存储为16进制
  2. retryLimitHashedCredentialsMatcher.setStoredCredentialsHexEncoded(true);

默认是true,如果改为false,则会出现 对比的时候从数据库拿出密码,然后转 base64 变成了另外一个更长的字符串,所以怎么对比都是不通过的。