0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

三次输入密码错误怎么办?

jf_ro2CN3Fa 来源:稀土掘金 2023-11-28 10:00 次阅读

1 故事背景

忘记密码这件事,相信绝大多数人都遇到过,输一次错一次,错到几次以上,就不允许你继续尝试了。

但当你尝试重置密码,又发现新密码不能和原密码重复:

虽然,但是,密码还是很重要的,顺便我有了一个问题:三次输错密码后,系统是怎么做到不让我继续尝试的?

2 我想了想,有如下几个问题需要搞定

是只有输错密码才锁定,还是账户名和密码任何一个输错就锁定?

输错之后也不是完全冻结,为啥隔了几分钟又可以重新输了?

技术栈到底麻不麻烦?

去网上搜了搜,也问了下ChatGPT,找到一套解决方案:SpringBoot+Redis+Lua脚本。

这套方案也不算新,很早就有人在用了,不过难得是自己想到的问题和解法,就记录一下吧。

顺便回答一下上面的三个问题:

锁定的是IP,不是输入的账户名或者密码,也就是说任一一个输错3次就会被锁定

Redis的Lua脚本中实现了key过期策略,当key消失时锁定自然也就消失了

技术栈同SpringBoot+Redis+Lua脚本

3 那么自己动手实现一下

前端部分

首先写一个账密输入页面,使用很简单HTML加表单提交




登录页面




用户名

密码





效果如下:

e9fe2882-8b73-11ee-939d-92fbcf53809c.png

后端部分

技术选型分析

首先我们画一个流程图来分析一下这个登录限制流程

ea140d8c-8b73-11ee-939d-92fbcf53809c.png

从流程图上看,首先访问次数的统计与判断不是在登录逻辑执行后,而是执行前就加1了;

其次登录逻辑的成功与失败并不会影响到次数的统计;

最后还有一点流程图上没有体现出来,这个次数的统计是有过期时间的,当过期之后又可以重新登录了。

那为什么是Redis+Lua脚本呢?

Redis的选择不难看出,这个流程比较重要的是存在一个用来计数的变量,这个变量既要满足分布式读写需求,还要满足全局递增或递减的需求,那Redis的incr方法是最优选了。

那为什么需要Lua脚本呢?流程上在验证用户操作前有些操作,如图:

ea32f9c2-8b73-11ee-939d-92fbcf53809c.png

这里至少有3步Redis的操作,get、incr、expire,如果全放到应用里面来操作,有点慢且浪费资源。

Lua脚本的优点如下:

减少网络开销。 可以将多个请求通过脚本的形式一次发送,减少网络时延。

原子操作。 Redis会将整个脚本作为一个整体执行,中间不会被其他请求插入。因此在脚本运行过程中无需担心会出现竞态条件,无需使用事务。

复用。 客户端发送的脚本会永久存在redis中,这样其他客户端可以复用这一脚本,而不需要使用代码完成相同的逻辑。

最后为了增加功能的复用性,我打算使用Java注解的方式实现这个功能。

代码实现

项目结构如下

ea4302ae-8b73-11ee-939d-92fbcf53809c.png

配置文件

pom.xml



4.0.0

org.springframework.boot
spring-boot-starter-parent
2.7.11
 

com.example
LoginLimit
0.0.1-SNAPSHOT
LoginLimit
DemoprojectforSpringBoot

1.8



org.springframework.boot
spring-boot-starter-web



org.springframework.boot
spring-boot-starter-test
test

 

org.springframework.boot
spring-boot-starter-data-redis

 

redis.clients
jedis

 

org.aspectj
aspectjweaver

 

org.apache.commons
commons-lang3

 

com.google.guava
guava
23.0

 

org.projectlombok
lombok
true






org.springframework.boot
spring-boot-maven-plugin





application.properties

