Shiro+Redis实现登录次数冻结的示例

 更新时间:2020年12月17日 11:48:10   作者:一个JavaBean  
这篇文章主要介绍了Shiro+Redis实现登录次数冻结,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧

概述

假设我们需要有这样一个场景:如果用户连续输错5次密码,那可能说明有人在搞事情,所以需要暂时冻结该账户的登录功能

关于Shiro整合JWT,可以看这里:Springboot实现Shiro+JWT认证

假设我们的项目中用到了shiro,因为Shiro是建立在完善的接口驱动设计和面向对象原则之上的,支持各种自定义行为,所以我们可以结合Shiro框架的认证模块和redis来实现这个功能。

思路

我们大体的思路如下:

image-20200225151338221

  • 用户登录
  • Shiro去Redis检查账户的登录错误次数是否超过规定范围(超过了就是所谓的冻结)
  • Shiro进行密码比对
  • 如果登录失败,则去Redis里记录:登录错误次数+1
  • 如果密码正确,则登录成功,删除Redis里的登录错误记录

前期准备

除了需要用到Shiro以外,我们也需要用到Redis,这里需要先配置好RedisTemplate,(由于这个不是重点,我就把代码和配置方法贴在文章的最后了),另外,在Controller层,登录接口的异常处理除了之前的登录错误,还需要新增一个账户冻结类的异常,代码如下:

 @PostMapping(value = "/login")
 public AccountVO login(String userName, String password){
  
  //尝试登录
  Subject subject = SecurityUtils.getSubject();
  try {
   //通过shiro提供的安全接口来进行认证
   subject.login(new UsernamePasswordToken(userName, password));
  } catch (ExcessiveAttemptsException e1) {
   //新增一个账户锁定类错误
   throw new AccountLockedException();
  } catch (Exception e) {
   //其他的错误判定
   throw new LoginFailed();
  }
  //聚合登录信息
  AccountVO account = accountService.getAccountByUserName(userName);
  //返回正确登录的结果
  return account;
 }

自定义Shiro认证管理器

HashedCredentialsMatcher

当你在上面的Controller层调用subject.login方法后,会进入到自定义的Realm里去,然后慢慢进入到Shiro当前的Security Manager里定义的HashedCredentialsMatcher认证管理器的doCredentialsMatch方法,进行密码匹配,原版代码如下:

 /**
  * This implementation first hashes the {@code token}'s credentials, potentially using a
  * {@code salt} if the {@code info} argument is a
  * {@link org.apache.shiro.authc.SaltedAuthenticationInfo SaltedAuthenticationInfo}. It then compares the hash
  * against the {@code AuthenticationInfo}'s
  * {@link #getCredentials(org.apache.shiro.authc.AuthenticationInfo) already-hashed credentials}. This method
  * returns {@code true} if those two values are {@link #equals(Object, Object) equal}, {@code false} otherwise.
  *
  * @param token the {@code AuthenticationToken} submitted during the authentication attempt.
  * @param info the {@code AuthenticationInfo} stored in the system matching the token principal
  * @return {@code true} if the provided token credentials hash match to the stored account credentials hash,
  *   {@code false} otherwise
  * @since 1.1
  */
 @Override
 public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
  Object tokenHashedCredentials = hashProvidedCredentials(token, info);
  Object accountCredentials = getCredentials(info);
  return equals(tokenHashedCredentials, accountCredentials);
 }

可以发现,原版的逻辑很简单,就做了两件事,获取密码,比对密码。

由于我们需要联动Redis,在每次登录前都做一次冻结检查,每次遇到登录失败之后还需要实现对redis的写操作,所以现在需要重写一个认证管理器去配置到Security Manager里。

CustomMatcher

我们自定义一个CustomMatcher,这个类继承了HashedCredentialsMatcher,唯独重写了doCredentialsMatch方法,在这里面加入了我们自己的逻辑,代码如下:

import com.imlehr.internship.redis.RedisStringService;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.ExcessiveAttemptsException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.springframework.beans.factory.annotation.Autowired;

/**
 * @author Lehr
 * @create: 2020-02-25
 */
