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