#Redis配置
spring.redis.host=127.0.0.1
spring.redis.port=6379
spring.redis.password=
spring.redis.timeout=1000
#Jedis配置
spring.redis.jedis.pool.min-idle=0
spring.redis.jedis.pool.max-idle=500
spring.redis.jedis.pool.max-active=2000
spring.redis.jedis.pool.max-wait=10000

注解部分

LimitCount.java

packagecom.example.loginlimit.annotation;

importjava.lang.annotation.ElementType;
importjava.lang.annotation.Retention;
importjava.lang.annotation.RetentionPolicy;
importjava.lang.annotation.Target;

/**
*次数限制注解
*作用在接口方法上
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public@interfaceLimitCount{
/**
*资源名称,用于描述接口功能
*/
Stringname()default"";

/**
*资源key
*/
Stringkey()default"";

/**
*keyprefix
*
*@return
*/
Stringprefix()default"";

/**
*时间的,单位秒
*默认60s过期
*/
intperiod()default60;

/**
*限制访问次数
*默认3次
*/
intcount()default3;
}

核心处理逻辑类:LimitCountAspect.java

packagecom.example.loginlimit.aspect;

importjava.io.Serializable;
importjava.lang.reflect.Method;
importjava.util.Objects;

importjavax.servlet.http.HttpServletRequest;

importcom.example.loginlimit.annotation.LimitCount;
importcom.example.loginlimit.util.IPUtil;
importcom.google.common.collect.ImmutableList;
importlombok.extern.slf4j.Slf4j;
importorg.apache.commons.lang3.StringUtils;
importorg.aspectj.lang.ProceedingJoinPoint;
importorg.aspectj.lang.annotation.Around;
importorg.aspectj.lang.annotation.Aspect;
importorg.aspectj.lang.annotation.Pointcut;
importorg.aspectj.lang.reflect.MethodSignature;
importorg.springframework.beans.factory.annotation.Autowired;
importorg.springframework.data.redis.core.RedisTemplate;
importorg.springframework.data.redis.core.script.DefaultRedisScript;
importorg.springframework.data.redis.core.script.RedisScript;
importorg.springframework.stereotype.Component;
importorg.springframework.web.context.request.RequestContextHolder;
importorg.springframework.web.context.request.ServletRequestAttributes;

@Slf4j
@Aspect
@Component
publicclassLimitCountAspect{

privatefinalRedisTemplatelimitRedisTemplate;

@Autowired
publicLimitCountAspect(RedisTemplatelimitRedisTemplate){
this.limitRedisTemplate=limitRedisTemplate;
}

@Pointcut("@annotation(com.example.loginlimit.annotation.LimitCount)")
publicvoidpointcut(){
//donothing
}

@Around("pointcut()")
publicObjectaround(ProceedingJoinPointpoint)throwsThrowable{
HttpServletRequestrequest=((ServletRequestAttributes)Objects.requireNonNull(
RequestContextHolder.getRequestAttributes())).getRequest();

MethodSignaturesignature=(MethodSignature)point.getSignature();
Methodmethod=signature.getMethod();
LimitCountannotation=method.getAnnotation(LimitCount.class);
//注解名称
Stringname=annotation.name();
//注解key
Stringkey=annotation.key();
//访问IP
Stringip=IPUtil.getIpAddr(request);
//过期时间
intlimitPeriod=annotation.period();
//过期次数
intlimitCount=annotation.count();

ImmutableListkeys=ImmutableList.of(StringUtils.join(annotation.prefix()+"_",key,ip));
StringluaScript=buildLuaScript();
RedisScriptredisScript=newDefaultRedisScript<>(luaScript,Number.class);
Numbercount=limitRedisTemplate.execute(redisScript,keys,limitCount,limitPeriod);
log.info("IP:{}第{}次访问key为{},描述为[{}]的接口",ip,count,keys,name);
if(count!=null&&count.intValue()<= limitCount) {
            return point.proceed();
        } else {
            return "接口访问超出频率限制";
        }
    }

    /**
     * 限流脚本
     * 调用的时候不超过阈值,则直接返回并执行计算器自加。
     *
     * @return lua脚本
     */
    private String buildLuaScript() {
        return "local c" +
            "
c = redis.call('get',KEYS[1])" +
            "
if c and tonumber(c) >tonumber(ARGV[1])then"+
"
returnc;"+
"
end"+
"
c=redis.call('incr',KEYS[1])"+
"
iftonumber(c)==1then"+
"
redis.call('expire',KEYS[1],ARGV[2])"+
"
end"+
"
returnc;";
}

}

