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

    文章

    240

    浏览量

    16744
  • Redis
    +关注

    关注

    0

    文章

    363

    浏览量

    10496
  • ChatGPT
    +关注

    关注

    27

    文章

    1411

    浏览量

    4771
  • SpringBoot
    +关注

    关注

    0

    文章

    172

    浏览量

    106

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

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

收藏 人收藏

    评论

    相关推荐

    单片机密码

    密码时显示INPUTPASSWORD;3.实现输入密码错误超过限定的三次电子密码锁定;4.4×4
    发表于 08-30 13:46

    WINDOWS用户密码忘记了,怎么办?

    WINDOWS用户密码忘记了,怎么办?开机F8 进了安全模式后用Administrator登录后在控制面板把原来的用户密码删了就可以进入DOS实模式,将%SystemRoot%\system32
    发表于 11-06 12:51

    12306忘了当初注册时设置的密码、用户名怎么办

    12306忘了当初注册时设置的密码、用户名怎么办如果忘记密码,可点击“用户登录”页面下方的“忘记用户名/密码”功能,输入您在注册时提供的邮箱
    发表于 01-06 09:21

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

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

    [Protel 99SE]上密码了,忘了怎么办

    [Protel 99SE]上密码了,忘了怎么办? 谢谢
    发表于 04-03 08:18

    基于单片机的电子密码

    ;5、三次输入开锁密码不正确,发出报警声。设计要求:1、用继电器模拟“开”、“闭”锁,继电器吸合代表“闭”锁,继电器释放代表“开”所;2、若设置密码
    发表于 05-09 21:47

    基于单片机的电子密码锁设计

    ;5、三次输入开锁密码不正确,发出报警声。设计要求:1、用继电器模拟“开”、“闭”锁,继电器吸合代表“闭”锁,继电器释放代表“开”所;2、若设置密码
    发表于 05-09 22:09

    求大神帮编写电子密码锁程序

    `求大神帮写个程序,设计6位密码锁 。数码管可以记录六按键的键值,当输入密码后需要按下确认键,并可以修改内设密码,当
    发表于 12-31 14:53

    求高手帮帮忙 做一个密码锁 非常感谢

    密码密码用“******”显示。2.密码输入正确,则灯亮,弹出对话框“密码正确,锁打开。”表示锁打开。3.若
    发表于 06-09 13:24

    急!!!求VHDL的电子密码锁设计

    要求输入正确绿灯亮,错误红灯亮,三次输入错误蜂鸣器报警,密码可修改
    发表于 05-10 15:28

    用ActiveX控件读取加密EXCEL文件,出现错误!(一个需要输入三次密码才能打开的EXCEL)

    如图片,用ActiveX控制实现读取加密EXCEL,本身运行没有问题,也可以正常读取加密的EXCEL的内容(只需要输一密码就能打开的EXCEL),但是同事给的EXCEL,手动打开需要输入3
    发表于 07-11 00:03

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

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

    三次握手,四挥手你懂吗

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

    基于单片机的智能密码锁的设计

    【设计简介:本设计是基于单片机的智能密码锁的设计,主要实现以下功能:可实现输入正确密码进行开门,如果三次输入
    发表于 11-19 07:00

    iphone密码多次输入错误被锁如何解?

    iphone密码多次输入错误被锁如何解? iPhone允许您设置一个密码,在开机或按下唤醒按钮时可以输入
    发表于 02-02 09:45 3.3w次阅读