From ff8206323dc1b88c9373a3ee5cf5cb8e86ef60d0 Mon Sep 17 00:00:00 2001
From: RuoYi <yzz_ivy@163.com>
Date: 星期六, 30 七月 2022 14:00:32 +0800
Subject: [PATCH] 支持配置密码最大错误次数/锁定时间

---
 src/main/resources/i18n/messages.properties                                              |    2 
 src/main/java/com/ruoyi/project/monitor/controller/CacheController.java                  |    1 
 src/main/java/com/ruoyi/framework/redis/RedisCache.java                                  |   22 +++++
 src/main/java/com/ruoyi/common/constant/CacheConstants.java                              |    5 +
 src/main/java/com/ruoyi/framework/security/service/UserDetailsServiceImpl.java           |    5 +
 src/main/java/com/ruoyi/framework/web/domain/AjaxResult.java                             |   21 +++++
 src/main/java/com/ruoyi/framework/security/service/SysLoginService.java                  |    6 +
 src/main/java/com/ruoyi/framework/security/service/SysPasswordService.java               |   94 +++++++++++++++++++++++
 src/main/java/com/ruoyi/framework/security/context/AuthenticationContextHolder.java      |   28 +++++++
 src/main/java/com/ruoyi/common/exception/user/UserPasswordRetryLimitExceedException.java |   16 ++++
 src/main/resources/application.yml                                                       |    8 ++
 11 files changed, 205 insertions(+), 3 deletions(-)

diff --git a/src/main/java/com/ruoyi/common/constant/CacheConstants.java b/src/main/java/com/ruoyi/common/constant/CacheConstants.java
index 7ea15aa..c89692c 100644
--- a/src/main/java/com/ruoyi/common/constant/CacheConstants.java
+++ b/src/main/java/com/ruoyi/common/constant/CacheConstants.java
@@ -36,4 +36,9 @@
      * 闄愭祦 redis key
      */
     public static final String RATE_LIMIT_KEY = "rate_limit:";
+
+    /**
+     * 鐧诲綍璐︽埛瀵嗙爜閿欒娆℃暟 redis key
+     */
+    public static final String PWD_ERR_CNT_KEY = "pwd_err_cnt:";
 }
diff --git a/src/main/java/com/ruoyi/common/exception/user/UserPasswordRetryLimitExceedException.java b/src/main/java/com/ruoyi/common/exception/user/UserPasswordRetryLimitExceedException.java
new file mode 100644
index 0000000..0de8d24
--- /dev/null
+++ b/src/main/java/com/ruoyi/common/exception/user/UserPasswordRetryLimitExceedException.java
@@ -0,0 +1,16 @@
+package com.ruoyi.common.exception.user;
+
+/**
+ * 鐢ㄦ埛閿欒鏈�澶ф鏁板紓甯哥被
+ * 
+ * @author ruoyi
+ */
+public class UserPasswordRetryLimitExceedException extends UserException
+{
+    private static final long serialVersionUID = 1L;
+
+    public UserPasswordRetryLimitExceedException(int retryLimitCount, int lockTime)
+    {
+        super("user.password.retry.limit.exceed", new Object[] { retryLimitCount, lockTime });
+    }
+}
diff --git a/src/main/java/com/ruoyi/framework/redis/RedisCache.java b/src/main/java/com/ruoyi/framework/redis/RedisCache.java
index e701bcc..1e674db 100644
--- a/src/main/java/com/ruoyi/framework/redis/RedisCache.java
+++ b/src/main/java/com/ruoyi/framework/redis/RedisCache.java
@@ -62,6 +62,28 @@
     }
 
     /**
+     * 鑾峰彇鏈夋晥鏃堕棿
+     *
+     * @param key Redis閿�
+     * @return 鏈夋晥鏃堕棿
+     */
+    public long getExpire(final String key)
+    {
+        return redisTemplate.getExpire(key);
+    }
+
+    /**
+     * 鍒ゆ柇 key鏄惁瀛樺湪
+     *
+     * @param key 閿�
+     * @return true 瀛樺湪 false涓嶅瓨鍦�
+     */
+    public Boolean hasKey(String key)
+    {
+        return redisTemplate.hasKey(key);
+    }
+
+    /**
      * 璁剧疆鏈夋晥鏃堕棿
      *
      * @param key Redis閿�
diff --git a/src/main/java/com/ruoyi/framework/security/context/AuthenticationContextHolder.java b/src/main/java/com/ruoyi/framework/security/context/AuthenticationContextHolder.java
new file mode 100644
index 0000000..5ee5bbd
--- /dev/null
+++ b/src/main/java/com/ruoyi/framework/security/context/AuthenticationContextHolder.java
@@ -0,0 +1,28 @@
+package com.ruoyi.framework.security.context;
+
+import org.springframework.security.core.Authentication;
+
+/**
+ * 韬唤楠岃瘉淇℃伅
+ * 
+ * @author ruoyi
+ */
+public class AuthenticationContextHolder
+{
+    private static final ThreadLocal<Authentication> contextHolder = new ThreadLocal<>();
+
+    public static Authentication getContext()
+    {
+        return contextHolder.get();
+    }
+
+    public static void setContext(Authentication context)
+    {
+        contextHolder.set(context);
+    }
+
+    public static void clearContext()
+    {
+        contextHolder.remove();
+    }
+}
diff --git a/src/main/java/com/ruoyi/framework/security/service/SysLoginService.java b/src/main/java/com/ruoyi/framework/security/service/SysLoginService.java
index f7c195c..3261d3b 100644
--- a/src/main/java/com/ruoyi/framework/security/service/SysLoginService.java
+++ b/src/main/java/com/ruoyi/framework/security/service/SysLoginService.java
@@ -22,6 +22,7 @@
 import com.ruoyi.framework.manager.factory.AsyncFactory;
 import com.ruoyi.framework.redis.RedisCache;
 import com.ruoyi.framework.security.LoginUser;
+import com.ruoyi.framework.security.context.AuthenticationContextHolder;
 import com.ruoyi.project.system.domain.SysUser;
 import com.ruoyi.project.system.service.ISysConfigService;
 import com.ruoyi.project.system.service.ISysUserService;
@@ -70,9 +71,10 @@
         Authentication authentication = null;
         try
         {
+            UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password);
+            AuthenticationContextHolder.setContext(authenticationToken);
             // 璇ユ柟娉曚細鍘昏皟鐢║serDetailsServiceImpl.loadUserByUsername