获取IP地址的功能我写了一个工具类IPUtil.java,代码如下:

packagecom.example.loginlimit.util;

importjavax.servlet.http.HttpServletRequest;

publicclassIPUtil{

privatestaticfinalStringUNKNOWN="unknown";

protectedIPUtil(){

}

/**
*获取IP地址
*使用Nginx等反向代理软件,则不能通过request.getRemoteAddr()获取IP地址
*如果使用了多级反向代理的话,X-Forwarded-For的值并不止一个,而是一串IP地址,
*X-Forwarded-For中第一个非unknown的有效IP字符串,则为真实IP地址
*/
publicstaticStringgetIpAddr(HttpServletRequestrequest){
Stringip=request.getHeader("x-forwarded-for");
if(ip==null||ip.length()==0||UNKNOWN.equalsIgnoreCase(ip)){
ip=request.getHeader("Proxy-Client-IP");
}
if(ip==null||ip.length()==0||UNKNOWN.equalsIgnoreCase(ip)){
ip=request.getHeader("WL-Proxy-Client-IP");
}
if(ip==null||ip.length()==0||UNKNOWN.equalsIgnoreCase(ip)){
ip=request.getRemoteAddr();
}
return"0000:1".equals(ip)?"127.0.0.1":ip;
}

}

另外就是Lua限流脚本的说明,脚本代码如下:

privateStringbuildLuaScript(){
return"localc"+
"
c=redis.call('get',KEYS[1])"+
"
ifcandtonumber(c)>tonumber(ARGV[1])then"+
"
returnc;"+
"
end"+
"
c=redis.call('incr',KEYS[1])"+
"
iftonumber(c)==1then"+
"
redis.call('expire',KEYS[1],ARGV[2])"+
"
end"+
"
returnc;";
}

这段脚本有一个判断, tonumber(c) > tonumber(ARGV[1])这行表示如果当前key 的值大于了limitCount,直接返回;否则调用incr方法进行累加1,且调用expire方法设置过期时间。

最后就是RedisConfig.java,代码如下:

packagecom.example.loginlimit.config;

importjava.io.IOException;
importjava.io.Serializable;
importjava.time.Duration;
importjava.util.Arrays;

importcom.fasterxml.jackson.core.JsonProcessingException;
importcom.fasterxml.jackson.databind.ObjectMapper;
importorg.apache.commons.lang3.StringUtils;
importorg.springframework.beans.factory.annotation.Value;
importorg.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
importorg.springframework.cache.CacheManager;
importorg.springframework.cache.annotation.CachingConfigurerSupport;
importorg.springframework.cache.interceptor.KeyGenerator;
importorg.springframework.context.annotation.Bean;
importorg.springframework.context.annotation.Configuration;
importorg.springframework.data.redis.cache.RedisCacheManager;
importorg.springframework.data.redis.connection.RedisConnectionFactory;
importorg.springframework.data.redis.connection.RedisPassword;
importorg.springframework.data.redis.connection.RedisStandaloneConfiguration;
importorg.springframework.data.redis.connection.jedis.JedisClientConfiguration;
importorg.springframework.data.redis.connection.jedis.JedisConnectionFactory;
importorg.springframework.data.redis.core.RedisTemplate;
importorg.springframework.data.redis.core.StringRedisTemplate;
importorg.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
importorg.springframework.data.redis.serializer.RedisSerializer;
importorg.springframework.data.redis.serializer.SerializationException;
importorg.springframework.data.redis.serializer.StringRedisSerializer;
importredis.clients.jedis.JedisPool;
importredis.clients.jedis.JedisPoolConfig;

