redis 实现登陆次数限制

逻辑寻星者
• 阅读 2418

title: redis-login-limitation

<!--more-->

<div align="center">
利用 redis 实现登陆次数限制, 注解 + aop, 核心代码很简单.
</div>

基本思路

比如希望达到的要求是这样: 在 1min 内登陆异常次数达到5次, 锁定该用户 1h

那么登陆请求的参数中, 会有一个参数唯一标识一个 user, 比如 邮箱/手机号/userName

用这个参数作为key存入redis, 对应的value为登陆错误的次数, string 类型, 并设置过期时间为 1min. 当获取到的 value == "4" , 说明当前请求为第 5 次登陆异常, 锁定.

所谓的锁定, 就是将对应的value设置为某个标识符, 比如"lock", 并设置过期时间为 1h

核心代码

定义一个注解, 用来标识需要登陆次数校验的方法

package io.github.xiaoyureed.redispractice.anno;

import java.lang.annotation.*;

@Documented
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface RedisLimit {
    /**
     * 标识参数名, 必须是请求参数中的一个
     */
    String identifier();

    /**
     * 在多长时间内监控, 如希望在 60s 内尝试
     * 次数限制为5次, 那么 watch=60; unit: s
     */
    long watch();

    /**
     * 锁定时长, unit: s
     */
    long lock();

    /**
     * 错误的尝试次数
     */
    int times();
}

编写切面, 在目标方法前后进行校验, 处理...

package io.github.xiaoyureed.redispractice.aop;

@Component
@Aspect
// Ensure that current advice is outer compared with ControllerAOP
// so we can handling login limitation Exception in this aop advice.
//@Order(9)
@Slf4j
public class RedisLimitAOP {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Around("@annotation(io.github.xiaoyureed.redispractice.anno.RedisLimit)")
    public Object handleLimit(ProceedingJoinPoint joinPoint) {
        MethodSignature  methodSignature = (MethodSignature) joinPoint.getSignature();
        final Method     method          = methodSignature.getMethod();
        final RedisLimit redisLimitAnno  = method.getAnnotation(RedisLimit.class);// 貌似可以直接在方法参数中注入 todo

        final String identifier = redisLimitAnno.identifier();
        final long   watch      = redisLimitAnno.watch();
        final int    times      = redisLimitAnno.times();
        final long   lock       = redisLimitAnno.lock();
        // final ServletRequestAttributes att             = (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes();
        // final HttpServletRequest       request         = att.getRequest();
        // final String                   identifierValue = request.getParameter(identifier);

        String identifierValue = null;
        try {
            final Object arg           = joinPoint.getArgs()[0];
            final Field  declaredField = arg.getClass().getDeclaredField(identifier);
            declaredField.setAccessible(true);
            identifierValue = (String) declaredField.get(arg);
        } catch (NoSuchFieldException e) {
            log.error(">>> invalid identifier [{}], cannot find this field in request params", identifier);
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
        if (StringUtils.isBlank(identifierValue)) {
            log.error(">>> the value of RedisLimit.identifier cannot be blank, invalid identifier: {}", identifier);
        }

        // check User locked
        final ValueOperations<String, String> ssOps = stringRedisTemplate.opsForValue();
        final String                          flag  = ssOps.get(identifierValue);
        if (flag != null && "lock".contentEquals(flag)) {
            final BaseResp result = new BaseResp();
            result.setErrMsg("user locked");
            result.setCode("1");
            return new ResponseEntity<>(result, HttpStatus.OK);
        }

        ResponseEntity result;
        try {
            result = (ResponseEntity) joinPoint.proceed();
        } catch (Throwable e) {
            result = handleLoginException(e, identifierValue, watch, times, lock);
        }
        return result;
    }

    private ResponseEntity handleLoginException(Throwable e, String identifierValue, long watch, int times, long lock) {
        final BaseResp result = new BaseResp();
        result.setCode("1");
        if (e instanceof LoginException) {
            log.info(">>> handle login exception...");
            final ValueOperations<String, String> ssOps = stringRedisTemplate.opsForValue();
            Boolean                               exist = stringRedisTemplate.hasKey(identifierValue);
            // key doesn't exist, so it is the first login failure
            if (exist == null || !exist) {
                ssOps.set(identifierValue, "1", watch, TimeUnit.SECONDS);
                result.setErrMsg(e.getMessage());
                return new ResponseEntity<>(result, HttpStatus.OK);
            }

            String count = ssOps.get(identifierValue);
            // has been reached the limitation
            if (Integer.parseInt(count) + 1 == times) {
                log.info(">>> [{}] has been reached the limitation and will be locked for {}s", identifierValue, lock);
                ssOps.set(identifierValue, "lock", lock, TimeUnit.SECONDS);
                result.setErrMsg("user locked");
                return new ResponseEntity<>(result, HttpStatus.OK);
            }
            ssOps.increment(identifierValue);
            result.setErrMsg(e.getMessage() + "; you have try " + ssOps.get(identifierValue) + "times.");
        }
        log.error(">>> RedisLimitAOP cannot handle {}", e.getClass().getName());
        return new ResponseEntity<>(result, HttpStatus.OK);
    }
}

这样使用:

package io.github.xiaoyureed.redispractice.web;

@RestController
public class SessionResources {

