diff --git a/src/main/java/cc/ryanc/halo/security/context/SecurityContext.java b/src/main/java/cc/ryanc/halo/security/context/SecurityContext.java index eb37b3808..b8a00013b 100644 --- a/src/main/java/cc/ryanc/halo/security/context/SecurityContext.java +++ b/src/main/java/cc/ryanc/halo/security/context/SecurityContext.java @@ -24,4 +24,13 @@ public interface SecurityContext { * @param authentication the new authentication or null if no further authentication should not be stored */ void setAuthentication(@Nullable Authentication authentication); + + /** + * Check if the current context has authenticated or not. + * + * @return true if authenticate; false otherwise + */ + default boolean isAuthenticate() { + return getAuthentication() != null; + } } diff --git a/src/main/java/cc/ryanc/halo/service/impl/UserServiceImpl.java b/src/main/java/cc/ryanc/halo/service/impl/UserServiceImpl.java index b41f43c15..d09c3697d 100644 --- a/src/main/java/cc/ryanc/halo/service/impl/UserServiceImpl.java +++ b/src/main/java/cc/ryanc/halo/service/impl/UserServiceImpl.java @@ -6,11 +6,13 @@ import cc.ryanc.halo.exception.NotFoundException; import cc.ryanc.halo.model.entity.User; import cc.ryanc.halo.model.params.UserParam; import cc.ryanc.halo.repository.UserRepository; +import cc.ryanc.halo.security.context.SecurityContextHolder; import cc.ryanc.halo.security.filter.AdminAuthenticationFilter; import cc.ryanc.halo.security.support.UserDetail; import cc.ryanc.halo.service.UserService; import cc.ryanc.halo.service.base.AbstractCrudService; import cc.ryanc.halo.utils.DateUtils; +import cc.ryanc.halo.utils.HaloUtils; import cn.hutool.core.lang.Validator; import cn.hutool.crypto.digest.BCrypt; import org.springframework.lang.NonNull; @@ -18,6 +20,7 @@ import org.springframework.stereotype.Service; import org.springframework.util.Assert; import javax.servlet.http.HttpSession; +import java.util.Date; import java.util.Optional; import java.util.concurrent.TimeUnit; @@ -80,24 +83,32 @@ public class UserServiceImpl extends AbstractCrudService implemen Assert.hasText(password, "Password must not be blank"); Assert.notNull(httpSession, "Http session must not be null"); + // Check login status + if (SecurityContextHolder.getContext().isAuthenticate()) { + throw new BadRequestException("You have logged in already, no need to log in again"); + } + // Ger user by username User user = Validator.isEmail(key) ? getByEmailOfNonNull(key) : getByUsernameOfNonNull(key); + Date now = DateUtils.now(); + // Check expiration - if (user.getExpireTime() != null && DateUtils.now().before(user.getExpireTime())) { + if (user.getExpireTime() != null && user.getExpireTime().after(now)) { + long seconds = TimeUnit.MINUTES.toSeconds(user.getExpireTime().getTime() - now.getTime()); // If expired - throw new BadRequestException("账号已被禁止登陆,请 " + LOCK_MINUTES + " 分钟后再试"); + throw new BadRequestException("You have been temporarily disabled,please try again " + seconds + " second(s) later").setErrorData(seconds); } if (!BCrypt.checkpw(password, user.getPassword())) { - // If the password is mismatched + // If the password is mismatch // Add login failure count Integer loginFailureCount = stringCacheStore.get(LOGIN_FAILURE_COUNT_KEY).map(Integer::valueOf).orElse(0); - if (loginFailureCount >= MAX_LOGIN_TRY) { + if (loginFailureCount >= MAX_LOGIN_TRY - 1) { // Set expiration - user.setExpireTime(org.apache.commons.lang3.time.DateUtils.addMilliseconds(DateUtils.now(), LOCK_MINUTES)); + user.setExpireTime(org.apache.commons.lang3.time.DateUtils.addMinutes(now, LOCK_MINUTES)); // Update user update(user); } @@ -106,9 +117,16 @@ public class UserServiceImpl extends AbstractCrudService implemen stringCacheStore.put(LOGIN_FAILURE_COUNT_KEY, loginFailureCount.toString(), LOCK_MINUTES, TimeUnit.MINUTES); - throw new BadRequestException("账号或者密码错误,您还有" + (MAX_LOGIN_TRY - loginFailureCount) + "次机会"); + int remainder = MAX_LOGIN_TRY - loginFailureCount; + + String errorMessage = String.format("Username or password incorrect, you%shave %s", remainder <= 0 ? "" : " still ", HaloUtils.pluralize(remainder, "chance", "chances")); + + throw new BadRequestException(errorMessage); } + // Clear the login failure count cache + stringCacheStore.delete(LOGIN_FAILURE_COUNT_KEY); + // Set session httpSession.setAttribute(AdminAuthenticationFilter.ADMIN_SESSION_KEY, new UserDetail(user)); diff --git a/src/main/java/cc/ryanc/halo/utils/HaloUtils.java b/src/main/java/cc/ryanc/halo/utils/HaloUtils.java index 9f59d27ac..698afe5ad 100755 --- a/src/main/java/cc/ryanc/halo/utils/HaloUtils.java +++ b/src/main/java/cc/ryanc/halo/utils/HaloUtils.java @@ -37,6 +37,30 @@ import java.util.UUID; @Slf4j public class HaloUtils { + /** + * Pluralize the time label format. + * + * @param time time + * @param label label + * @param pluralLabel plural label + * @return pluralized format + */ + @NonNull + public static String pluralize(long time, @NonNull String label, @NonNull String pluralLabel) { + Assert.hasText(label, "Label must not be blank"); + Assert.hasText(pluralLabel, "Plural label must not be blank"); + + if (time <= 0) { + return "no " + label; + } + + if (time == 1) { + return time + " " + label; + } + + return time + " " + pluralLabel; + } + /** * Gets random uuid without dash. * diff --git a/src/test/java/cc/ryanc/halo/utils/HaloUtilsTest.java b/src/test/java/cc/ryanc/halo/utils/HaloUtilsTest.java new file mode 100644 index 000000000..f0bd3d5b4 --- /dev/null +++ b/src/test/java/cc/ryanc/halo/utils/HaloUtilsTest.java @@ -0,0 +1,54 @@ +package cc.ryanc.halo.utils; + +import org.apache.commons.lang3.RandomUtils; +import org.junit.Test; + +import java.util.stream.IntStream; + +import static org.hamcrest.Matchers.equalTo; +import static org.junit.Assert.assertThat; + +/** + * Halo utilities test. + * + * @author johnniang + * @date 3/29/19 + */ +public class HaloUtilsTest { + + @Test + public void pluralizeTest() { + + String label = "chance"; + String pluralLabel = "chances"; + + String pluralizedFormat = HaloUtils.pluralize(1, label, pluralLabel); + assertThat(pluralizedFormat, equalTo("1 chance")); + + pluralizedFormat = HaloUtils.pluralize(2, label, pluralLabel); + assertThat(pluralizedFormat, equalTo("2 chances")); + + pluralizedFormat = HaloUtils.pluralize(0, label, pluralLabel); + assertThat(pluralizedFormat, equalTo("no chance")); + + // Test random positive time + IntStream.range(0, 10000).forEach(i -> { + long time = RandomUtils.nextLong(2, Long.MAX_VALUE); + String result = HaloUtils.pluralize(time, label, pluralLabel); + assertThat(result, equalTo(time + " " + pluralLabel)); + }); + + // Test random negative time + IntStream.range(0, 10000).forEach(i -> { + long time = (-1) * RandomUtils.nextLong(); + String result = HaloUtils.pluralize(time, label, pluralLabel); + assertThat(result, equalTo("no " + label)); + }); + + } + + @Test(expected = IllegalArgumentException.class) + public void pluralizeLabelExceptionTest() { + HaloUtils.pluralize(1, null, null); + } +} \ No newline at end of file