public class CustomMatcher extends HashedCredentialsMatcher {

	//这个是redis里的key的统一前缀
 private static final String PREFIX = "USER_LOGIN_FAIL:";

 @Autowired
 RedisStringService redisUtils;

 @Override
 public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {

  //检查本账号是否被冻结

  //先获取用户的登录名字 
  UsernamePasswordToken myToken = (UsernamePasswordToken) token;

  String userName = myToken.getUsername();

  //初始化错误登录次数
  Integer errorNum = 0;

  //从数据库里获取错误次数
  String errorTimes = (String)redisUtils.get(PREFIX+userName);

  if(errorTimes!=null && errorTimes.trim().length()>0)
  {
   //如果得到的字符串不为空不为空
   errorNum = Integer.parseInt(errorTimes);
  }

  //如果用户错误登录次数超过十次
  if (errorNum >= 10) {
   //抛出账号锁定异常类
   throw new ExcessiveAttemptsException();
  }

  //先按照父类的规则来比对密码
  boolean matched = super.doCredentialsMatch(token, info);

  if(matched)
  {
   //清空错误次数
   redisUtils.remove(PREFIX+userName);
  }
  else{
   //添加一次错误次数 秒为单位
   redisUtils.set(PREFIX+userName,String.valueOf(++errorNum),60*30L);
  }

  return matched;
 }
}

首先,我们从AuthenticationToken里面拿到之前存入的用户的登录信息,这个对象其实就是你在Controller层

subject.login(new UsernamePasswordToken(userName, password));

这一步里面你实例化的对象

然后,通过用户的登录名加上固定前缀(为了防止防止userName和其他主键冲突)去Redis里获取到错误次数。判断账户是否被冻结的逻辑其实就是看当前用户的错误登录次数是否超过某个规定值,这里我们定为5次。

接下来,说明用户没有被冻结,可以执行登录操作,所以我们就直接调用父类的验证方法来进行密码比对(就是之前提到的那三行代码),得到密码的比对结果

如果比对一致,那么就成功登录,返回true即可,也可以选择一旦登录成功,就消除所有错误次数记录,上面的代码就是这样做的。

如果对比结果不一样,那就再添加一次错误记录,然后返回false

测试

第一次登录:页面结果:

image-20200225153743161

Redis中:

image-20200225154251776

然后连续错误10次:

页面结果:

image-20200225154434428

Redis中:

image-20200225154406113

然后等待了半小时之后(其实我调成了5分钟)

再次尝试错误密码登录:

image-20200225153743161

再次报错,此时Redis里由于之前的记录到期了,自动销毁了,所以再次触发错误又会添加一次错误记录

image-20200225154645626

现在尝试一次正确登录:

image-20200225154735645

成功登录

查看Redis:

image-20200225154807011

🎉Done!

附RedisTemplate代码

配置类

import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Configuration
public class RedisConfig {

 @Bean
 public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory)
 {
	//我就用的默认的序列化处理器
  StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
  JdkSerializationRedisSerializer ser = new JdkSerializationRedisSerializer();

  RedisTemplate<String, Object> template = new RedisTemplate<String, Object>();
  template.setConnectionFactory(redisConnectionFactory);

  template.setKeySerializer(stringRedisSerializer);
  template.setValueSerializer(ser);
  return template;
 }

 @Bean
 public RedisStringService myStringRedisTemplate()
 {
  return new RedisStringService();
 }
}

工具类RedisStringService

一个只能用来处理Value是String的工具类,就是我在CustomMatcher里Autowired的这个类

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Service;

import java.util.concurrent.TimeUnit;

public class RedisStringService {

 @Autowired
 protected StringRedisTemplate redisTemplate;

 /**
  * 写入redis缓存(不设置expire存活时间)
  * @param key
  * @param value
  * @return
  */
 public boolean set(final String key, String value){
  boolean result = false;
  try {
   ValueOperations operations = redisTemplate.opsForValue();
   operations.set(key, value);
   result = true;
  } catch (Exception e) {
   e.getMessage();
  }
  return result;
 }

