redis 实现登陆次数限制

Posted xiaoyuhah

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了redis 实现登陆次数限制相关的知识,希望对你有一定的参考价值。


title: redis-login-limitation

利用 redis 实现登陆次数限制, 注解 + aop, 核心代码很简单.

基本思路

比如希望达到的要求是这样: 在 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/redis-login-limitation

以上是关于redis 实现登陆次数限制的主要内容,如果未能解决你的问题,请参考以下文章

Redis限制在规定时间范围内登陆错误次数限制

.NetCore利用Redis实现对接口访问次数限制

必看企业级Redis锁资产巡检扫描业务场景实现(加锁限制扫描次数)

redis实现网关限流(限制API调用次数1000次/分)

关于JAVA的WEB开发,想在页面中实现在一段时间内限制同一个IP登陆失败次数过多

DISCUZ登陆限制IP绕过