@Configuration
publicclassRedisConfigextendsCachingConfigurerSupport{

@Value("${spring.redis.host}")
privateStringhost;

@Value("${spring.redis.port}")
privateintport;

@Value("${spring.redis.password}")
privateStringpassword;

@Value("${spring.redis.timeout}")
privateinttimeout;

@Value("${spring.redis.jedis.pool.max-idle}")
privateintmaxIdle;

@Value("${spring.redis.jedis.pool.max-wait}")
privatelongmaxWaitMillis;

@Value("${spring.redis.database:0}")
privateintdatabase;

@Bean
publicJedisPoolredisPoolFactory(){
JedisPoolConfigjedisPoolConfig=newJedisPoolConfig();
jedisPoolConfig.setMaxIdle(maxIdle);
jedisPoolConfig.setMaxWaitMillis(maxWaitMillis);
if(StringUtils.isNotBlank(password)){
returnnewJedisPool(jedisPoolConfig,host,port,timeout,password,database);
}else{
returnnewJedisPool(jedisPoolConfig,host,port,timeout,null,database);
}
}

@Bean
JedisConnectionFactoryjedisConnectionFactory(){
RedisStandaloneConfigurationredisStandaloneConfiguration=newRedisStandaloneConfiguration();
redisStandaloneConfiguration.setHostName(host);
redisStandaloneConfiguration.setPort(port);
redisStandaloneConfiguration.setPassword(RedisPassword.of(password));
redisStandaloneConfiguration.setDatabase(database);

JedisClientConfiguration.JedisClientConfigurationBuilderjedisClientConfiguration=JedisClientConfiguration
.builder();
jedisClientConfiguration.connectTimeout(Duration.ofMillis(timeout));
jedisClientConfiguration.usePooling();
returnnewJedisConnectionFactory(redisStandaloneConfiguration,jedisClientConfiguration.build());
}

@Bean(name="redisTemplate")
@SuppressWarnings({"rawtypes"})
@ConditionalOnMissingBean(name="redisTemplate")
publicRedisTemplateredisTemplate(RedisConnectionFactoryredisConnectionFactory){
RedisTemplatetemplate=newRedisTemplate<>();
//使用fastjson序列化
JacksonRedisSerializerjacksonRedisSerializer=newJacksonRedisSerializer<>(Object.class);
//value值的序列化采用fastJsonRedisSerializer
template.setValueSerializer(jacksonRedisSerializer);
template.setHashValueSerializer(jacksonRedisSerializer);
//key的序列化采用StringRedisSerializer
template.setKeySerializer(newStringRedisSerializer());
template.setHashKeySerializer(newStringRedisSerializer());

template.setConnectionFactory(redisConnectionFactory);
returntemplate;
}

//缓存管理器
@Bean
publicCacheManagercacheManager(RedisConnectionFactoryredisConnectionFactory){
RedisCacheManager.RedisCacheManagerBuilderbuilder=RedisCacheManager.RedisCacheManagerBuilder
.fromConnectionFactory(redisConnectionFactory);
returnbuilder.build();
}

@Bean
@ConditionalOnMissingBean(StringRedisTemplate.class)
publicStringRedisTemplatestringRedisTemplate(RedisConnectionFactoryredisConnectionFactory){
StringRedisTemplatetemplate=newStringRedisTemplate();
template.setConnectionFactory(redisConnectionFactory);
returntemplate;
}

@Bean
publicKeyGeneratorwiselyKeyGenerator(){
return(target,method,params)->{
StringBuildersb=newStringBuilder();
sb.append(target.getClass().getName());
sb.append(method.getName());
Arrays.stream(params).map(Object::append);
returnsb.toString();
};
}

@Bean
publicRedisTemplatelimitRedisTemplate(RedisConnectionFactoryredisConnectionFactory){
RedisTemplatetemplate=newRedisTemplate<>();
template.setKeySerializer(newStringRedisSerializer());
template.setValueSerializer(newGenericJackson2JsonRedisSerializer());
template.setConnectionFactory(redisConnectionFactory);
returntemplate;
}
}

