在实际的开发过程中,我们通常会有这样的一个需求:
之前定义过一个用于接收用户认证鉴权信息的实体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中等等,完全取决于你对你所处的应用架构适用性的判断。具体的实现逻辑无非就是:
这是一种非常典型的实现方式,笔者向大家介绍一款非常有用的开源软件叫做: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);
}
}
}
@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转换为提示信息:“您已经多次登陆失败,账户已被锁定,请稍后再试!”。
需要注意的是,我们这种实现方式,实际上是有两个锁定状态
所以账户解锁的2个条件缺一不可:一是到达时间窗口限制边界(或重启应用),二是accountNonLocked字段为1 。但是更重要的是如何选择重置锁定状态的时机。笔者能想到几种方案如下
版权说明 : 本文为转载文章, 版权归原作者所有 版权申明
原文链接 : https://blog.csdn.net/m0_53157173/article/details/121426948
内容来源于网络,如有侵权,请联系作者删除!