Complete token authentication

pull/146/head
johnniang 2019-04-29 13:52:23 +08:00
parent e580f4fa96
commit 7c6708d5f4
9 changed files with 198 additions and 45 deletions

View File

@ -91,6 +91,7 @@ public class InMemoryCacheStore extends StringCacheStore {
Assert.hasText(key, "Cache key must not be blank"); Assert.hasText(key, "Cache key must not be blank");
cacheContainer.remove(key); cacheContainer.remove(key);
log.debug("Removed key: [{}]", key);
} }
/** /**

View File

@ -4,15 +4,11 @@ import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import run.halo.app.cache.lock.CacheLock; import run.halo.app.cache.lock.CacheLock;
import run.halo.app.exception.BadRequestException; import run.halo.app.model.dto.StatisticDTO;
import run.halo.app.model.dto.CountDTO;
import run.halo.app.model.params.LoginParam; 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.security.token.AuthToken;
import run.halo.app.service.AdminService; import run.halo.app.service.AdminService;
import javax.servlet.http.HttpServletRequest;
import javax.validation.Valid; import javax.validation.Valid;
/** /**
@ -39,7 +35,7 @@ public class AdminController {
*/ */
@GetMapping("counts") @GetMapping("counts")
@ApiOperation("Gets count info") @ApiOperation("Gets count info")
public CountDTO getCount() { public StatisticDTO getCount() {
return adminService.getCount(); return adminService.getCount();
} }
@ -52,17 +48,7 @@ public class AdminController {
@PostMapping("logout") @PostMapping("logout")
@ApiOperation("Logs out (Clear session)") @ApiOperation("Logs out (Clear session)")
@CacheLock @CacheLock
public void logout(HttpServletRequest request) { public void logout() {
adminService.clearAuthentication(); adminService.clearToken();
// 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!");
} }
} }

View File

@ -3,13 +3,13 @@ package run.halo.app.model.dto;
import lombok.Data; import lombok.Data;
/** /**
* Count output DTO. * Statistic DTO.
* *
* @author johnniang * @author johnniang
* @date 3/19/19 * @date 3/19/19
*/ */
@Data @Data
public class CountDTO { public class StatisticDTO {
private long postCount; private long postCount;

View File

@ -14,6 +14,7 @@ import run.halo.app.security.authentication.AuthenticationImpl;
import run.halo.app.security.context.SecurityContextHolder; import run.halo.app.security.context.SecurityContextHolder;
import run.halo.app.security.context.SecurityContextImpl; import run.halo.app.security.context.SecurityContextImpl;
import run.halo.app.security.support.UserDetail; import run.halo.app.security.support.UserDetail;
import run.halo.app.security.util.SecurityUtils;
import run.halo.app.service.UserService; import run.halo.app.service.UserService;
import javax.servlet.FilterChain; import javax.servlet.FilterChain;
@ -37,6 +38,16 @@ public class AdminAuthenticationFilter extends AbstractAuthenticationFilter {
*/ */
public final static String ADMIN_SESSION_KEY = "halo.admin.session"; 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. * Admin token header name.
*/ */
@ -82,20 +93,25 @@ public class AdminAuthenticationFilter extends AbstractAuthenticationFilter {
if (StringUtils.isNotBlank(token)) { if (StringUtils.isNotBlank(token)) {
// Valid the token // Get user id from cache
Optional<UserDetail> optionalUserDetail = cacheStore.getAny(token, UserDetail.class); Optional<Integer> 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)); getFailureHandler().onFailure(request, response, new AuthenticationException("The token has been expired or not exist").setErrorData(token));
return; return;
} }
UserDetail userDetail = optionalUserDetail.get(); // Get the user
User user = userService.getById(optionalUserId.get());
// Build user detail
UserDetail userDetail = new UserDetail(user);
// Set security // Set security
SecurityContextHolder.setContext(new SecurityContextImpl(new AuthenticationImpl(userDetail))); SecurityContextHolder.setContext(new SecurityContextImpl(new AuthenticationImpl(userDetail)));
filterChain.doFilter(request, response); filterChain.doFilter(request, response);
return; return;
} }

View File

@ -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;
}
}

View File

@ -1,7 +1,7 @@
package run.halo.app.service; package run.halo.app.service;
import org.springframework.lang.NonNull; 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.model.params.LoginParam;
import run.halo.app.security.token.AuthToken; import run.halo.app.security.token.AuthToken;
@ -13,6 +13,10 @@ import run.halo.app.security.token.AuthToken;
*/ */
public interface AdminService { public interface AdminService {
String ACCESS_TOKEN_CACHE_PREFIX = "halo.admin.access_token.";
String REFRESH_TOKEN_CACHE_PREFIX = "halo.admin.refresh_token.";
/** /**
* Authenticates. * Authenticates.
* *
@ -25,7 +29,7 @@ public interface AdminService {
/** /**
* Clears authentication. * Clears authentication.
*/ */
void clearAuthentication(); void clearToken();
/** /**
* Get system counts. * Get system counts.
@ -33,5 +37,5 @@ public interface AdminService {
* @return count dto * @return count dto
*/ */
@NonNull @NonNull
CountDTO getCount(); StatisticDTO getCount();
} }

View File