-            authentication = authenticationManager
-                    .authenticate(new UsernamePasswordAuthenticationToken(username, password));
+            authentication = authenticationManager.authenticate(authenticationToken);
         }
         catch (Exception e)
         {
diff --git a/src/main/java/com/ruoyi/framework/security/service/SysPasswordService.java b/src/main/java/com/ruoyi/framework/security/service/SysPasswordService.java
new file mode 100644
index 0000000..c50a073
--- /dev/null
+++ b/src/main/java/com/ruoyi/framework/security/service/SysPasswordService.java
@@ -0,0 +1,94 @@
+package com.ruoyi.framework.security.service;
+
+import java.util.concurrent.TimeUnit;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.security.core.Authentication;
+import org.springframework.stereotype.Component;
+import com.ruoyi.common.constant.CacheConstants;
+import com.ruoyi.common.constant.Constants;
+import com.ruoyi.common.exception.user.UserPasswordNotMatchException;
+import com.ruoyi.common.exception.user.UserPasswordRetryLimitExceedException;
+import com.ruoyi.common.utils.MessageUtils;
+import com.ruoyi.common.utils.SecurityUtils;
+import com.ruoyi.framework.manager.AsyncManager;
+import com.ruoyi.framework.manager.factory.AsyncFactory;
+import com.ruoyi.framework.redis.RedisCache;
+import com.ruoyi.framework.security.context.AuthenticationContextHolder;
+import com.ruoyi.project.system.domain.SysUser;
+
+/**
+ * 鐧诲綍瀵嗙爜鏂规硶
+ * 
+ * @author ruoyi
+ */
+@Component
+public class SysPasswordService
+{
+    @Autowired
+    private RedisCache redisCache;
+
+    @Value(value = "${user.password.maxRetryCount}")
+    private int maxRetryCount;
+
+    @Value(value = "${user.password.lockTime}")
+    private int lockTime;
+
+    /**
+     * 鐧诲綍璐︽埛瀵嗙爜閿欒娆℃暟缂撳瓨閿悕
+     * 
+     * @param username 鐢ㄦ埛鍚�
+     * @return 缂撳瓨閿甼ey
+     */
+    private String getCacheKey(String username)
+    {
+        return CacheConstants.PWD_ERR_CNT_KEY + username;
+    }
+
+    public void validate(SysUser user)
+    {
+        Authentication usernamePasswordAuthenticationToken = AuthenticationContextHolder.getContext();
+        String username = usernamePasswordAuthenticationToken.getName();
+        String password = usernamePasswordAuthenticationToken.getCredentials().toString();
+
+        Integer retryCount = redisCache.getCacheObject(getCacheKey(username));
+
+        if (retryCount == null)
+        {
+            retryCount = 0;
+        }
+
+        if (retryCount >= Integer.valueOf(maxRetryCount).intValue())
+        {
+            AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL,
+                    MessageUtils.message("user.password.retry.limit.exceed", maxRetryCount, lockTime)));
+            throw new UserPasswordRetryLimitExceedException(maxRetryCount, lockTime);
+        }
+
+        if (!matches(user, password))
+        {
+            retryCount = retryCount + 1;
+            AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL,
+                    MessageUtils.message("user.password.retry.limit.count", retryCount)));
+            redisCache.setCacheObject(getCacheKey(username), retryCount, lockTime, TimeUnit.MINUTES);
+            throw new UserPasswordNotMatchException();
+        }
+        else
+        {
+            clearLoginRecordCache(username);
+        }
+    }
+
+    public boolean matches(SysUser user, String rawPassword)
+    {
+        return SecurityUtils.matchesPassword(rawPassword, user.getPassword());
+    }
+
+    public void clearLoginRecordCache(String loginName)
+    {
+        if (redisCache.hasKey(getCacheKey(loginName)))
+        {
+            redisCache.deleteObject(getCacheKey(loginName));
+        }
+    }
+}
diff --git a/src/main/java/com/ruoyi/framework/security/service/UserDetailsServiceImpl.java b/src/main/java/com/ruoyi/framework/security/service/UserDetailsServiceImpl.java
index 861e3ec..d48c4c9 100644
--- a/src/main/java/com/ruoyi/framework/security/service/UserDetailsServiceImpl.java
+++ b/src/main/java/com/ruoyi/framework/security/service/UserDetailsServiceImpl.java
@@ -25,6 +25,9 @@
 
     @Autowired
     private ISysUserService userService;
+    
+    @Autowired
+    private SysPasswordService passwordService;
 
     @Autowired
     private SysPermissionService permissionService;
@@ -49,6 +52,8 @@
             throw new ServiceException("瀵逛笉璧凤紝鎮ㄧ殑璐﹀彿锛�" + username + " 宸插仠鐢�");
         }
 
+        passwordService.validate(user);
+
         return createLoginUser(user);
     }
 
diff --git a/src/main/java/com/ruoyi/framework/web/domain/AjaxResult.java b/src/main/java/com/ruoyi/framework/web/domain/AjaxResult.java
index 51253c6..cba99ba 100644
--- a/src/main/java/com/ruoyi/framework/web/domain/AjaxResult.java
+++ b/src/main/java/com/ruoyi/framework/web/domain/AjaxResult.java
@@ -1,6 +1,7 @@
 package com.ruoyi.framework.web.domain;
 
 import java.util.HashMap;