    @Autowired
    private SessionService sessionService;

    /**
     * 1 min 之内尝试超过5次, 锁定 user 1h
     */
    @RedisLimit(identifier = "name", watch = 30, times = 5, lock = 10)
    @RequestMapping(value = "/session", method = RequestMethod.POST)
    public ResponseEntity<LoginResp> login(@Validated @RequestBody LoginReq req) {
        return new ResponseEntity<>(sessionService.login(req), HttpStatus.OK);
    }
}

references

https://github.com/xiaoyureed...

点赞
收藏
评论区
推荐文章
blmius blmius
4年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
美凌格栋栋酱 美凌格栋栋酱
7个月前
Oracle 分组与拼接字符串同时使用
SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(
Stella981 Stella981
3年前
Opencv中Mat矩阵相乘——点乘、dot、mul运算详解
Opencv中Mat矩阵相乘——点乘、dot、mul运算详解2016年09月02日00:00:36 \牧野(https://www.oschina.net/action/GoToLink?urlhttps%3A%2F%2Fme.csdn.net%2Fdcrmg) 阅读数:59593
Stella981 Stella981
3年前
Linux查看GPU信息和使用情况
1、Linux查看显卡信息:lspci|grepivga2、使用nvidiaGPU可以:lspci|grepinvidia!(https://oscimg.oschina.net/oscnet/36e7c7382fa9fe49068e7e5f8825bc67a17.png)前边的序号"00:0f.0"是显卡的代
Stella981 Stella981
3年前
KVM调整cpu和内存
一.修改kvm虚拟机的配置1、virsheditcentos7找到“memory”和“vcpu”标签,将<namecentos7</name<uuid2220a6d1a36a4fbb8523e078b3dfe795</uuid
Stella981 Stella981
3年前
Nginx + lua +[memcached,redis]
精品案例1、Nginxluamemcached,redis实现网站灰度发布2、分库分表/基于Leaf组件实现的全球唯一ID(非UUID)3、Redis独立数据监控,实现订单超时操作/MQ死信操作SelectPollEpollReactor模型4、分布式任务调试Quartz应用
Easter79 Easter79
3年前
Twitter的分布式自增ID算法snowflake (Java版)
概述分布式系统中,有一些需要使用全局唯一ID的场景,这种时候为了防止ID冲突可以使用36位的UUID,但是UUID有一些缺点,首先他相对比较长,另外UUID一般是无序的。有些时候我们希望能使用一种简单一些的ID,并且希望ID能够按照时间有序生成。而twitter的snowflake解决了这种需求,最初Twitter把存储系统从MySQL迁移
Wesley13 Wesley13
3年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
3年前
API 调用次数限制实现
!API调用次数限制实现(https://pic1.zhimg.com/ac955973d508b10db58572fa2a5fa5b8_r.jpg)API调用次数限制实现!YigWoo(https://pic1.zhimg.com/50/8f3a0d2b8_xs.jpg)(https
Python进阶者 Python进阶者
1年前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这