Spring Security中如何设置账户在多次登录失败后自动锁定?

2026-04-11 04:492阅读0评论SEO基础
  • 内容介绍
  • 文章标签
  • 相关推荐

本文共计1875个文字,预计阅读时间需要8分钟。

Spring Security中如何设置账户在多次登录失败后自动锁定?

原文链接:https://cloud.tencent.com/developer/article/1914275

需求:在现实开发过程中,我们经常会遇到这样一个需求:当用户多次登录失败时,账户被锁定。账户被锁定后,立即使用正确的用户名和密码也无法登录。

原文链接:cloud.tencent.com/developer/article/1914275

需求

在实际的开发过程中,我们通常会有这样的一个需求:

  • 当用户多次登录失败的时候,账户被锁定
  • 账户被锁定之后,即使使用正确的用户名密码登录,也不可以(防止你的密码已经被盗)
  • 被锁定的账户应该由管理员解锁,或者等待一定的时间周期之后才能再次进行登录操作。

知识回顾
  • Spring Security 不需要我们自己实现登录验证逻辑,而是将用户、角色、权限信息以实现UserDetails和UserDetailsService接口的方式告知Spring Security。具体的登录验证逻辑Spring Security 会帮助我们实现。
  • UserDetails接口实现中有一个方法叫做isAccountNonLocked()用于判断账号是否被锁定,也就是说我们应该通过该方法对应的set方法setAccountNonLocked(false)告知Spring Security该登录账户被锁定。
  • 那么应该在哪里判断账号登录失败的次数并执行锁定机制呢?当然是自定义登录成功及失败结果处理的AuthenticationFailureHandler。

MyUserDetails修改

之前定义过一个用于接收用户认证鉴权信息的实体MyUserDetails。当时没有使用到accountNonLocked字段,所以它的get方法也是直接返回true,表示该账户没有被锁定。

现在我们需要这个字段,Spring Security会根据该字段的值判断账户是否未被锁定,如果该字段的值为0(false),Spring Security会抛出LockedException,禁止用户登录。所以我们去sys_user表添加一个accountNonLocked字段,默认值是1(true),表示未被锁定。

需要注意的是mysql并没有boolean类型,int或tinyint类型,1就是true,0就是false。

同时修改MyUserDetails里面的isAccountNonLocked,让它返回值是从数据库加载的accountNonLocked字段数据。

public class MyUserDetails implements UserDetails { boolean accountNonLocked; //是否没被锁定 ......//这里省略其他属性,省略其他get、set方法 @Override public boolean isAccountNonLocked() { //return true; //原来是这样的 return this.accountNonLocked; //现在改成这个样子 } }


实现多次登录失败锁定的原理

一般来说实现这个需求,我们需要针对每一个用户记录登录失败的次数nLock和锁定账户的到期时间releaseTime。具体你是把这2个信息存储在mysql、还是文件中、还是redis中等等,完全取决于你对你所处的应用架构适用性的判断。具体的实现逻辑无非就是:

  • 登陆失败之后,从存储中将nLock取出来加1。
  • 如果nLock大于登陆失败阈值(比如3次),则将nLock=0,然后设置releaseTime为当前时间加上锁定周期。通过setAccountNonLocked(false)告知Spring Security该登录账户被锁定。
  • 如果nLock小于等于1,则将nLock再次存起来。
  • 在一个合适的时机,将锁定状态重置为setAccountNonLocked(true)。

这是一种非常典型的实现方式,笔者向大家介绍一款非常有用的开源软件叫做:ratelimitj。这个软件的功能主要是为API访问进行限流,也就是说可以通过制定规则限制API接口的访问频率。那恰好登录验证接口也是API的一种啊,我们正好也需要限制它在一定的时间内的访问次数。


具体实现

首先需要将ratelimitj通过maven坐标引入到我们的应用里面来。我们使用的是内存存储的版本,还有redis存储的版本,大家可以根据自己的应用情况选用。

<dependency> <groupId>es.moki.ratelimitj</groupId> <artifactId>ratelimitj-inmemory</artifactId> <version>0.7.0-RC1</version> </dependency>

之后通过继承SimpleUrlAuthenticationFailureHandler ,实现onAuthenticationFailure方法。该实现是针对登录失败的结果的处理,在我们之前的文章中已经讲过

@Component public class MyAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler { @Value("${spring.security.logintype}") private String loginType; private static ObjectMapper objectMapper = new ObjectMapper(); //引入MyUserDetailsServiceMapper @Resource MyUserDetailsServiceMapper myUserDetailsServiceMapper; //规则定义:1小时之内5次机会,第6次失败就触发限流行为(禁止访问) Set<RequestLimitRule> rules = Collections.singleton(RequestLimitRule.of(Duration.ofMinutes(60),5)); RequestRateLimiter limiter = new InMemorySlidingWindowRequestRateLimiter(rules); @Override public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException { //从request或request.getSession中获取登录用户名 String userId = request.getParameter("uname"); //默认提示信息 String errorMsg; if(exception instanceof LockedException){ //账户被锁定了 errorMsg = "您已经多次登陆失败,账户已被锁定,请稍后再试!"; }else if(exception instanceof SessionAuthenticationException){ errorMsg = exception.getMessage(); }else{ errorMsg = "请检查您的用户名和密码输入是否正确"; } //每次登陆失败计数器加1,并判断该用户是否已经到了触发了锁定规则 boolean reachLimit = limiter.overLimitWhenIncremented(userId); if(reachLimit){ //如果触发了锁定规则,修改数据库 accountNonLocked字段锁定用户 myUserDetailsServiceMapper.updateLockedByUserId(userId); errorMsg = "您多次登陆失败,账户已被锁定,请稍后再试!"; } if ("JSON".equalsIgnoreCase(loginType)) { response.setContentType("application/json;charset=UTF-8"); response.getWriter().write(objectMapper.writeValueAsString( AjaxResponse.userInputError(errorMsg) )); } else { response.setContentType("text/html;charset=UTF-8"); super.onAuthenticationFailure(request, response, exception); } } }

  • 核心实现注意看代码中的注释,userId就是登陆用户名,你可以在request中取出来。
  • 代码中的MyUserDetails为UserDetails的实现类
  • 在MyUserDetailsServiceMapper新增updateLockedByUserId(user);是用来更新数据库accountNonLocked字段的(更新为锁定状态),代码如下:

@Update({"UPDATE sys_user u \n" + " SET u.accountNonLocked = 0 \n" + " WHERE u.username = #{userId}" }) int updateLockedByUserId(@Param("userId") String userId);

该字段被更新为0之后,用户下次登录会从sys_user加载UserDetails数据。所以MyUserDetailsServiceMapper查询SQL增加字段

当Spring Security发现accountNonLocked=0的时候,就会抛出LockedException(即使输入正确的用户名密码也不行,因为这个账户已经被锁定了)。从而登陆失败再次进入AuthenticationFailureHandler ,我们将LockedException转换为提示信息:“您已经多次登陆失败,账户已被锁定,请稍后再试!”。


重置数据库锁定状态的时机

需要注意的是,我们这种实现方式,实际上是有两个锁定状态

  • 第一个锁定状态是由ratelimitj-inmemory控制的窗口,是存储在内存中的,锁定时间不到,这个窗口是打不开的。除非你重启应用,内存被刷新。
  • 第二个锁定状态是数据库sys_user表的accountNonLocked字段,你可以人为去修改。但是第一个锁定状态不打开,你人为修改accountNonLocked字段是没用的,登录还是会被再次更新为账户锁定

所以账户解锁的2个条件缺一不可:一是到达时间窗口限制边界(或重启应用),二是accountNonLocked字段为1 。但是更重要的是如何选择重置锁定状态的时机。笔者能想到几种方案如下

Spring Security中如何设置账户在多次登录失败后自动锁定?

