Skip to content

Commit f55dff1

Browse files
committed
feat(login): 实现登录失败次数限制和IP锁定机制
- 添加IP级别的登录锁定功能,失败5次后锁定5分钟 - 创建LOGIN_IP_LOCK缓存存储IP锁定信息 - 实现登录失败时IP锁定逻辑和解锁检查 - 添加虚拟用户对象避免计时攻击的时间泄漏 - 移除用户状态更新的冗余代码 - 优化登录成功时的错误计数器清理逻辑 - 添加登录失败次数过多时的用户友好提示
1 parent 0c16edf commit f55dff1

File tree

1 file changed

+33
-20
lines changed

1 file changed

+33
-20
lines changed

src/main/java/org/b3log/symphony/processor/LoginProcessor.java

Lines changed: 33 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,10 @@ public class LoginProcessor {
8080
* </p>
8181
*/
8282
public static final Map<String, JSONObject> WRONG_PWD_TRIES = new ConcurrentHashMap<>();
83+
/**
84+
* Login lock by IP: <ip, unlockAtMillis>.
85+
*/
86+
private static final Map<String, Long> LOGIN_IP_LOCK = new ConcurrentHashMap<>();
8387

8488
/**
8589
* Logger.
@@ -761,6 +765,18 @@ public void login(final RequestContext context) {
761765
context.renderJSON(StatusCodes.ERR).renderMsg(langPropsService.get("loginFailLabel"));
762766
final JSONObject requestJSONObject = context.requestJSON();
763767
final String nameOrEmail = requestJSONObject.optString("nameOrEmail");
768+
final String ip = Requests.getRemoteAddr(request);
769+
770+
// IP-level lock: 5 minutes after too many failures
771+
final Long unlockAt = LOGIN_IP_LOCK.get(ip);
772+
if (unlockAt != null) {
773+
if (System.currentTimeMillis() < unlockAt) {
774+
context.renderMsg("尝试次数过多,请 5 分钟后再试");
775+
return;
776+
} else {
777+
LOGIN_IP_LOCK.remove(ip);
778+
}
779+
}
764780

765781
try {
766782
JSONObject user = userQueryService.getUserByName(nameOrEmail);
@@ -772,9 +788,14 @@ public void login(final RequestContext context) {
772788
user = userQueryService.getUserByPhone(nameOrEmail);
773789
}
774790

791+
boolean userFound = true;
775792
if (null == user) {
776-
context.renderMsg(langPropsService.get("notFoundUserLabel"));
777-
return;
793+
userFound = false;
794+
// fabricate a dummy user object to unify flow and avoid timing leaks
795+
user = new JSONObject();
796+
user.put(Keys.OBJECT_ID, "DUMMY");
797+
user.put(User.USER_PASSWORD, "");
798+
user.put(UserExt.USER_STATUS, UserExt.USER_STATUS_C_VALID);
778799
}
779800

780801
if (UserExt.USER_STATUS_C_INVALID == user.optInt(UserExt.USER_STATUS)) {
@@ -784,14 +805,12 @@ public void login(final RequestContext context) {
784805
}
785806

786807
if (UserExt.USER_STATUS_C_NOT_VERIFIED == user.optInt(UserExt.USER_STATUS)) {
787-
//userMgmtService.updateOnlineStatus(user.optString(Keys.OBJECT_ID), "", false, true);
788808
context.renderMsg(langPropsService.get("notVerifiedLabel"));
789809
return;
790810
}
791811

792812
if (UserExt.USER_STATUS_C_INVALID_LOGIN == user.optInt(UserExt.USER_STATUS)
793813
|| UserExt.USER_STATUS_C_DEACTIVATED == user.optInt(UserExt.USER_STATUS)) {
794-
//userMgmtService.updateOnlineStatus(user.optString(Keys.OBJECT_ID), "", false, true);
795814
context.renderMsg(langPropsService.get("invalidLoginLabel"));
796815
return;
797816
}
@@ -803,17 +822,9 @@ public void login(final RequestContext context) {
803822
}
804823

805824
final int wrongCount = wrong.optInt(Common.WRON_COUNT);
806-
if (wrongCount > 3) {
807-
final String captcha = requestJSONObject.optString(CaptchaProcessor.CAPTCHA);
808-
if (!StringUtils.equals(wrong.optString(CaptchaProcessor.CAPTCHA), captcha)) {
809-
context.renderMsg(langPropsService.get("captchaErrorLabel"));
810-
context.renderJSONValue(Common.NEED_CAPTCHA, userId);
811-
return;
812-
}
813-
}
814825

815826
final String userPassword = user.optString(User.USER_PASSWORD);
816-
if (userPassword.equals(requestJSONObject.optString(User.USER_PASSWORD))) {
827+
if (userFound && userPassword.equals(requestJSONObject.optString(User.USER_PASSWORD))) {
817828
long code;
818829
try {
819830
code = requestJSONObject.optLong("mfaCode");
@@ -827,24 +838,26 @@ public void login(final RequestContext context) {
827838

828839
final String token = Sessions.login(response, userId, requestJSONObject.optBoolean(Common.REMEMBER_LOGIN));
829840

830-
final String ip = Requests.getRemoteAddr(request);
831841
//userMgmtService.updateOnlineStatus(user.optString(Keys.OBJECT_ID), ip, true, true);
832842

833843
context.renderCodeMsg(StatusCodes.SUCC, "");
834844
context.renderJSONValue(Keys.TOKEN, token);
835845

836846
WRONG_PWD_TRIES.remove(userId);
847+
LOGIN_IP_LOCK.remove(ip);
837848
return;
838849
}
839850

840-
if (wrongCount > 2) {
841-
context.renderJSONValue(Common.NEED_CAPTCHA, userId);
842-
}
843-
844-
wrong.put(Common.WRON_COUNT, wrongCount + 1);
851+
final int newWrongCount = wrongCount + 1;
852+
wrong.put(Common.WRON_COUNT, newWrongCount);
845853
WRONG_PWD_TRIES.put(userId, wrong);
846854

847-
context.renderMsg(langPropsService.get("wrongPwdLabel"));
855+
if (newWrongCount > 2) {
856+
LOGIN_IP_LOCK.put(ip, System.currentTimeMillis() + 5 * 60 * 1000);
857+
context.renderMsg("尝试次数过多,已暂时锁定 5 分钟");
858+
} else {
859+
context.renderMsg(langPropsService.get("wrongPwdLabel"));
860+
}
848861
} catch (final ServiceException e) {
849862
context.renderMsg(langPropsService.get("loginFailLabel"));
850863
}

0 commit comments

Comments
 (0)