classJacksonRedisSerializerimplementsRedisSerializer{
privateClassclazz;
privateObjectMappermapper;

JacksonRedisSerializer(Classclazz){
super();
this.clazz=clazz;
this.mapper=newObjectMapper();
mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
}

@Override
publicbyte[]serialize(Tt)throwsSerializationException{
try{
returnmapper.writeValueAsBytes(t);
}catch(JsonProcessingExceptione){
e.printStackTrace();
returnnull;
}
}

@Override
publicTdeserialize(byte[]bytes)throwsSerializationException{
if(bytes.length<= 0) {
            return null;
        }
        try {
            return mapper.readValue(bytes, clazz);
        } catch (IOException e) {
            e.printStackTrace();
            return null;
        }
    }
}

LoginController.java

packagecom.example.loginlimit.controller;

importjavax.servlet.http.HttpServletRequest;

importcom.example.loginlimit.annotation.LimitCount;
importlombok.extern.slf4j.Slf4j;
importorg.apache.commons.lang3.StringUtils;
importorg.springframework.web.bind.annotation.GetMapping;
importorg.springframework.web.bind.annotation.RequestParam;
importorg.springframework.web.bind.annotation.RestController;

@Slf4j
@RestController
publicclassLoginController{

@GetMapping("/login")
@LimitCount(key="login",name="登录接口",prefix="limit")
publicStringlogin(
@RequestParam(required=true)Stringusername,
@RequestParam(required=true)Stringpassword,HttpServletRequestrequest)throwsException{
if(StringUtils.equals("张三",username)&&StringUtils.equals("123456",password)){
return"登录成功";
}
return"账户名或密码错误";
}

}

LoginLimitApplication.java

packagecom.example.loginlimit;

importorg.springframework.boot.SpringApplication;
importorg.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
publicclassLoginLimitApplication{

publicstaticvoidmain(String[]args){
SpringApplication.run(LoginLimitApplication.class,args);
}

}

4 演示一下效果

ea62eb28-8b73-11ee-939d-92fbcf53809c.png

上面这套限流的逻辑感觉用在小型或中型的项目上应该问题不大,不过目前的登录很少有直接锁定账号不能输入的,一般都是弹出一个验证码框,让你输入验证码再提交。我觉得用我这套逻辑改改应该不成问题,核心还是接口尝试次数的限制嘛!

审核编辑:黄飞

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • ip地址
    +关注

    关注

    0

    文章

    248

    浏览量

    16852
  • Redis
    +关注

    关注

    0

    文章

    367

    浏览量

    10730
  • ChatGPT
    +关注

    关注

    28

    文章

    1505

    浏览量

    6135
  • SpringBoot
    +关注

    关注

    0

    文章

    172

    浏览量

    131

原文标题:三次输错密码后,系统是怎么做到不让我继续尝试的?

文章出处:【微信号:芋道源码,微信公众号:芋道源码】欢迎添加关注!文章转载请注明出处。

