Commit eb7ceaa5 by linxu

improve login failed hits

parent 940c84c0
...@@ -33,7 +33,7 @@ public class LicenseMgr { ...@@ -33,7 +33,7 @@ public class LicenseMgr {
} }
public static void main(String [] args) throws InvalidAlgorithmParameterException, NoSuchPaddingException, IllegalBlockSizeException, NoSuchAlgorithmException, BadPaddingException, InvalidKeyException { 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); System.out.println("licenseText:" + licenseText);
check(licenseText); check(licenseText);
} }
......
package com.keymobile.sso.conf; package com.keymobile.sso.conf;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.ServletException; import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse; 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.core.AuthenticationException;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler; import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import java.io.IOException; import java.io.IOException;
import java.io.PrintWriter; import java.util.HashMap;
import java.util.Map;
@Component @Component
public class RESTAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler { public class RESTAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
private final ObjectMapper objectMapper = new ObjectMapper();
@Override @Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) throws IOException, ServletException { AuthenticationException exception) throws IOException, ServletException {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); response.setStatus(HttpStatus.UNAUTHORIZED.value());
PrintWriter writer = response.getWriter(); response.setContentType(MediaType.APPLICATION_JSON_VALUE);
writer.write("Invalid credentials"); response.setCharacterEncoding("UTF-8");
writer.flush();
writer.close(); 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; package com.keymobile.sso.conf;
import com.keymobile.sso.exception.LicenseException;
import com.keymobile.sso.logging.LogConstants; import com.keymobile.sso.logging.LogConstants;
import com.keymobile.sso.logging.LogManager; import com.keymobile.sso.logging.LogManager;
import jakarta.servlet.ServletException; import jakarta.servlet.ServletException;
import jakarta.servlet.UnavailableException;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler; import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
...@@ -40,11 +41,35 @@ public class RESTAuthenticationSuccessHandler extends SimpleUrlAuthenticationSuc ...@@ -40,11 +41,35 @@ public class RESTAuthenticationSuccessHandler extends SimpleUrlAuthenticationSuc
if (!SystemVariable.isDisableLicenseCheck()) { if (!SystemVariable.isDisableLicenseCheck()) {
try { try {
if (!licenseChecker.check()) { 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) { } 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; 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) { public LoginException(String msg) {
super(msg); super(msg);
this.cnMessage = msg;
} }
public String getCnMessage() { public String getCnMessage() {
return null; return cnMessage;
} }
} }
...@@ -3,6 +3,7 @@ package com.keymobile.sso.exception; ...@@ -3,6 +3,7 @@ package com.keymobile.sso.exception;
import org.springframework.core.Ordered; import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order; import org.springframework.core.annotation.Order;
import org.springframework.dao.DataIntegrityViolationException; import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ExceptionHandler;
...@@ -15,6 +16,18 @@ import static org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR; ...@@ -15,6 +16,18 @@ import static org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR;
@ControllerAdvice @ControllerAdvice
public class RestExceptionHandler extends ResponseEntityExceptionHandler { 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) @ExceptionHandler(Exception.class)
protected ResponseEntity<Object> handlException(Exception ex, WebRequest request) { protected ResponseEntity<Object> handlException(Exception ex, WebRequest request) {
ApiError apiError; ApiError apiError;
......
package com.keymobile.sso.security; package com.keymobile.sso.security;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.keymobile.sso.logging.LogConstants; import com.keymobile.sso.logging.LogConstants;
import com.keymobile.sso.logging.LogManager; import com.keymobile.sso.logging.LogManager;
import jakarta.servlet.FilterChain; import jakarta.servlet.FilterChain;
...@@ -7,10 +8,14 @@ import jakarta.servlet.ServletException; ...@@ -7,10 +8,14 @@ import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired; 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.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter; import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException; import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
@Component @Component
public class LoginAttemptFilter extends OncePerRequestFilter { public class LoginAttemptFilter extends OncePerRequestFilter {
...@@ -18,6 +23,8 @@ public class LoginAttemptFilter extends OncePerRequestFilter { ...@@ -18,6 +23,8 @@ public class LoginAttemptFilter extends OncePerRequestFilter {
@Autowired @Autowired
private LoginAttemptService loginAttemptService; private LoginAttemptService loginAttemptService;
private final ObjectMapper objectMapper = new ObjectMapper();
@Override @Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException { throws ServletException, IOException {
...@@ -27,9 +34,7 @@ public class LoginAttemptFilter extends OncePerRequestFilter { ...@@ -27,9 +34,7 @@ public class LoginAttemptFilter extends OncePerRequestFilter {
if (username != null && !username.isEmpty()) { if (username != null && !username.isEmpty()) {
if (loginAttemptService.isBlocked(username)) { if (loginAttemptService.isBlocked(username)) {
LogManager.logWarning(LogConstants.CTX_AUDIT, "Blocked login attempt for locked user: " + username); LogManager.logWarning(LogConstants.CTX_AUDIT, "Blocked login attempt for locked user: " + username);
response.setStatus(429); // HTTP 429 Too Many Requests writeErrorResponse(response, HttpStatus.TOO_MANY_REQUESTS, "Account is temporarily locked due to too many failed login attempts. Please try again later.", "账户因多次登录失败被暂时锁定,请稍后再试");
response.getWriter().write("Account is temporarily locked due to too many failed login attempts. Please try again later.");
response.getWriter().flush();
return; return;
} }
} }
...@@ -41,4 +46,18 @@ public class LoginAttemptFilter extends OncePerRequestFilter { ...@@ -41,4 +46,18 @@ public class LoginAttemptFilter extends OncePerRequestFilter {
private boolean isLoginRequest(HttpServletRequest request) { private boolean isLoginRequest(HttpServletRequest request) {
return "/signin".equals(request.getRequestURI()) && "POST".equalsIgnoreCase(request.getMethod()); 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; ...@@ -11,13 +11,13 @@ import java.util.concurrent.ConcurrentHashMap;
@Service @Service
public class LoginAttemptService { public class LoginAttemptService {
@Value("${security.login.max-attempts:5}") @Value("${self.login.max-attempts:5}")
private int maxAttempts; private int maxAttempts;
@Value("${security.login.lockout-duration:30}") @Value("${self.login.lockout-duration:30}")
private int lockoutDurationMinutes; private int lockoutDurationMinutes;
@Value("${security.login.attempt-window:15}") @Value("${self.login.attempt-window:15}")
private int attemptWindowMinutes; private int attemptWindowMinutes;
private final Map<String, LoginAttempt> attempts = new ConcurrentHashMap<>(); private final Map<String, LoginAttempt> attempts = new ConcurrentHashMap<>();
......
...@@ -24,13 +24,13 @@ management: ...@@ -24,13 +24,13 @@ management:
exposure: exposure:
include: prometheus include: prometheus
security:
login:
max-attempts: 5
lockout-duration: 30
attempt-window: 15
logging: logging:
level: level:
root: info root: info
config: classpath:logback-custom.xml 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