Commit 9f1b7bb5 by xieshaohua

Merge remote-tracking branch 'refs/remotes/origin/product-v2' into product-v2-hnrcb

parents 9a444b5c b92533cb
......@@ -83,6 +83,8 @@ Java Spring Boot SSO (Single Sign-On) authentication service. Part of the KeyMob
- Session info endpoint: `/sessionInfo`
- Language endpoint: `/lang`
- Login info endpoint: `/loginInfo?userId=` (admin only) - accepts multiple user ids (repeated param or comma-separated), returns last login time, current locked state/lock time per user
- Unlock endpoint: `/unlock` (admin only) - takes `userId` param; `/lockedUsers` returns locked user ids
- Login attempts/locks are keyed by user id (resolved via `LoginAttemptService.resolveKey`); for unknown usernames the submitted username is used as a fallback key
- Principal format: `username:userId:displayName` (colon-delimited)
### Login Records
......
......@@ -76,7 +76,7 @@ public class LoginManagement {
@Operation(summary = "List currently locked users", description = "Admin only.")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "List of locked usernames returned")})
@ApiResponse(responseCode = "200", description = "List of locked user ids returned")})
@RequestMapping(value = "/lockedUsers", method = RequestMethod.GET)
public @ResponseBody Map<String, Object> lockedUsers(HttpServletResponse response) {
Map<String, Object> rs = new HashMap<>();
......@@ -89,13 +89,13 @@ public class LoginManagement {
@ApiResponse(responseCode = "200", description = "Unlock result returned")})
@RequestMapping(value = "/unlock", method = RequestMethod.POST)
public @ResponseBody Map<String, Object> unlock(HttpServletResponse response,
@Parameter(description = "Username to unlock", required = true)
@RequestParam(value = "username", required = true) String username) {
@Parameter(description = "User id to unlock", required = true)
@RequestParam(value = "userId", required = true) String userId) {
String currentUser = getCurrentUserName();
Map<String, Object> rs = new HashMap<>();
boolean wasLocked = loginAttemptService.unlock(username);
LogManager.logWarning(LogConstants.CTX_AUDIT, "Admin " + currentUser + " unlocked account: " + username);
rs.put("username", username);
boolean wasLocked = loginAttemptService.unlock(userId);
LogManager.logWarning(LogConstants.CTX_AUDIT, "Admin " + currentUser + " unlocked account: " + userId);
rs.put("userId", userId);
rs.put("wasLocked", wasLocked);
return rs;
}
......@@ -116,16 +116,10 @@ public class LoginManagement {
Map<String, Object> info = new HashMap<>();
UserLoginRecord record = userLoginRecordService.getRecord(userId);
if (record != null) {
info.put("lastLoginTime", record.getLastLoginTime());
boolean locked = loginAttemptService.isBlocked(record.getUsername());
info.put("locked", locked);
info.put("lockTime", locked ? loginAttemptService.getLockTime(record.getUsername()) : null);
} else {
info.put("lastLoginTime", null);
info.put("locked", false);
info.put("lockTime", null);
}
info.put("lastLoginTime", record != null ? record.getLastLoginTime() : null);
boolean locked = loginAttemptService.isBlocked(userId);
info.put("locked", locked);
info.put("lockTime", locked ? loginAttemptService.getLockTime(userId) : null);
rs.put(userId, info);
}
......
......@@ -33,7 +33,7 @@ public class LicenseMgr {
}
public static void main(String [] args) throws InvalidAlgorithmParameterException, NoSuchPaddingException, IllegalBlockSizeException, NoSuchAlgorithmException, BadPaddingException, InvalidKeyException {
String licenseText = generate("2026-10-15");
String licenseText = generate("2026-10-27");
System.out.println("licenseText:" + licenseText);
check(licenseText);
}
......
......@@ -16,7 +16,7 @@ public class AuthenticationFailureListener implements ApplicationListener<Authen
@Override
public void onApplicationEvent(AuthenticationFailureBadCredentialsEvent event) {
String username = event.getAuthentication().getName();
loginAttemptService.loginFailed(username);
loginAttemptService.loginFailed(loginAttemptService.resolveKey(username));
LogManager.logWarning(LogConstants.CTX_AUDIT, "Failed login attempt for user: " + username);
}
}
......@@ -20,10 +20,12 @@ public class AuthenticationSuccessListener implements ApplicationListener<Authen
@Override
public void onApplicationEvent(AuthenticationSuccessEvent event) {
String username = event.getAuthentication().getName();
loginAttemptService.loginSucceeded(username);
String[] parts = username.split(":", 3);
if (parts.length >= 2) {
loginAttemptService.loginSucceeded(parts[1]);
userLoginRecordService.recordLogin(parts[1], parts[0]);
} else {
loginAttemptService.loginSucceeded(username);
}
LogManager.logInfo(LogConstants.CTX_AUDIT, "Successful login for user: " + username + ", cleared failed attempts");
}
......
......@@ -32,7 +32,8 @@ public class LoginAttemptFilter extends OncePerRequestFilter {
if (isLoginRequest(request)) {
String username = request.getParameter("username");
if (username != null && !username.isEmpty()) {
if (loginAttemptService.isBlocked(username)) {
String key = loginAttemptService.resolveKey(username);
if (loginAttemptService.isBlocked(key)) {
LogManager.logWarning(LogConstants.CTX_AUDIT, "Blocked login attempt for locked user: " + username);
writeErrorResponse(response, HttpStatus.TOO_MANY_REQUESTS, "Account is temporarily locked due to too many failed login attempts. Please try again later.", "账户因多次登录失败被暂时锁定,请稍后再试");
return;
......
package com.keymobile.sso.security;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
......@@ -24,21 +28,42 @@ public class LoginAttemptService {
private final Map<String, LoginAttempt> attempts = new ConcurrentHashMap<>();
public void loginFailed(String key) {
LoginAttempt attempt = attempts.computeIfAbsent(key, k -> new LoginAttempt());
@Autowired
private UserDetailsService userDetailsService;
/**
* Resolves the tracking key for a submitted username: the user id if the user
* exists (parsed from the "username:userId:displayName" principal), otherwise
* the submitted username itself as a fallback.
*/
public String resolveKey(String username) {
try {
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
String[] parts = userDetails.getUsername().split(":", 3);
if (parts.length >= 2) {
return parts[1];
}
} catch (UsernameNotFoundException e) {
// unknown user: fall back to the submitted username
}
return username;
}
public void loginFailed(String userIdOrName) {
LoginAttempt attempt = attempts.computeIfAbsent(userIdOrName, k -> new LoginAttempt());
attempt.incrementAttempts();
}
public void loginSucceeded(String key) {
attempts.remove(key);
public void loginSucceeded(String userIdOrName) {
attempts.remove(userIdOrName);
}
public boolean unlock(String key) {
LoginAttempt attempt = attempts.get(key);
public boolean unlock(String userIdOrName) {
LoginAttempt attempt = attempts.get(userIdOrName);
if (attempt == null) {
return false;
}
attempts.remove(key);
attempts.remove(userIdOrName);
return attempt.isLocked();
}
......@@ -57,15 +82,15 @@ public class LoginAttemptService {
return lockedUsers;
}
public boolean isBlocked(String key) {
LoginAttempt attempt = attempts.get(key);
public boolean isBlocked(String userIdOrName) {
LoginAttempt attempt = attempts.get(userIdOrName);
if (attempt == null) {
return false;
}
if (attempt.isLocked()) {
if (attempt.getLockTime().plus(lockoutDurationMinutes, ChronoUnit.MINUTES).isBefore(LocalDateTime.now())) {
attempts.remove(key);
attempts.remove(userIdOrName);
return false;
}
return true;
......@@ -77,23 +102,23 @@ public class LoginAttemptService {
}
if (attempt.getFirstAttemptTime().plus(attemptWindowMinutes, ChronoUnit.MINUTES).isBefore(LocalDateTime.now())) {
attempts.remove(key);
attempts.remove(userIdOrName);
return false;
}
return false;
}
public int getRemainingAttempts(String key) {
LoginAttempt attempt = attempts.get(key);
public int getRemainingAttempts(String userIdOrName) {
LoginAttempt attempt = attempts.get(userIdOrName);
if (attempt == null || attempt.isLocked()) {
return 0;
}
return Math.max(0, maxAttempts - attempt.getAttempts());
}
public LocalDateTime getLockTime(String key) {
LoginAttempt attempt = attempts.get(key);
public LocalDateTime getLockTime(String userIdOrName) {
LoginAttempt attempt = attempts.get(userIdOrName);
if (attempt == null || !attempt.isLocked()) {
return null;
}
......
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