  • 一般的小型的管理系统,在“用户管理”功能模块提供一个功能,重置账户可用及锁定状态。
  • 下一次登陆的时候,自定义过滤器,加在Spring Boot过滤器链最前端做锁定状态重置的判断(要在数据库表里面加锁定状态的时间点字段)。
  • 写一个Spring 的定时器轮询,当然这是最差的方案(要在数据库表里面加锁定状态的时间点字段)。

本文共计1875个文字,预计阅读时间需要8分钟。

Spring Security中如何设置账户在多次登录失败后自动锁定?

原文链接:https://cloud.tencent.com/developer/article/1914275

需求:在现实开发过程中,我们经常会遇到这样一个需求:当用户多次登录失败时,账户被锁定。账户被锁定后,立即使用正确的用户名和密码也无法登录。

原文链接:cloud.tencent.com/developer/article/1914275

需求

在实际的开发过程中,我们通常会有这样的一个需求:

  • 当用户多次登录失败的时候,账户被锁定
  • 账户被锁定之后,即使使用正确的用户名密码登录,也不可以(防止你的密码已经被盗)
  • 被锁定的账户应该由管理员解锁,或者等待一定的时间周期之后才能再次进行登录操作。

知识回顾
  • Spring Security 不需要我们自己实现登录验证逻辑,而是将用户、角色、权限信息以实现UserDetails和UserDetailsService接口的方式告知Spring Security。具体的登录验证逻辑Spring Security 会帮助我们实现。
  • UserDetails接口实现中有一个方法叫做isAccountNonLocked()用于判断账号是否被锁定,也就是说我们应该通过该方法对应的set方法setAccountNonLocked(false)告知Spring Security该登录账户被锁定。
  • 那么应该在哪里判断账号登录失败的次数并执行锁定机制呢?当然是自定义登录成功及失败结果处理的AuthenticationFailureHandler。

MyUserDetails修改

之前定义过一个用于接收用户认证鉴权信息的实体MyUserDetails。当时没有使用到accountNonLocked字段,所以它的get方法也是直接返回true,表示该账户没有被锁定。

现在我们需要这个字段,Spring Security会根据该字段的值判断账户是否未被锁定,如果该字段的值为0(false),Spring Security会抛出LockedException,禁止用户登录。所以我们去sys_user表添加一个accountNonLocked字段,默认值是1(true),表示未被锁定。

需要注意的是mysql并没有boolean类型,int或tinyint类型,1就是true,0就是false。

同时修改MyUserDetails里面的isAccountNonLocked,让它返回值是从数据库加载的accountNonLocked字段数据。

public class MyUserDetails implements UserDetails { boolean accountNonLocked; //是否没被锁定 ......//这里省略其他属性,省略其他get、set方法 @Override public boolean isAccountNonLocked() { //return true; //原来是这样的 return this.accountNonLocked; //现在改成这个样子 } }


实现多次登录失败锁定的原理

一般来说实现这个需求,我们需要针对每一个用户记录登录失败的次数nLock和锁定账户的到期时间releaseTime。具体你是把这2个信息存储在mysql、还是文件中、还是redis中等等,完全取决于你对你所处的应用架构适用性的判断。具体的实现逻辑无非就是:

  • 登陆失败之后,从存储中将nLock取出来加1。
  • 如果nLock大于登陆失败阈值(比如3次),则将nLock=0,然后设置releaseTime为当前时间加上锁定周期。通过setAccountNonLocked(false)告知Spring Security该登录账户被锁定。
  • 如果nLock小于等于1,则将nLock再次存起来。
  • 在一个合适的时机,将锁定状态重置为setAccountNonLocked(true)。

这是一种非常典型的实现方式,笔者向大家介绍一款非常有用的开源软件叫做:ratelimitj。这个软件的功能主要是为API访问进行限流,也就是说可以通过制定规则限制API接口的访问频率。那恰好登录验证接口也是API的一种啊,我们正好也需要限制它在一定的时间内的访问次数。


具体实现

首先需要将ratelimitj通过maven坐标引入到我们的应用里面来。我们使用的是内存存储的版本,还有redis存储的版本,大家可以根据自己的应用情况选用。

<dependency> <groupId>es.moki.ratelimitj</groupId> <artifactId>ratelimitj-inmemory</artifactId> <version>0.7.0-RC1</version> </dependency>

之后通过继承SimpleUrlAuthenticationFailureHandler ,实现onAuthenticationFailure方法。该实现是针对登录失败的结果的处理,在我们之前的文章中已经讲过

@Component public class MyAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler { @Value("${spring.security.logintype}") private String loginType; private static ObjectMapper objectMapper = new ObjectMapper(); //引入MyUserDetailsServiceMapper @Resource MyUserDetailsServiceMapper myUserDetailsServiceMapper; //规则定义:1小时之内5次机会,第6次失败就触发限流行为(禁止访问) Set<RequestLimitRule> rules = Collections.singleton(RequestLimitRule.of(Duration.ofMinutes(60),5)); RequestRateLimiter limiter = new InMemorySlidingWindowRequestRateLimiter(rules); @Override public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException { //从request或request.getSession中获取登录用户名 String userId = request.getParameter("uname"); //默认提示信息 String errorMsg; if(exception instanceof LockedException){ //账户被锁定了 errorMsg = "您已经多次登陆失败,账户已被锁定,请稍后再试!"; }else if(exception instanceof SessionAuthenticationException){ errorMsg = exception.getMessage(); }else{ errorMsg = "请检查您的用户名和密码输入是否正确"; } //每次登陆失败计数器加1,并判断该用户是否已经到了触发了锁定规则 boolean reachLimit = limiter.overLimitWhenIncremented(userId); if(reachLimit){ //如果触发了锁定规则,修改数据库 accountNonLocked字段锁定用户 myUserDetailsServiceMapper.updateLockedByUserId(userId); errorMsg = "您多次登陆失败,账户已被锁定,请稍后再试!"; } if ("JSON".equalsIgnoreCase(loginType)) { response.setContentType("application/json;charset=UTF-8"); response.getWriter().write(objectMapper.writeValueAsString( AjaxResponse.userInputError(errorMsg) )); } else { response.setContentType("text/html;charset=UTF-8"); super.onAuthenticationFailure(request, response, exception); } } }

  • 核心实现注意看代码中的注释,userId就是登陆用户名,你可以在request中取出来。
  • 代码中的MyUserDetails为UserDetails的实现类
  • 在MyUserDetailsServiceMapper新增updateLockedByUserId(user);是用来更新数据库accountNonLocked字段的(更新为锁定状态),代码如下:

@Update({"UPDATE sys_user u \n" + " SET u.accountNonLocked = 0 \n" + " WHERE u.username = #{userId}" }) int updateLockedByUserId(@Param("userId") String userId);

该字段被更新为0之后,用户下次登录会从sys_user加载UserDetails数据。所以MyUserDetailsServiceMapper查询SQL增加字段

当Spring Security发现accountNonLocked=0的时候,就会抛出LockedException(即使输入正确的用户名密码也不行,因为这个账户已经被锁定了)。从而登陆失败再次进入AuthenticationFailureHandler ,我们将LockedException转换为提示信息:“您已经多次登陆失败,账户已被锁定,请稍后再试!”。


重置数据库锁定状态的时机

需要注意的是,我们这种实现方式,实际上是有两个锁定状态

  • 第一个锁定状态是由ratelimitj-inmemory控制的窗口,是存储在内存中的,锁定时间不到,这个窗口是打不开的。除非你重启应用,内存被刷新。
  • 第二个锁定状态是数据库sys_user表的accountNonLocked字段,你可以人为去修改。但是第一个锁定状态不打开,你人为修改accountNonLocked字段是没用的,登录还是会被再次更新为账户锁定

所以账户解锁的2个条件缺一不可:一是到达时间窗口限制边界(或重启应用),二是accountNonLocked字段为1 。但是更重要的是如何选择重置锁定状态的时机。笔者能想到几种方案如下

Spring Security中如何设置账户在多次登录失败后自动锁定?

  • 一般的小型的管理系统,在“用户管理”功能模块提供一个功能,重置账户可用及锁定状态。
  • 下一次登陆的时候,自定义过滤器,加在Spring Boot过滤器链最前端做锁定状态重置的判断(要在数据库表里面加锁定状态的时间点字段)。
  • 写一个Spring 的定时器轮询,当然这是最差的方案(要在数据库表里面加锁定状态的时间点字段)。