收藏 人收藏

    评论

    相关推荐

    自动布线换了N 还是那么多错误 怎么办

    `RT 自动布线原件位置换了N 还是那么多错误怎么办啊`
    发表于 01-02 20:46

    我这个设计怎么添加让密码输入三次错误失效的程序

    怎么把各模块独立出来,变为子程序。密码怎么写三次输入错误失效的程序?
    发表于 06-18 12:07

    三次握手,四挥手你懂吗

    程序员面试被问到“三次握手,四挥手”怎么办
    发表于 04-08 07:23

    手机密码锁忘了怎么办?教你解手机密码

    手机密码锁忘了怎么办? 解话机锁:*2767*2878#/*2767*7377#   星码片复位:*2767*3855# 也可用于解机锁或卡锁   星显温度、电池容量:*#022
    发表于 02-25 09:49 74.4w次阅读

    电脑密码忘记了怎么办_win7系统电脑忘记密码的处理方法

    现在使用电脑的人越来越多,而各种各样的密码也越来越多,很多人在刚刚设置了电脑密码后一下就忘记了。电脑密码忘记了怎么办?并且现在使用WIN7系统的人也越来越多,WIN7电脑密
    发表于 10-23 17:00 77.5w次阅读

    TCP三次握手的过程描述

    本文档主要描述TCP三次握手的过程,一个完整的三次握手也就是 请求---应答---再次确认
    发表于 03-02 15:37 8次下载

    苹果iphone7plus的下载密码忘了怎么办

    忘记手机解锁密码是用户经常忘记的事情,有很多人都不得不去刷机解锁重新设置。前几天有个人问小编说iPhone7Plus的解锁密码忘记了怎么办?今天小编就来说说苹果iPhone解锁密码那些
    发表于 10-13 16:40 8304次阅读

    若忘记了Linux系统的root密码,该怎么办

    很多朋友经常会忘记Linux系统的root密码,linux系统忘记root密码的情况该怎么办呢?
    的头像 发表于 10-15 16:49 1.2w次阅读

    苹果iPad忘记了Apple ID密码怎么办

    Apple ID密码忘了怎么办?苹果用户最常见的一个问题是忘记了Apple ID密码,这样就没办法从App Store下载应用了,那么如何才能将密码找回来呢?一般在苹果
    的头像 发表于 09-11 12:10 8.9w次阅读
    苹果iPad忘记了Apple ID<b class='flag-5'>密码</b>该<b class='flag-5'>怎么办</b>?

    三次谐波是什么,三次谐波会造成哪些影响

    每次平台文章过后看见很多朋友问到底什么是三次谐波跟电工有什么关系?会产生怎样的影响?如何治理或防止三次谐波的滋生?别急今天就出一期有关三次谐波的全面讲解! ①三次谐波是什么? 答:
    发表于 11-16 15:44 2.8w次阅读
    <b class='flag-5'>三次</b>谐波是什么,<b class='flag-5'>三次</b>谐波会造成哪些影响

    deepin密码忘了怎么办_deepin分区方案

    为了安全考虑,在安装 Deepin 系统的时候一定会要用户设置好密码,你只有知道电脑的登录密码后,才能够登录进行一系列的操作,可是万一忘记了登录密码怎么办呢?这里就教大家一招重设 D
    发表于 11-11 14:31 1w次阅读

    windows不能更改密码怎么办

     很多电脑用户为了加强电脑安全或保护个人隐私会给电脑设置开机密码,只有正确输入密码后才能进入桌面操作。但是有些用户在设置密码时却遇到了一些问题,例如系统弹出Windows不能更改
    的头像 发表于 12-23 15:24 1w次阅读

    MySQL密码忘记了怎么办?MySQL密码快速重置方法步骤命令示例!

    MySQL密码忘记了怎么办?MySQL密码快速重置方法步骤命令示例! MySQL是一种常用的关系型数据库管理系统,如果你忘记了MySQL的密码,不必担心,可以通过一些简单的步骤来快速重
    的头像 发表于 01-12 16:06 571次阅读

    说说TCP三次握手的过程?为什么是三次而不是两、四

    说说TCP三次握手的过程?为什么是三次而不是两、四? TCP三次握手是建立TCP连接的过程,确保数据的可靠传输。它是由发送端和接收端完成
    的头像 发表于 02-04 11:03 370次阅读

    谐波和三次谐波区别 二谐波危害没有三次谐波大?

    谐波和三次谐波区别 二谐波危害没有三次谐波大? 在现代电力系统中,谐波问题逐渐引起人们的关注。谐波是指频率是基波频率的倍数的电流或电压成分。二
    的头像 发表于 04-08 17:11 2329次阅读