 /**
  * 写入redis缓存(设置expire存活时间)
  * @param key
  * @param value
  * @param expire
  * @return
  */
 public boolean set(final String key, String value, Long expire){
  boolean result = false;
  try {
   ValueOperations operations = redisTemplate.opsForValue();
   operations.set(key, value);
   redisTemplate.expire(key, expire, TimeUnit.SECONDS);
   result = true;
  } catch (Exception e) {
   e.getMessage();
  }
  return result;
 }


 /**
  * 读取redis缓存
  * @param key
  * @return
  */
 public Object get(final String key){
  Object result = null;
  try {
   ValueOperations operations = redisTemplate.opsForValue();
   result = operations.get(key);
  } catch (Exception e) {
   e.getMessage();
  }
  return result;
 }

 /**
  * 判断redis缓存中是否有对应的key
  * @param key
  * @return
  */
 public boolean exists(final String key){
  boolean result = false;
  try {
   result = redisTemplate.hasKey(key);
  } catch (Exception e) {
   e.getMessage();
  }
  return result;
 }

 /**
  * redis根据key删除对应的value
  * @param key
  * @return
  */
 public boolean remove(final String key){
  boolean result = false;
  try {
   if(exists(key)){
    redisTemplate.delete(key);
   }
   result = true;
  } catch (Exception e) {
   e.getMessage();
  }
  return result;
 }

 /**
  * redis根据keys批量删除对应的value
  * @param keys
  * @return
  */
 public void remove(final String... keys){
  for(String key : keys){
   remove(key);
  }
 }
}

到此这篇关于Shiro+Redis实现登录次数冻结的文章就介绍到这了,更多相关Shiro+Redis登录冻结内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • Netty分布式pipeline管道Handler的删除逻辑操作

    Netty分布式pipeline管道Handler的删除逻辑操作

    这篇文章主要为大家介绍了Netty分布式pipeline管道Handler的删除逻辑操作,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-03-03
  • JavaWeb使用Session和Cookie实现登录认证

    JavaWeb使用Session和Cookie实现登录认证

    本篇文章主要介绍了JavaWeb使用Session和Cookie实现登录认证,具有一定的参考价值,感兴趣的小伙伴们可以参考一下。
    2017-03-03
  • Mybatis插入语句默认值不生效的问题及解决

    Mybatis插入语句默认值不生效的问题及解决

    这篇文章主要介绍了Mybatis插入语句默认值不生效的问题及解决方案,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-07-07
  • Spring一步到位精通拦截器

    Spring一步到位精通拦截器

    拦截器(Interceptor)是一种动态拦截方法调用的机制,在SpringMVC中动态拦截控制器方法的执行。本文将详细讲讲SpringMVC中拦截器的概念及入门案例,感兴趣的可以尝试一下
    2023-01-01
  • logback的addtivity属性定义源码解读

    logback的addtivity属性定义源码解读

    这篇文章主要为大家介绍了logback的addtivity属性定义源码解读,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-11-11
  • Java里volatile关键字是什么意思

    Java里volatile关键字是什么意思

    volatile具有synchronized关键字的“可见性”,但是没有synchronized关键字的“并发正确性”,也就是说不保证线程执行的有序性。这篇文章主要介绍了Java里volatile关键字是什么意思的相关资料,需要的朋友可以参考下
    2016-11-11
  • javaweb分页原理详解

    javaweb分页原理详解

    这篇文章主要为大家详细介绍了javaweb分页的原理,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2017-04-04
  • Java 如何快速,优雅的实现导出Excel

    Java 如何快速,优雅的实现导出Excel

    这篇文章主要介绍了Java 如何快速,优雅的实现导出Excel,帮助大家更好的理解和学习使用Java,感兴趣的朋友可以了解下
    2021-03-03
  • 基于Java并发容器ConcurrentHashMap#put方法解析

    基于Java并发容器ConcurrentHashMap#put方法解析

    下面小编就为大家带来一篇基于Java并发容器ConcurrentHashMap#put方法解析。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2017-06-06
  • Spring AOP执行先后顺序实例详解

    Spring AOP执行先后顺序实例详解

    这篇文章主要介绍了Spring AOP执行先后顺序实例详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2020-01-01

最新评论