diff --git a/src/main/java/run/halo/app/cache/InMemoryCacheStore.java b/src/main/java/run/halo/app/cache/InMemoryCacheStore.java index 1d9c46cfc..3f0984062 100644 --- a/src/main/java/run/halo/app/cache/InMemoryCacheStore.java +++ b/src/main/java/run/halo/app/cache/InMemoryCacheStore.java @@ -91,6 +91,7 @@ public class InMemoryCacheStore extends StringCacheStore { Assert.hasText(key, "Cache key must not be blank"); cacheContainer.remove(key); + log.debug("Removed key: [{}]", key); } /** diff --git a/src/main/java/run/halo/app/controller/admin/api/AdminController.java b/src/main/java/run/halo/app/controller/admin/api/AdminController.java index 09a67d49f..b6457ca1c 100644 --- a/src/main/java/run/halo/app/controller/admin/api/AdminController.java +++ b/src/main/java/run/halo/app/controller/admin/api/AdminController.java @@ -4,15 +4,11 @@ import io.swagger.annotations.ApiOperation; import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.*; import run.halo.app.cache.lock.CacheLock; -import run.halo.app.exception.BadRequestException; -import run.halo.app.model.dto.CountDTO; +import run.halo.app.model.dto.StatisticDTO; import run.halo.app.model.params.LoginParam; -import run.halo.app.security.context.SecurityContextHolder; -import run.halo.app.security.filter.AdminAuthenticationFilter; import run.halo.app.security.token.AuthToken; import run.halo.app.service.AdminService; -import javax.servlet.http.HttpServletRequest; import javax.validation.Valid; /** @@ -39,7 +35,7 @@ public class AdminController { */ @GetMapping("counts") @ApiOperation("Gets count info") - public CountDTO getCount() { + public StatisticDTO getCount() { return adminService.getCount(); } @@ -52,17 +48,7 @@ public class AdminController { @PostMapping("logout") @ApiOperation("Logs out (Clear session)") @CacheLock - public void logout(HttpServletRequest request) { - adminService.clearAuthentication(); - // Check if the current is logging in - boolean authenticated = SecurityContextHolder.getContext().isAuthenticated(); - - if (!authenticated) { - throw new BadRequestException("You haven't logged in yet, so you can't log out"); - } - - request.getSession().removeAttribute(AdminAuthenticationFilter.ADMIN_SESSION_KEY); - - log.info("You have been logged out, Welcome to you next time!"); + public void logout() { + adminService.clearToken(); } } diff --git a/src/main/java/run/halo/app/model/dto/CountDTO.java b/src/main/java/run/halo/app/model/dto/StatisticDTO.java similarity index 87% rename from src/main/java/run/halo/app/model/dto/CountDTO.java rename to src/main/java/run/halo/app/model/dto/StatisticDTO.java index 8d7b0d93d..d9247d530 100644 --- a/src/main/java/run/halo/app/model/dto/CountDTO.java +++ b/src/main/java/run/halo/app/model/dto/StatisticDTO.java @@ -3,13 +3,13 @@ package run.halo.app.model.dto; import lombok.Data; /** - * Count output DTO. + * Statistic DTO. * * @author johnniang * @date 3/19/19 */ @Data -public class CountDTO { +public class StatisticDTO { private long postCount; diff --git a/src/main/java/run/halo/app/security/filter/AdminAuthenticationFilter.java b/src/main/java/run/halo/app/security/filter/AdminAuthenticationFilter.java index c49cb06f4..a7b0aaa27 100644 --- a/src/main/java/run/halo/app/security/filter/AdminAuthenticationFilter.java +++ b/src/main/java/run/halo/app/security/filter/AdminAuthenticationFilter.java @@ -14,6 +14,7 @@ import run.halo.app.security.authentication.AuthenticationImpl; import run.halo.app.security.context.SecurityContextHolder; import run.halo.app.security.context.SecurityContextImpl; import run.halo.app.security.support.UserDetail; +import run.halo.app.security.util.SecurityUtils; import run.halo.app.service.UserService; import javax.servlet.FilterChain; @@ -37,6 +38,16 @@ public class AdminAuthenticationFilter extends AbstractAuthenticationFilter { */ public final static String ADMIN_SESSION_KEY = "halo.admin.session"; + /** + * Access token cache prefix. + */ + public final static String TOKEN_ACCESS_CACHE_PREFIX = "halo.admin.access.token."; + + /** + * Refresh token cache prefix. + */ + public final static String TOKEN_REFRESH_CACHE_PREFIX = "halo.admin.refresh.token."; + /** * Admin token header name. */ @@ -82,20 +93,25 @@ public class AdminAuthenticationFilter extends AbstractAuthenticationFilter { if (StringUtils.isNotBlank(token)) { - // Valid the token - Optional optionalUserDetail = cacheStore.getAny(token, UserDetail.class); + // Get user id from cache + Optional optionalUserId = cacheStore.getAny(SecurityUtils.buildTokenAccessKey(token), Integer.class); - if (!optionalUserDetail.isPresent()) { + if (!optionalUserId.isPresent()) { getFailureHandler().onFailure(request, response, new AuthenticationException("The token has been expired or not exist").setErrorData(token)); return; } - UserDetail userDetail = optionalUserDetail.get(); + // Get the user + User user = userService.getById(optionalUserId.get()); + + // Build user detail + UserDetail userDetail = new UserDetail(user); // Set security SecurityContextHolder.setContext(new SecurityContextImpl(new AuthenticationImpl(userDetail))); filterChain.doFilter(request, response); + return; } diff --git a/src/main/java/run/halo/app/security/util/SecurityUtils.java b/src/main/java/run/halo/app/security/util/SecurityUtils.java new file mode 100644 index 000000000..8f828659e --- /dev/null +++ b/src/main/java/run/halo/app/security/util/SecurityUtils.java @@ -0,0 +1,50 @@ +package run.halo.app.security.util; + +import org.springframework.lang.NonNull; +import org.springframework.util.Assert; +import run.halo.app.model.entity.User; + +import static run.halo.app.security.filter.AdminAuthenticationFilter.TOKEN_ACCESS_CACHE_PREFIX; +import static run.halo.app.security.filter.AdminAuthenticationFilter.TOKEN_REFRESH_CACHE_PREFIX; +import static run.halo.app.service.AdminService.ACCESS_TOKEN_CACHE_PREFIX; +import static run.halo.app.service.AdminService.REFRESH_TOKEN_CACHE_PREFIX; + +/** + * Security utilities. + * + * @author johnniang + * @date 19-4-29 + */ +public class SecurityUtils { + + private SecurityUtils() { + } + + @NonNull + public static String buildAccessTokenKey(@NonNull User user) { + Assert.notNull(user, "User must not be null"); + + return ACCESS_TOKEN_CACHE_PREFIX + user.getId(); + } + + @NonNull + public static String buildRefreshTokenKey(@NonNull User user) { + Assert.notNull(user, "User must not be null"); + + return REFRESH_TOKEN_CACHE_PREFIX + user.getId(); + } + + @NonNull + public static String buildTokenAccessKey(@NonNull String accessToken) { + Assert.hasText(accessToken, "Access token must not be blank"); + + return TOKEN_ACCESS_CACHE_PREFIX + accessToken; + } + + @NonNull + public static String buildTokenRefreshKey(@NonNull String refreshToken) { + Assert.hasText(refreshToken, "Refresh token must not be blank"); + + return TOKEN_REFRESH_CACHE_PREFIX + refreshToken; + } +} diff --git a/src/main/java/run/halo/app/service/AdminService.java b/src/main/java/run/halo/app/service/AdminService.java index bc9272409..dba740899 100644 --- a/src/main/java/run/halo/app/service/AdminService.java +++ b/src/main/java/run/halo/app/service/AdminService.java @@ -1,7 +1,7 @@ package run.halo.app.service; import org.springframework.lang.NonNull; -import run.halo.app.model.dto.CountDTO; +import run.halo.app.model.dto.StatisticDTO; import run.halo.app.model.params.LoginParam; import run.halo.app.security.token.AuthToken; @@ -13,6 +13,10 @@ import run.halo.app.security.token.AuthToken; */ public interface AdminService { + String ACCESS_TOKEN_CACHE_PREFIX = "halo.admin.access_token."; + + String REFRESH_TOKEN_CACHE_PREFIX = "halo.admin.refresh_token."; + /** * Authenticates. * @@ -25,7 +29,7 @@ public interface AdminService { /** * Clears authentication. */ - void clearAuthentication(); + void clearToken(); /** * Get system counts. @@ -33,5 +37,5 @@ public interface AdminService { * @return count dto */ @NonNull - CountDTO getCount(); + StatisticDTO getCount(); } diff --git a/src/main/java/run/halo/app/service/UserService.java b/src/main/java/run/halo/app/service/UserService.java index 1dda68976..db4cb206b 100755 --- a/src/main/java/run/halo/app/service/UserService.java +++ b/src/main/java/run/halo/app/service/UserService.java @@ -1,12 +1,13 @@ package run.halo.app.service; import org.springframework.lang.NonNull; +import org.springframework.lang.Nullable; +import run.halo.app.exception.ForbiddenException; import run.halo.app.exception.NotFoundException; import run.halo.app.model.entity.User; import run.halo.app.model.params.UserParam; import run.halo.app.service.base.CrudService; -import javax.servlet.http.HttpSession; import java.util.Optional; /** @@ -80,8 +81,8 @@ public interface UserService extends CrudService { /** * Logins by username and password. * - * @param key username or email must not be blank - * @param password password must not be blank + * @param key username or email must not be blank + * @param password password must not be blank * @return user info */ @NonNull @@ -107,4 +108,21 @@ public interface UserService extends CrudService { */ @NonNull User createBy(@NonNull UserParam userParam); + + /** + * The user must not expire. + * + * @param user user info must not be null + * @throws ForbiddenException throws if the given user has been expired + */ + void mustNotExpire(@NonNull User user); + + /** + * Checks the password is match the user password. + * + * @param user user info must not be null + * @param plainPassword plain password + * @return true if the given password is match the user password; false otherwise + */ + boolean passwordMatch(@NonNull User user, @Nullable String plainPassword); } diff --git a/src/main/java/run/halo/app/service/impl/AdminServiceImpl.java b/src/main/java/run/halo/app/service/impl/AdminServiceImpl.java index a5199bb23..6dc4fdc26 100644 --- a/src/main/java/run/halo/app/service/impl/AdminServiceImpl.java +++ b/src/main/java/run/halo/app/service/impl/AdminServiceImpl.java @@ -1,17 +1,24 @@ package run.halo.app.service.impl; +import cn.hutool.core.lang.Validator; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.util.Assert; import run.halo.app.cache.StringCacheStore; import run.halo.app.exception.BadRequestException; -import run.halo.app.model.dto.CountDTO; +import run.halo.app.model.dto.StatisticDTO; +import run.halo.app.model.entity.User; import run.halo.app.model.enums.CommentStatus; import run.halo.app.model.enums.PostStatus; import run.halo.app.model.params.LoginParam; +import run.halo.app.security.authentication.Authentication; import run.halo.app.security.context.SecurityContextHolder; import run.halo.app.security.token.AuthToken; +import run.halo.app.security.util.SecurityUtils; import run.halo.app.service.*; +import run.halo.app.utils.HaloUtils; + +import java.util.concurrent.TimeUnit; /** * Admin service implementation. @@ -69,42 +76,92 @@ public class AdminServiceImpl implements AdminService { public AuthToken authenticate(LoginParam loginParam) { Assert.notNull(loginParam, "Login param must not be null"); - return null; + if (SecurityContextHolder.getContext().isAuthenticated()) { + // If the user has been logged in + throw new BadRequestException("您已经登录,无需重复登录"); + } + + String username = loginParam.getUsername(); + User user = Validator.isEmail(username) ? + userService.getByEmailOfNonNull(username) : userService.getByUsernameOfNonNull(username); + + userService.mustNotExpire(user); + + if (!userService.passwordMatch(user, loginParam.getPassword())) { + // If the password is mismatch + throw new BadRequestException("Username or password is incorrect"); + } + + // Generate new token + AuthToken token = new AuthToken(); + + int expiredIn = 24 * 3600; + + token.setAccessToken(HaloUtils.randomUUIDWithoutDash()); + token.setExpiredIn(expiredIn); + token.setRefreshToken(HaloUtils.randomUUIDWithoutDash()); + + // Cache those tokens, just for clearing + cacheStore.putAny(SecurityUtils.buildAccessTokenKey(user), token.getAccessToken(), 30, TimeUnit.DAYS); + cacheStore.putAny(SecurityUtils.buildRefreshTokenKey(user), token.getRefreshToken(), 30, TimeUnit.DAYS); + + // Cache those tokens with user id + cacheStore.putAny(SecurityUtils.buildTokenAccessKey(token.getAccessToken()), user.getId(), expiredIn, TimeUnit.SECONDS); + cacheStore.putAny(SecurityUtils.buildTokenRefreshKey(token.getRefreshToken()), user.getId(), 30, TimeUnit.DAYS); + + return token; } @Override - public void clearAuthentication() { + public void clearToken() { // Check if the current is logging in - boolean authenticated = SecurityContextHolder.getContext().isAuthenticated(); + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - if (!authenticated) { + if (authentication == null) { throw new BadRequestException("You haven't logged in yet, so you can't log out"); } + // Get current user + User user = authentication.getDetail().getUser(); + + // Clear access token + cacheStore.getAny(SecurityUtils.buildAccessTokenKey(user), String.class).ifPresent(accessToken -> { + // Delete token + cacheStore.delete(SecurityUtils.buildTokenAccessKey(accessToken)); + cacheStore.delete(SecurityUtils.buildAccessTokenKey(user)); + }); + + // Clear refresh token + cacheStore.getAny(SecurityUtils.buildRefreshTokenKey(user), String.class).ifPresent(refreshToken -> { + cacheStore.delete(SecurityUtils.buildTokenRefreshKey(refreshToken)); + cacheStore.delete(SecurityUtils.buildRefreshTokenKey(user)); + }); + log.info("You have been logged out, looking forward to your next visit!"); } @Override - public CountDTO getCount() { - CountDTO countDTO = new CountDTO(); - countDTO.setPostCount(postService.countByStatus(PostStatus.PUBLISHED)); - countDTO.setAttachmentCount(attachmentService.count()); + public StatisticDTO getCount() { + StatisticDTO statisticDTO = new StatisticDTO(); + statisticDTO.setPostCount(postService.countByStatus(PostStatus.PUBLISHED)); + statisticDTO.setAttachmentCount(attachmentService.count()); // Handle comment count long postCommentCount = postCommentService.countByStatus(CommentStatus.PUBLISHED); long sheetCommentCount = sheetCommentService.countByStatus(CommentStatus.PUBLISHED); long journalCommentCount = journalCommentService.countByStatus(CommentStatus.PUBLISHED); - countDTO.setCommentCount(postCommentCount + sheetCommentCount + journalCommentCount); + statisticDTO.setCommentCount(postCommentCount + sheetCommentCount + journalCommentCount); long birthday = optionService.getBirthday(); long days = (System.currentTimeMillis() - birthday) / (1000 * 24 * 3600); - countDTO.setEstablishDays(days); + statisticDTO.setEstablishDays(days); - countDTO.setLinkCount(linkService.count()); + statisticDTO.setLinkCount(linkService.count()); - countDTO.setVisitCount(postService.countVisit() + sheetService.countVisit()); - countDTO.setLikeCount(postService.countLike() + sheetService.countLike()); - return countDTO; + statisticDTO.setVisitCount(postService.countVisit() + sheetService.countVisit()); + statisticDTO.setLikeCount(postService.countLike() + sheetService.countLike()); + return statisticDTO; } + } diff --git a/src/main/java/run/halo/app/service/impl/UserServiceImpl.java b/src/main/java/run/halo/app/service/impl/UserServiceImpl.java index bd9b119c9..a36cacbf7 100644 --- a/src/main/java/run/halo/app/service/impl/UserServiceImpl.java +++ b/src/main/java/run/halo/app/service/impl/UserServiceImpl.java @@ -2,6 +2,7 @@ package run.halo.app.service.impl; import cn.hutool.core.lang.Validator; import cn.hutool.crypto.digest.BCrypt; +import org.apache.commons.lang3.StringUtils; import org.springframework.context.ApplicationEventPublisher; import org.springframework.lang.NonNull; import org.springframework.stereotype.Service; @@ -12,6 +13,7 @@ import run.halo.app.cache.lock.CacheLock; import run.halo.app.event.logger.LogEvent; import run.halo.app.event.user.UserUpdatedEvent; import run.halo.app.exception.BadRequestException; +import run.halo.app.exception.ForbiddenException; import run.halo.app.exception.NotFoundException; import run.halo.app.model.entity.User; import run.halo.app.model.enums.LogType; @@ -205,6 +207,25 @@ public class UserServiceImpl extends AbstractCrudService implemen return create(user); } + @Override + public void mustNotExpire(User user) { + Assert.notNull(user, "User must not be null"); + + Date now = DateUtils.now(); + if (user.getExpireTime() != null && user.getExpireTime().after(now)) { + long seconds = TimeUnit.MILLISECONDS.toSeconds(user.getExpireTime().getTime() - now.getTime()); + // If expired + throw new ForbiddenException("You have been temporarily disabled,please try again " + HaloUtils.timeFormat(seconds) + " later").setErrorData(seconds); + } + } + + @Override + public boolean passwordMatch(User user, String plainPassword) { + Assert.notNull(user, "User must not be null"); + + return !StringUtils.isBlank(plainPassword) && BCrypt.checkpw(plainPassword, user.getPassword()); + } + @Override @CacheLock public User create(User user) {