This is an automated email from the ASF dual-hosted git repository.

dockerzhang pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/inlong.git


The following commit(s) were added to refs/heads/master by this push:
     new 48b40feeb [INLONG-6771][Manager] Add login failure limit (#6846)
48b40feeb is described below

commit 48b40feeb351cd80858139deafc50d397f42863b
Author: leosanqing <[email protected]>
AuthorDate: Tue Dec 13 17:13:40 2022 +0800

    [INLONG-6771][Manager] Add login failure limit (#6846)
    
    Co-authored-by: healchow <[email protected]>
---
 .../manager/pojo/user/UserLoginLockStatus.java     | 33 ++++++++++++
 .../inlong/manager/service/user/UserService.java   |  6 +++
 .../manager/service/user/UserServiceImpl.java      | 62 ++++++++++++++++++++++
 .../manager/web/controller/AnnoController.java     |  9 +---
 .../manager/web/controller/AnnoControllerTest.java | 59 ++++++++++++++++++++
 5 files changed, 161 insertions(+), 8 deletions(-)

diff --git 
a/inlong-manager/manager-pojo/src/main/java/org/apache/inlong/manager/pojo/user/UserLoginLockStatus.java
 
b/inlong-manager/manager-pojo/src/main/java/org/apache/inlong/manager/pojo/user/UserLoginLockStatus.java
new file mode 100644
index 000000000..2aedd6f36
--- /dev/null
+++ 
b/inlong-manager/manager-pojo/src/main/java/org/apache/inlong/manager/pojo/user/UserLoginLockStatus.java
@@ -0,0 +1,33 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.inlong.manager.pojo.user;
+
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+/**
+ * Record whether the account is locked and the
+ */
+@Data
+public class UserLoginLockStatus {
+
+    private int loginErrorCount;
+
+    private LocalDateTime lockoutTime;
+}
diff --git 
a/inlong-manager/manager-service/src/main/java/org/apache/inlong/manager/service/user/UserService.java
 
b/inlong-manager/manager-service/src/main/java/org/apache/inlong/manager/service/user/UserService.java
index a4baeb95e..9e2de4bfe 100644
--- 
a/inlong-manager/manager-service/src/main/java/org/apache/inlong/manager/service/user/UserService.java
+++ 
b/inlong-manager/manager-service/src/main/java/org/apache/inlong/manager/service/user/UserService.java
@@ -19,6 +19,7 @@ package org.apache.inlong.manager.service.user;
 
 import org.apache.inlong.manager.pojo.common.PageResult;
 import org.apache.inlong.manager.pojo.user.UserInfo;
+import org.apache.inlong.manager.pojo.user.UserLoginRequest;
 import org.apache.inlong.manager.pojo.user.UserRequest;
 
 /**
@@ -76,4 +77,9 @@ public interface UserService {
      */
     Boolean delete(Integer userId, String currentUser);
 
+    /**
+     * Account password login
+     */
+    void login(UserLoginRequest req);
+
 }
diff --git 
a/inlong-manager/manager-service/src/main/java/org/apache/inlong/manager/service/user/UserServiceImpl.java
 
b/inlong-manager/manager-service/src/main/java/org/apache/inlong/manager/service/user/UserServiceImpl.java
index 212aadae5..963a21ef5 100644
--- 
a/inlong-manager/manager-service/src/main/java/org/apache/inlong/manager/service/user/UserServiceImpl.java
+++ 
b/inlong-manager/manager-service/src/main/java/org/apache/inlong/manager/service/user/UserServiceImpl.java
@@ -35,17 +35,26 @@ import org.apache.inlong.manager.dao.entity.UserEntity;
 import org.apache.inlong.manager.dao.mapper.UserEntityMapper;
 import org.apache.inlong.manager.pojo.common.PageResult;
 import org.apache.inlong.manager.pojo.user.UserInfo;
+import org.apache.inlong.manager.pojo.user.UserLoginLockStatus;
+import org.apache.inlong.manager.pojo.user.UserLoginRequest;
 import org.apache.inlong.manager.pojo.user.UserRequest;
+import org.apache.shiro.SecurityUtils;
+import org.apache.shiro.authc.AuthenticationException;
+import org.apache.shiro.authc.UsernamePasswordToken;
+import org.apache.shiro.subject.Subject;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 
 import java.nio.charset.StandardCharsets;
+import java.time.Duration;
+import java.time.LocalDateTime;
 import java.util.Date;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
+import java.util.concurrent.ConcurrentHashMap;
 
 /**
  * User service layer implementation
@@ -57,6 +66,14 @@ public class UserServiceImpl implements UserService {
 
     private static final Integer SECRET_KEY_SIZE = 16;
 
+    /**
+     * locked time, the unit is minute
+     */
+    private static final Integer LOCKED_TIME = 3;
+    private static final Integer LOCKED_THRESHOLD = 10;
+
+    private final Map<String, UserLoginLockStatus> loginLockStatusMap = new 
ConcurrentHashMap<>();
+
     @Autowired
     private UserEntityMapper userMapper;
 
@@ -245,4 +262,49 @@ public class UserServiceImpl implements UserService {
         return true;
     }
 
+    /**
+     * This implementation is just to intercept some error requests and reduce 
the pressure on the database.
+     * <p/>
+     * This is a memory-based implementation. There is a problem with 
concurrency security when there are
+     * multiple service nodes because the data in memory cannot be shared.
+     *
+     * @param req username login request
+     */
+    @Override
+    public void login(UserLoginRequest req) {
+        String username = req.getUsername();
+        UserLoginLockStatus userLoginLockStatus = 
loginLockStatusMap.getOrDefault(username, new UserLoginLockStatus());
+        LocalDateTime lockoutTime = userLoginLockStatus.getLockoutTime();
+        if (lockoutTime != null && lockoutTime.isAfter(LocalDateTime.now())) {
+            // part of a minute counts as one minute
+            long waitMinutes = Duration.between(LocalDateTime.now(), 
lockoutTime).toMinutes() + 1;
+            throw new BusinessException("account has been locked, please try 
again in " + waitMinutes + " minutes");
+        }
+
+        Subject subject = SecurityUtils.getSubject();
+        UsernamePasswordToken token = new UsernamePasswordToken(username, 
req.getPassword());
+        try {
+            subject.login(token);
+        } catch (AuthenticationException e) {
+            LOGGER.error("login error for request {}", req, e);
+            int loginErrorCount = userLoginLockStatus.getLoginErrorCount() + 1;
+
+            if (loginErrorCount % LOCKED_THRESHOLD == 0) {
+                LocalDateTime lockedTime = 
LocalDateTime.now().plusMinutes(LOCKED_TIME);
+                userLoginLockStatus.setLockoutTime(lockedTime);
+                LOGGER.error("account {} is locked, lockout time: {}", 
username, lockedTime);
+            }
+            userLoginLockStatus.setLoginErrorCount(loginErrorCount);
+
+            loginLockStatusMap.put(username, userLoginLockStatus);
+            throw e;
+        }
+
+        LoginUserUtils.setUserLoginInfo((UserInfo) subject.getPrincipal());
+
+        // login successfully, clear error count
+        userLoginLockStatus.setLoginErrorCount(0);
+        loginLockStatusMap.put(username, userLoginLockStatus);
+    }
+
 }
diff --git 
a/inlong-manager/manager-web/src/main/java/org/apache/inlong/manager/web/controller/AnnoController.java
 
b/inlong-manager/manager-web/src/main/java/org/apache/inlong/manager/web/controller/AnnoController.java
index 8867fc5ef..cf0e8c074 100644
--- 
a/inlong-manager/manager-web/src/main/java/org/apache/inlong/manager/web/controller/AnnoController.java
+++ 
b/inlong-manager/manager-web/src/main/java/org/apache/inlong/manager/web/controller/AnnoController.java
@@ -20,14 +20,11 @@ package org.apache.inlong.manager.web.controller;
 import io.swagger.annotations.Api;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.inlong.manager.pojo.common.Response;
-import org.apache.inlong.manager.pojo.user.UserInfo;
 import org.apache.inlong.manager.pojo.user.UserLoginRequest;
 import org.apache.inlong.manager.pojo.user.UserRequest;
 import org.apache.inlong.manager.service.user.LoginUserUtils;
 import org.apache.inlong.manager.service.user.UserService;
 import org.apache.shiro.SecurityUtils;
-import org.apache.shiro.authc.UsernamePasswordToken;
-import org.apache.shiro.subject.Subject;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.validation.annotation.Validated;
 import org.springframework.web.bind.annotation.GetMapping;
@@ -50,11 +47,7 @@ public class AnnoController {
 
     @PostMapping("/anno/login")
     public Response<Boolean> login(@Validated @RequestBody UserLoginRequest 
loginRequest) {
-        Subject subject = SecurityUtils.getSubject();
-        UsernamePasswordToken token = new 
UsernamePasswordToken(loginRequest.getUsername(), loginRequest.getPassword());
-        subject.login(token);
-        LoginUserUtils.setUserLoginInfo((UserInfo) subject.getPrincipal());
-
+        userService.login(loginRequest);
         return Response.success(true);
     }
 
diff --git 
a/inlong-manager/manager-web/src/test/java/org/apache/inlong/manager/web/controller/AnnoControllerTest.java
 
b/inlong-manager/manager-web/src/test/java/org/apache/inlong/manager/web/controller/AnnoControllerTest.java
index fea16801f..7910b28d4 100644
--- 
a/inlong-manager/manager-web/src/test/java/org/apache/inlong/manager/web/controller/AnnoControllerTest.java
+++ 
b/inlong-manager/manager-web/src/test/java/org/apache/inlong/manager/web/controller/AnnoControllerTest.java
@@ -72,6 +72,65 @@ class AnnoControllerTest extends WebBaseTest {
         Assertions.assertTrue(response.getErrMsg().contains("incorrect"));
     }
 
+    @Test
+    void testLoginFailByWrongPwdAndLockAccount() throws Exception {
+        UserLoginRequest loginUser = new UserLoginRequest();
+        loginUser.setUsername("test_lock_account");
+        // Wrong pwd
+        loginUser.setPassword("test_wrong_pwd");
+
+        for (int i = 0; i < 10; i++) {
+            mockMvc.perform(
+                    post("/api/anno/login")
+                            .content(JsonUtils.toJsonString(loginUser))
+                            .contentType(MediaType.APPLICATION_JSON)
+                            .accept(MediaType.APPLICATION_JSON))
+                    .andExpect(status().isOk())
+                    .andReturn();
+        }
+
+        // account is locked
+        MvcResult mvcResult = mockMvc.perform(
+                post("/api/anno/login")
+                        .content(JsonUtils.toJsonString(loginUser))
+                        .contentType(MediaType.APPLICATION_JSON)
+                        .accept(MediaType.APPLICATION_JSON))
+                .andExpect(status().isOk())
+                .andReturn();
+        Response<String> response = getResBody(mvcResult, String.class);
+        Assertions.assertFalse(response.isSuccess());
+        Assertions.assertTrue(response.getErrMsg().contains("account has been 
locked"));
+    }
+
+    @Test
+    void testLoginSuccessfulAndClearErrorCount() throws Exception {
+        UserLoginRequest loginUser = new UserLoginRequest();
+        loginUser.setUsername("admin");
+        // Wrong pwd
+        loginUser.setPassword("test_wrong_pwd");
+
+        MvcResult mvcResult = null;
+        for (int i = 0; i < 19; i++) {
+            // Before locking account, input correct pwd to clear error count
+            if (i == 9) {
+                loginUser.setPassword("inlong");
+            } else {
+                loginUser.setPassword("test_wrong_pwd");
+            }
+            mvcResult = mockMvc.perform(
+                    post("/api/anno/login")
+                            .content(JsonUtils.toJsonString(loginUser))
+                            .contentType(MediaType.APPLICATION_JSON)
+                            .accept(MediaType.APPLICATION_JSON))
+                    .andExpect(status().isOk())
+                    .andReturn();
+        }
+
+        Response<String> response = getResBody(mvcResult, String.class);
+        Assertions.assertFalse(response.isSuccess());
+        Assertions.assertFalse(response.getErrMsg().contains("account has been 
locked"));
+    }
+
     @Test
     void testRegister() throws Exception {
         UserRequest userInfo = UserRequest.builder()

Reply via email to