@ -1,12 +1,13 @@
package run.halo.app.service; package run.halo.app.service;
import org.springframework.lang.NonNull; 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.exception.NotFoundException;
import run.halo.app.model.entity.User; import run.halo.app.model.entity.User;
import run.halo.app.model.params.UserParam; import run.halo.app.model.params.UserParam;
import run.halo.app.service.base.CrudService; import run.halo.app.service.base.CrudService;
import javax.servlet.http.HttpSession;
import java.util.Optional; import java.util.Optional;
/** /**
@ -80,8 +81,8 @@ public interface UserService extends CrudService<User, Integer> {
/** /**
* Logins by username and password. * Logins by username and password.
* *
* @param key username or email must not be blank * @param key username or email must not be blank
* @param password password must not be blank * @param password password must not be blank
* @return user info * @return user info
*/ */
@NonNull @NonNull
@ -107,4 +108,21 @@ public interface UserService extends CrudService<User, Integer> {
*/ */
@NonNull @NonNull
User createBy(@NonNull UserParam userParam); 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);
} }

View File

@ -1,17 +1,24 @@
package run.halo.app.service.impl; package run.halo.app.service.impl;
import cn.hutool.core.lang.Validator;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import run.halo.app.cache.StringCacheStore; import run.halo.app.cache.StringCacheStore;
import run.halo.app.exception.BadRequestException; 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.CommentStatus;
import run.halo.app.model.enums.PostStatus; import run.halo.app.model.enums.PostStatus;
import run.halo.app.model.params.LoginParam; 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.context.SecurityContextHolder;
import run.halo.app.security.token.AuthToken; import run.halo.app.security.token.AuthToken;
import run.halo.app.security.util.SecurityUtils;
import run.halo.app.service.*; import run.halo.app.service.*;
import run.halo.app.utils.HaloUtils;
import java.util.concurrent.TimeUnit;
/** /**
* Admin service implementation. * Admin service implementation.
@ -69,42 +76,92 @@ public class AdminServiceImpl implements AdminService {
public AuthToken authenticate(LoginParam loginParam) { public AuthToken authenticate(LoginParam loginParam) {
Assert.notNull(loginParam, "Login param must not be null"); 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 @Override
public void clearAuthentication() { public void clearToken() {
// Check if the current is logging in // 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"); 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!"); log.info("You have been logged out, looking forward to your next visit!");
} }
@Override @Override
public CountDTO getCount() { public StatisticDTO getCount() {
CountDTO countDTO = new CountDTO(); StatisticDTO statisticDTO = new StatisticDTO();
countDTO.setPostCount(postService.countByStatus(PostStatus.PUBLISHED)); statisticDTO.setPostCount(postService.countByStatus(PostStatus.PUBLISHED));
countDTO.setAttachmentCount(attachmentService.count()); statisticDTO.setAttachmentCount(attachmentService.count());
// Handle comment count // Handle comment count
long postCommentCount = postCommentService.countByStatus(CommentStatus.PUBLISHED); long postCommentCount = postCommentService.countByStatus(CommentStatus.PUBLISHED);
long sheetCommentCount = sheetCommentService.countByStatus(CommentStatus.PUBLISHED); long sheetCommentCount = sheetCommentService.countByStatus(CommentStatus.PUBLISHED);
long journalCommentCount = journalCommentService.countByStatus(CommentStatus.PUBLISHED); long journalCommentCount = journalCommentService.countByStatus(CommentStatus.PUBLISHED);
countDTO.setCommentCount(postCommentCount + sheetCommentCount + journalCommentCount); statisticDTO.setCommentCount(postCommentCount + sheetCommentCount + journalCommentCount);
long birthday = optionService.getBirthday(); long birthday = optionService.getBirthday();
long days = (System.currentTimeMillis() - birthday) / (1000 * 24 * 3600); 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()); statisticDTO.setVisitCount(postService.countVisit() + sheetService.countVisit());
countDTO.setLikeCount(postService.countLike() + sheetService.countLike()); statisticDTO.setLikeCount(postService.countLike() + sheetService.countLike());
return countDTO; return statisticDTO;
} }
} }

View File

@ -2,6 +2,7 @@ package run.halo.app.service.impl;
import cn.hutool.core.lang.Validator; import cn.hutool.core.lang.Validator;
import cn.hutool.crypto.digest.BCrypt; import cn.hutool.crypto.digest.BCrypt;
import org.apache.commons.lang3.StringUtils;
import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.ApplicationEventPublisher;
import org.springframework.lang.NonNull; import org.springframework.lang.NonNull;
import org.springframework.stereotype.Service; 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.logger.LogEvent;
import run.halo.app.event.user.UserUpdatedEvent; import run.halo.app.event.user.UserUpdatedEvent;
import run.halo.app.exception.BadRequestException; import run.halo.app.exception.BadRequestException;
import run.halo.app.exception.ForbiddenException;
import run.halo.app.exception.NotFoundException; import run.halo.app.exception.NotFoundException;
import run.halo.app.model.entity.User; import run.halo.app.model.entity.User;
import run.halo.app.model.enums.LogType; import run.halo.app.model.enums.LogType;
@ -205,6 +207,25 @@ public class UserServiceImpl extends AbstractCrudService<User, Integer> implemen
return create(user); 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 disabledplease 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 @Override
@CacheLock @CacheLock
public User create(User user) { public User create(User user) {