+import java.util.Objects;
 import com.ruoyi.common.constant.HttpStatus;
 import com.ruoyi.common.utils.StringUtils;
 
@@ -147,6 +148,26 @@
     }
 
     /**
+     * 鏄惁涓烘垚鍔熸秷鎭�
+     *
+     * @return 缁撴灉
+     */
+    public boolean isSuccess()
+    {
+        return !isError();
+    }
+
+    /**
+     * 鏄惁涓洪敊璇秷鎭�
+     *
+     * @return 缁撴灉
+     */
+    public boolean isError()
+    {
+        return Objects.equals(HttpStatus.ERROR, this.get(CODE_TAG));
+    }
+
+    /**
      * 鏂逛究閾惧紡璋冪敤
      *
      * @param key 閿�
diff --git a/src/main/java/com/ruoyi/project/monitor/controller/CacheController.java b/src/main/java/com/ruoyi/project/monitor/controller/CacheController.java
index 37bf080..8dce0e6 100644
--- a/src/main/java/com/ruoyi/project/monitor/controller/CacheController.java
+++ b/src/main/java/com/ruoyi/project/monitor/controller/CacheController.java
@@ -41,6 +41,7 @@
         caches.add(new SysCache(CacheConstants.CAPTCHA_CODE_KEY, "楠岃瘉鐮�"));
         caches.add(new SysCache(CacheConstants.REPEAT_SUBMIT_KEY, "闃查噸鎻愪氦"));
         caches.add(new SysCache(CacheConstants.RATE_LIMIT_KEY, "闄愭祦澶勭悊"));
+        caches.add(new SysCache(CacheConstants.PWD_ERR_CNT_KEY, "瀵嗙爜閿欒娆℃暟"));
     }
 
     @PreAuthorize("@ss.hasPermi('monitor:cache:list')")
diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml
index 6d00acb..214c5f2 100644
--- a/src/main/resources/application.yml
+++ b/src/main/resources/application.yml
@@ -39,6 +39,14 @@
     com.ruoyi: debug
     org.springframework: warn
 
+# 鐢ㄦ埛閰嶇疆
+user:
+  password:
+    # 瀵嗙爜鏈�澶ч敊璇鏁�
+    maxRetryCount: 5
+    # 瀵嗙爜閿佸畾鏃堕棿锛堥粯璁�10鍒嗛挓锛�
+    lockTime: 10
+
 # Spring閰嶇疆
 spring:
   # 璧勬簮淇℃伅
diff --git a/src/main/resources/i18n/messages.properties b/src/main/resources/i18n/messages.properties
index 5a41f8c..b7433dc 100644
--- a/src/main/resources/i18n/messages.properties
+++ b/src/main/resources/i18n/messages.properties
@@ -5,7 +5,7 @@
 user.not.exists=鐢ㄦ埛涓嶅瓨鍦�/瀵嗙爜閿欒
 user.password.not.match=鐢ㄦ埛涓嶅瓨鍦�/瀵嗙爜閿欒
 user.password.retry.limit.count=瀵嗙爜杈撳叆閿欒{0}娆�
-user.password.retry.limit.exceed=瀵嗙爜杈撳叆閿欒{0}娆★紝甯愭埛閿佸畾10鍒嗛挓
+user.password.retry.limit.exceed=瀵嗙爜杈撳叆閿欒{0}娆★紝甯愭埛閿佸畾{1}鍒嗛挓
 user.password.delete=瀵逛笉璧凤紝鎮ㄧ殑璐﹀彿宸茶鍒犻櫎
 user.blocked=鐢ㄦ埛宸插皝绂侊紝璇疯仈绯荤鐞嗗憳
 role.blocked=瑙掕壊宸插皝绂侊紝璇疯仈绯荤鐞嗗憳

--
Gitblit v1.9.3