Commit eb7ceaa5 by linxu

improve login failed hits

parent 940c84c0
......@@ -33,7 +33,7 @@ public class LicenseMgr {
}
public static void main(String [] args) throws InvalidAlgorithmParameterException, NoSuchPaddingException, IllegalBlockSizeException, NoSuchAlgorithmException, BadPaddingException, InvalidKeyException {
String licenseText = generate("2026-05-15");
String licenseText = generate("2026-10-15");
System.out.println("licenseText:" + licenseText);
check(licenseText);
}
......
package com.keymobile.sso.conf;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.Map;
@Component
public class RESTAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
private final ObjectMapper objectMapper = new ObjectMapper();
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) throws IOException, ServletException {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
PrintWriter writer = response.getWriter();
writer.write("Invalid credentials");
writer.flush();
writer.close();
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding("UTF-8");
Map<String, Object> errorResponse = new HashMap<>();
errorResponse.put("status", HttpStatus.UNAUTHORIZED.value());
errorResponse.put("timestamp", System.currentTimeMillis());
errorResponse.put("message", "Invalid credentials");
errorResponse.put("cnMessage", "用户名或密码错误");
objectMapper.writeValue(response.getWriter(), errorResponse);
}
}
\ No newline at end of file
package com.keymobile.sso.conf;
import com.keymobile.sso.exception.LicenseException;
import com.keymobile.sso.logging.LogConstants;
import com.keymobile.sso.logging.LogManager;
import jakarta.servlet.ServletException;
import jakarta.servlet.UnavailableException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
......@@ -40,11 +41,35 @@ public class RESTAuthenticationSuccessHandler extends SimpleUrlAuthenticationSuc
if (!SystemVariable.isDisableLicenseCheck()) {
try {
if (!licenseChecker.check()) {
returnStatus = "license expired";
LogManager.logWarning(LogConstants.CTX_AUDIT,
"License expired for user: " + userNameWithIdAttached);
throw new LicenseException("license expired", "license expired, contact support");
} else {
LogManager.logInfo(LogConstants.CTX_AUDIT, "License valid.");
}
LogManager.logInfo(LogConstants.CTX_AUDIT, "License checked.");
} catch (LicenseException e) {
SecurityContextHolder.clearContext();
if (request.getSession(false) != null) {
request.getSession().invalidate();
}
LogManager.logWarning(LogConstants.CTX_AUDIT,
"License check failed, session invalidated for user: " + userNameWithIdAttached);
throw e;
} catch (Exception e) {
throw new UnavailableException("License已过期, 请与我们联系。");
LogManager.logError(LogConstants.CTX_AUDIT, e,
"License check failed for user: " + userNameWithIdAttached);
SecurityContextHolder.clearContext();
if (request.getSession(false) != null) {
request.getSession().invalidate();
}
throw new LicenseException("license error", "license error, contact support");
}
}
if (!returnStatus.equals("ok")) {
SecurityContextHolder.clearContext();
if (request.getSession(false) != null) {
request.getSession().invalidate();
}
}
......
package com.keymobile.sso.exception;
import java.io.IOException;
public class LicenseException extends IOException {
private String cnMessage;
public LicenseException(String message) {
super(message);
this.cnMessage = message;
}
public LicenseException(String message, String cnMessage) {
super(message);
this.cnMessage = cnMessage;
}
public String getCnMessage() {
return cnMessage;
}
}
package com.keymobile.sso.exception;
public class LoginException extends Exception {
import java.io.IOException;
public class LoginException extends IOException {
private String cnMessage;
public LoginException(String msg) {
super(msg);
this.cnMessage = msg;
}
public String getCnMessage() {
return null;
return cnMessage;
}
}
......@@ -3,6 +3,7 @@ package com.keymobile.sso.exception;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
......@@ -15,6 +16,18 @@ import static org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR;
@ControllerAdvice
public class RestExceptionHandler extends ResponseEntityExceptionHandler {
@ExceptionHandler(LoginException.class)
protected ResponseEntity<Object> handleLoginException(LoginException ex, WebRequest request) {
ApiError apiError = new ApiError(HttpStatus.INTERNAL_SERVER_ERROR, ex.getMessage(), "用户名或密码错误", ex);
return buildResponseEntity(apiError);
}
@ExceptionHandler(LicenseException.class)
protected ResponseEntity<Object> handleLicenseException(LicenseException ex, WebRequest request) {
ApiError apiError = new ApiError(HttpStatus.INTERNAL_SERVER_ERROR, ex.getMessage(), ex.getCnMessage(), ex);
return buildResponseEntity(apiError);
}
@ExceptionHandler(Exception.class)
protected ResponseEntity<Object> handlException(Exception ex, WebRequest request) {
ApiError apiError;
......
package com.keymobile.sso.security;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.keymobile.sso.logging.LogConstants;
import com.keymobile.sso.logging.LogManager;
import jakarta.servlet.FilterChain;
......@@ -7,10 +8,14 @@ import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
@Component
public class LoginAttemptFilter extends OncePerRequestFilter {
......@@ -18,6 +23,8 @@ public class LoginAttemptFilter extends OncePerRequestFilter {
@Autowired
private LoginAttemptService loginAttemptService;
private final ObjectMapper objectMapper = new ObjectMapper();
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
......@@ -27,9 +34,7 @@ public class LoginAttemptFilter extends OncePerRequestFilter {
if (username != null && !username.isEmpty()) {
if (loginAttemptService.isBlocked(username)) {
LogManager.logWarning(LogConstants.CTX_AUDIT, "Blocked login attempt for locked user: " + username);
response.setStatus(429); // HTTP 429 Too Many Requests
response.getWriter().write("Account is temporarily locked due to too many failed login attempts. Please try again later.");
response.getWriter().flush();
writeErrorResponse(response, HttpStatus.TOO_MANY_REQUESTS, "Account is temporarily locked due to too many failed login attempts. Please try again later.", "账户因多次登录失败被暂时锁定,请稍后再试");
return;
}
}
......@@ -41,4 +46,18 @@ public class LoginAttemptFilter extends OncePerRequestFilter {
private boolean isLoginRequest(HttpServletRequest request) {
return "/signin".equals(request.getRequestURI()) && "POST".equalsIgnoreCase(request.getMethod());
}
private void writeErrorResponse(HttpServletResponse response, HttpStatus status, String message, String cnMessage) throws IOException {
response.setStatus(status.value());
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding("UTF-8");
Map<String, Object> errorResponse = new HashMap<>();
errorResponse.put("status", status.value());
errorResponse.put("timestamp", System.currentTimeMillis());
errorResponse.put("message", message);
errorResponse.put("cnMessage", cnMessage);
objectMapper.writeValue(response.getWriter(), errorResponse);
}
}
......@@ -11,13 +11,13 @@ import java.util.concurrent.ConcurrentHashMap;
@Service
public class LoginAttemptService {
@Value("${security.login.max-attempts:5}")
@Value("${self.login.max-attempts:5}")
private int maxAttempts;
@Value("${security.login.lockout-duration:30}")
@Value("${self.login.lockout-duration:30}")
private int lockoutDurationMinutes;
@Value("${security.login.attempt-window:15}")
@Value("${self.login.attempt-window:15}")
private int attemptWindowMinutes;
private final Map<String, LoginAttempt> attempts = new ConcurrentHashMap<>();
......
......@@ -24,13 +24,13 @@ management:
exposure:
include: prometheus
security:
login:
max-attempts: 5
lockout-duration: 30
attempt-window: 15
logging:
level:
root: info
config: classpath:logback-custom.xml
self:
login:
max-attempts: 5
lockout-duration: 30
attempt-window: 15
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment