refactor: post and cateogry authentication (#1678)

pull/1692/head
guqing 2022-03-01 13:32:52 +08:00 committed by GitHub
parent 5064837cf8
commit 4f1a3c5f0b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
36 changed files with 1035 additions and 1069 deletions

View File

@ -57,10 +57,10 @@ public class CategoryController {
@SortDefault(sort = "priority", direction = ASC) Sort sort, @SortDefault(sort = "priority", direction = ASC) Sort sort,
@RequestParam(name = "more", required = false, defaultValue = "false") boolean more) { @RequestParam(name = "more", required = false, defaultValue = "false") boolean more) {
if (more) { if (more) {
return postCategoryService.listCategoryWithPostCountDto(sort, true); return postCategoryService.listCategoryWithPostCountDto(sort);
} }
return categoryService.convertTo(categoryService.listAll(sort, true)); return categoryService.convertTo(categoryService.listAll(sort));
} }
@GetMapping("tree_view") @GetMapping("tree_view")

View File

@ -1,8 +1,9 @@
package run.halo.app.controller.content; package run.halo.app.controller.content;
import static run.halo.app.model.support.HaloConst.POST_PASSWORD_TEMPLATE;
import static run.halo.app.model.support.HaloConst.SUFFIX_FTL;
import java.io.UnsupportedEncodingException; import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
@ -16,6 +17,8 @@ import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes; import org.springframework.web.context.request.ServletRequestAttributes;
import run.halo.app.cache.lock.CacheLock; import run.halo.app.cache.lock.CacheLock;
import run.halo.app.controller.content.auth.ContentAuthenticationManager;
import run.halo.app.controller.content.auth.ContentAuthenticationRequest;
import run.halo.app.controller.content.model.CategoryModel; import run.halo.app.controller.content.model.CategoryModel;
import run.halo.app.controller.content.model.JournalModel; import run.halo.app.controller.content.model.JournalModel;
import run.halo.app.controller.content.model.LinkModel; import run.halo.app.controller.content.model.LinkModel;
@ -23,24 +26,27 @@ import run.halo.app.controller.content.model.PhotoModel;
import run.halo.app.controller.content.model.PostModel; import run.halo.app.controller.content.model.PostModel;
import run.halo.app.controller.content.model.SheetModel; import run.halo.app.controller.content.model.SheetModel;
import run.halo.app.controller.content.model.TagModel; import run.halo.app.controller.content.model.TagModel;
import run.halo.app.exception.AuthenticationException;
import run.halo.app.exception.NotFoundException; import run.halo.app.exception.NotFoundException;
import run.halo.app.exception.UnsupportedException; import run.halo.app.exception.UnsupportedException;
import run.halo.app.model.dto.CategoryDTO; import run.halo.app.model.dto.CategoryDTO;
import run.halo.app.model.dto.post.BasePostMinimalDTO; import run.halo.app.model.dto.post.BasePostMinimalDTO;
import run.halo.app.model.entity.Category;
import run.halo.app.model.entity.Post; import run.halo.app.model.entity.Post;
import run.halo.app.model.entity.Sheet; import run.halo.app.model.entity.Sheet;
import run.halo.app.model.enums.EncryptTypeEnum; import run.halo.app.model.enums.EncryptTypeEnum;
import run.halo.app.model.enums.PostPermalinkType; import run.halo.app.model.enums.PostPermalinkType;
import run.halo.app.model.enums.PostStatus; import run.halo.app.model.enums.PostStatus;
import run.halo.app.model.enums.SheetPermalinkType; import run.halo.app.model.enums.SheetPermalinkType;
import run.halo.app.service.AuthenticationService;
import run.halo.app.service.CategoryService; import run.halo.app.service.CategoryService;
import run.halo.app.service.OptionService; import run.halo.app.service.OptionService;
import run.halo.app.service.PostService; import run.halo.app.service.PostService;
import run.halo.app.service.SheetService; import run.halo.app.service.SheetService;
import run.halo.app.service.ThemeService;
/** /**
* @author ryanwang * @author ryanwang
* @author guqing
* @date 2020-01-07 * @date 2020-01-07
*/ */
@Slf4j @Slf4j
@ -68,10 +74,12 @@ public class ContentContentController {
private final SheetService sheetService; private final SheetService sheetService;
private final AuthenticationService authenticationService;
private final CategoryService categoryService; private final CategoryService categoryService;
private final ThemeService themeService;
private final ContentAuthenticationManager providerManager;
public ContentContentController(PostModel postModel, public ContentContentController(PostModel postModel,
SheetModel sheetModel, SheetModel sheetModel,
CategoryModel categoryModel, CategoryModel categoryModel,
@ -82,8 +90,9 @@ public class ContentContentController {
OptionService optionService, OptionService optionService,
PostService postService, PostService postService,
SheetService sheetService, SheetService sheetService,
AuthenticationService authenticationService, CategoryService categoryService,
CategoryService categoryService) { ThemeService themeService,
ContentAuthenticationManager providerManager) {
this.postModel = postModel; this.postModel = postModel;
this.sheetModel = sheetModel; this.sheetModel = sheetModel;
this.categoryModel = categoryModel; this.categoryModel = categoryModel;
@ -94,8 +103,9 @@ public class ContentContentController {
this.optionService = optionService; this.optionService = optionService;
this.postService = postService; this.postService = postService;
this.sheetService = sheetService; this.sheetService = sheetService;
this.authenticationService = authenticationService;
this.categoryService = categoryService; this.categoryService = categoryService;
this.themeService = themeService;
this.providerManager = providerManager;
} }
@GetMapping("{prefix}") @GetMapping("{prefix}")
@ -240,18 +250,60 @@ public class ContentContentController {
@CacheLock(traceRequest = true, expired = 2) @CacheLock(traceRequest = true, expired = 2)
public String password(@PathVariable("type") String type, public String password(@PathVariable("type") String type,
@PathVariable("slug") String slug, @PathVariable("slug") String slug,
@RequestParam(value = "password") String password) throws UnsupportedEncodingException { @RequestParam(value = "password") String password,
HttpServletRequest request) throws UnsupportedEncodingException {
String redirectUrl;
if (EncryptTypeEnum.POST.getName().equals(type)) { if (EncryptTypeEnum.POST.getName().equals(type)) {
redirectUrl = doAuthenticationPost(slug, password); return authenticatePost(slug, type, password, request);
} else if (EncryptTypeEnum.CATEGORY.getName().equals(type)) { } else if (EncryptTypeEnum.CATEGORY.getName().equals(type)) {
redirectUrl = doAuthenticationCategory(slug, password); return authenticateCategory(slug, type, password, request);
} else { } else {
throw new UnsupportedException("未知的加密类型"); throw new UnsupportedException("未知的加密类型");
} }
return "redirect:" + redirectUrl; }
private String authenticatePost(String slug, String type, String password,
HttpServletRequest request) {
ContentAuthenticationRequest authRequest = new ContentAuthenticationRequest();
authRequest.setPassword(password);
Post post = postService.getBy(PostStatus.INTIMATE, slug);
authRequest.setId(post.getId());
authRequest.setPrincipal(EncryptTypeEnum.POST.getName());
try {
providerManager.authenticate(authRequest);
BasePostMinimalDTO basePostMinimal = postService.convertToMinimal(post);
return "redirect:" + buildRedirectUrl(basePostMinimal.getFullPath());
} catch (AuthenticationException e) {
request.setAttribute("errorMsg", e.getMessage());
request.setAttribute("type", type);
request.setAttribute("slug", slug);
return getPasswordPageUriToForward();
}
}
private String authenticateCategory(String slug, String type, String password,
HttpServletRequest request) {
ContentAuthenticationRequest authRequest = new ContentAuthenticationRequest();
authRequest.setPassword(password);
Category category = categoryService.getBySlugOfNonNull(slug);
authRequest.setId(category.getId());
authRequest.setPrincipal(EncryptTypeEnum.CATEGORY.getName());
try {
providerManager.authenticate(authRequest);
CategoryDTO categoryDto = categoryService.convertTo(category);
return "redirect:" + buildRedirectUrl(categoryDto.getFullPath());
} catch (AuthenticationException e) {
request.setAttribute("errorMsg", e.getMessage());
request.setAttribute("type", type);
request.setAttribute("slug", slug);
return getPasswordPageUriToForward();
}
}
private String getPasswordPageUriToForward() {
if (themeService.templateExists(POST_PASSWORD_TEMPLATE + SUFFIX_FTL)) {
return themeService.render(POST_PASSWORD_TEMPLATE);
}
return "common/template/" + POST_PASSWORD_TEMPLATE;
} }
private NotFoundException buildPathNotFoundException() { private NotFoundException buildPathNotFoundException() {
@ -265,41 +317,13 @@ public class ContentContentController {
return new NotFoundException("无法定位到该路径:" + requestUri); return new NotFoundException("无法定位到该路径:" + requestUri);
} }
private String doAuthenticationPost( private String buildRedirectUrl(String fullPath) {
String slug, String password) throws UnsupportedEncodingException {
Post post = postService.getBy(PostStatus.INTIMATE, slug);
post.setSlug(URLEncoder.encode(post.getSlug(), StandardCharsets.UTF_8.name()));
authenticationService.postAuthentication(post, password);
BasePostMinimalDTO postMinimalDTO = postService.convertToMinimal(post);
StringBuilder redirectUrl = new StringBuilder(); StringBuilder redirectUrl = new StringBuilder();
if (!optionService.isEnabledAbsolutePath()) { if (!optionService.isEnabledAbsolutePath()) {
redirectUrl.append(optionService.getBlogBaseUrl()); redirectUrl.append(optionService.getBlogBaseUrl());
} }
redirectUrl.append(fullPath);
redirectUrl.append(postMinimalDTO.getFullPath());
return redirectUrl.toString();
}
private String doAuthenticationCategory(String slug, String password) {
CategoryDTO
category = categoryService.convertTo(categoryService.getBySlugOfNonNull(slug, true));
authenticationService.categoryAuthentication(category.getId(), password);
StringBuilder redirectUrl = new StringBuilder();
if (!optionService.isEnabledAbsolutePath()) {
redirectUrl.append(optionService.getBlogBaseUrl());
}
redirectUrl.append(category.getFullPath());
return redirectUrl.toString(); return redirectUrl.toString();
} }
} }

View File

@ -5,23 +5,28 @@ import static org.springframework.data.domain.Sort.Direction.DESC;
import com.google.common.collect.Sets; import com.google.common.collect.Sets;
import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiOperation;
import java.util.List; import java.util.List;
import java.util.Set;
import org.springframework.data.domain.Page; import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort;
import org.springframework.data.web.PageableDefault; import org.springframework.data.web.PageableDefault;
import org.springframework.data.web.SortDefault; import org.springframework.data.web.SortDefault;
import org.springframework.util.Assert;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import run.halo.app.controller.content.auth.CategoryAuthentication;
import run.halo.app.controller.content.auth.ContentAuthenticationManager;
import run.halo.app.controller.content.auth.ContentAuthenticationRequest;
import run.halo.app.exception.ForbiddenException; import run.halo.app.exception.ForbiddenException;
import run.halo.app.model.dto.CategoryDTO; import run.halo.app.model.dto.CategoryDTO;
import run.halo.app.model.entity.Category; import run.halo.app.model.entity.Category;
import run.halo.app.model.entity.Post; import run.halo.app.model.entity.Post;
import run.halo.app.model.enums.EncryptTypeEnum;
import run.halo.app.model.enums.PostStatus; import run.halo.app.model.enums.PostStatus;
import run.halo.app.model.vo.PostListVO; import run.halo.app.model.vo.PostListVO;
import run.halo.app.service.AuthenticationService;
import run.halo.app.service.CategoryService; import run.halo.app.service.CategoryService;
import run.halo.app.service.PostCategoryService; import run.halo.app.service.PostCategoryService;
import run.halo.app.service.PostService; import run.halo.app.service.PostService;
@ -42,16 +47,20 @@ public class CategoryController {
private final PostService postService; private final PostService postService;
private final AuthenticationService authenticationService; private final CategoryAuthentication categoryAuthentication;
private final ContentAuthenticationManager contentAuthenticationManager;
public CategoryController(CategoryService categoryService, public CategoryController(CategoryService categoryService,
PostCategoryService postCategoryService, PostCategoryService postCategoryService,
PostService postService, PostService postService,
AuthenticationService authenticationService) { CategoryAuthentication categoryAuthentication,
ContentAuthenticationManager contentAuthenticationManager) {
this.categoryService = categoryService; this.categoryService = categoryService;
this.postCategoryService = postCategoryService; this.postCategoryService = postCategoryService;
this.postService = postService; this.postService = postService;
this.authenticationService = authenticationService; this.categoryAuthentication = categoryAuthentication;
this.contentAuthenticationManager = contentAuthenticationManager;
} }
@GetMapping @GetMapping
@ -60,7 +69,7 @@ public class CategoryController {
@SortDefault(sort = "updateTime", direction = DESC) Sort sort, @SortDefault(sort = "updateTime", direction = DESC) Sort sort,
@RequestParam(name = "more", required = false, defaultValue = "false") Boolean more) { @RequestParam(name = "more", required = false, defaultValue = "false") Boolean more) {
if (more) { if (more) {
return postCategoryService.listCategoryWithPostCountDto(sort, false); return postCategoryService.listCategoryWithPostCountDto(sort);
} }
return categoryService.convertTo(categoryService.listAll(sort)); return categoryService.convertTo(categoryService.listAll(sort));
} }
@ -72,15 +81,37 @@ public class CategoryController {
@PageableDefault(sort = {"topPriority", "updateTime"}, direction = DESC) @PageableDefault(sort = {"topPriority", "updateTime"}, direction = DESC)
Pageable pageable) { Pageable pageable) {
// Get category by slug // Get category by slug
Category category = categoryService.getBySlugOfNonNull(slug, true); Category category = categoryService.getBySlugOfNonNull(slug);
if (!authenticationService.categoryAuthentication(category.getId(), password)) { Set<PostStatus> statusesToQuery = Sets.immutableEnumSet(PostStatus.PUBLISHED);
throw new ForbiddenException("您没有该分类的访问权限"); if (allowIntimatePosts(category.getId(), password)) {
statusesToQuery = Sets.immutableEnumSet(PostStatus.PUBLISHED, PostStatus.INTIMATE);
} }
Page<Post> postPage = Page<Post> postPage =
postCategoryService.pagePostBy(category.getId(), postCategoryService.pagePostBy(category.getId(), statusesToQuery, pageable);
Sets.immutableEnumSet(PostStatus.PUBLISHED), pageable);
return postService.convertToListVo(postPage); return postService.convertToListVo(postPage);
} }
private boolean allowIntimatePosts(Integer categoryId, String password) {
Assert.notNull(categoryId, "The categoryId must not be null.");
if (!categoryService.isPrivate(categoryId)) {
return false;
}
if (categoryAuthentication.isAuthenticated(categoryId)) {
return true;
}
if (password != null) {
ContentAuthenticationRequest authRequest =
ContentAuthenticationRequest.of(categoryId, password,
EncryptTypeEnum.CATEGORY.getName());
// authenticate this request,throw an error if authenticate failed
contentAuthenticationManager.authenticate(authRequest);
return true;
}
throw new ForbiddenException("您没有该分类的访问权限");
}
} }

View File

@ -0,0 +1,85 @@
package run.halo.app.controller.content.auth;
import java.util.concurrent.TimeUnit;
import org.apache.commons.lang3.StringUtils;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Component;
import run.halo.app.cache.AbstractStringCacheStore;
import run.halo.app.model.entity.Category;
import run.halo.app.model.enums.EncryptTypeEnum;
import run.halo.app.service.CategoryService;
/**
* Authentication for category.
*
* @author guqing
* @date 2022-02-23
*/
@Component
public class CategoryAuthentication implements ContentAuthentication {
private final CategoryService categoryService;
private final AbstractStringCacheStore cacheStore;
public CategoryAuthentication(CategoryService categoryService,
AbstractStringCacheStore cacheStore) {
this.categoryService = categoryService;
this.cacheStore = cacheStore;
}
@Override
@NonNull
public Object getPrincipal() {
return EncryptTypeEnum.CATEGORY.getName();
}
@Override
public boolean isAuthenticated(Integer categoryId) {
Category category = categoryService.getById(categoryId);
if (category.getPassword() == null) {
// All parent category is not encrypted
if (categoryService.lookupFirstEncryptedBy(category.getId()).isEmpty()) {
return true;
}
}
String sessionId = getSessionId();
// No session is represent a client request
if (StringUtils.isEmpty(sessionId)) {
return false;
}
String cacheKey =
buildCacheKey(sessionId, getPrincipal().toString(), String.valueOf(categoryId));
return cacheStore.get(cacheKey).isPresent();
}
@Override
public void setAuthenticated(Integer resourceId, boolean isAuthenticated) {
String sessionId = getSessionId();
// No session is represent a client request
if (StringUtils.isEmpty(sessionId)) {
return;
}
String cacheKey =
buildCacheKey(sessionId, getPrincipal().toString(), String.valueOf(resourceId));
if (isAuthenticated) {
cacheStore.putAny(cacheKey, StringUtils.EMPTY, 1, TimeUnit.DAYS);
return;
}
cacheStore.delete(cacheKey);
}
@Override
public void clearByResourceId(Integer resourceId) {
String resourceCachePrefix =
StringUtils.joinWith(":", CACHE_PREFIX, getPrincipal(), resourceId);
cacheStore.toMap().forEach((key, value) -> {
if (StringUtils.startsWith(key, resourceCachePrefix)) {
cacheStore.delete(key);
}
});
}
}

View File

@ -0,0 +1,79 @@
package run.halo.app.controller.content.auth;
import java.util.Optional;
import javax.servlet.http.HttpServletRequest;
import org.apache.commons.lang3.StringUtils;
import org.springframework.util.Assert;
import run.halo.app.utils.ServletUtils;
/**
* Content authentication.
*
* @author guqing
* @date 2022-02-23
*/
public interface ContentAuthentication {
/**
* The identity of the principal being authenticated.
*
* @return authentication principal.
*/
Object getPrincipal();
/**
* whether the resource been authenticated by a sessionId.
*
* @param resourceId resourceId to authentication
* @see HttpServletRequest#getRequestedSessionId()
* @return true if the resourceId has been authenticated by a sessionId
*/
boolean isAuthenticated(Integer resourceId);
/**
* Set authentication state.
*
* @param resourceId resource identity
* @param isAuthenticated authentication state
* @see HttpServletRequest#getRequestedSessionId()
*/
void setAuthenticated(Integer resourceId, boolean isAuthenticated);
/**
* Clear authentication state.
*
* @param resourceId resource id.
*/
void clearByResourceId(Integer resourceId);
String CACHE_PREFIX = "CONTENT_AUTHENTICATED";
/**
* build authentication cache key.
*
* @param sessionId session id
* @param principal authentication principal
* @param value principal identity
* @return cache key
*/
default String buildCacheKey(String sessionId, String principal,
String value) {
Assert.notNull(sessionId, "The sessionId must not be null.");
Assert.notNull(principal, "The principal must not be null.");
Assert.notNull(value, "The value must not be null.");
return StringUtils.joinWith(":", CACHE_PREFIX, principal, value, sessionId);
}
/**
* Gets request session id.
*
* @return request session id.
*/
default String getSessionId() {
Optional<HttpServletRequest> currentRequest = ServletUtils.getCurrentRequest();
if (currentRequest.isEmpty()) {
return StringUtils.EMPTY;
}
return currentRequest.get().getRequestedSessionId();
}
}

View File

@ -0,0 +1,128 @@
package run.halo.app.controller.content.auth;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import run.halo.app.event.category.CategoryUpdatedEvent;
import run.halo.app.event.post.PostUpdatedEvent;
import run.halo.app.exception.AuthenticationException;
import run.halo.app.exception.NotFoundException;
import run.halo.app.model.entity.Category;
import run.halo.app.model.entity.Post;
import run.halo.app.model.enums.EncryptTypeEnum;
import run.halo.app.service.CategoryService;
import run.halo.app.service.PostCategoryService;
import run.halo.app.service.PostService;
/**
* Content authentication manager.
*
* @author guqing
* @date 2022-02-24
*/
@Component
public class ContentAuthenticationManager {
private final CategoryService categoryService;
private final CategoryAuthentication categoryAuthentication;
private final PostService postService;
private final PostAuthentication postAuthentication;
private final PostCategoryService postCategoryService;
public ContentAuthenticationManager(CategoryService categoryService,
CategoryAuthentication categoryAuthentication, PostService postService,
PostAuthentication postAuthentication,
PostCategoryService postCategoryService) {
this.categoryService = categoryService;
this.categoryAuthentication = categoryAuthentication;
this.postService = postService;
this.postAuthentication = postAuthentication;
this.postCategoryService = postCategoryService;
}
public ContentAuthentication authenticate(ContentAuthenticationRequest authRequest) throws
AuthenticationException {
if (EncryptTypeEnum.POST.getName().equals(authRequest.getPrincipal())) {
return authenticatePost(authRequest);
}
if (EncryptTypeEnum.CATEGORY.getName().equals(authRequest.getPrincipal())) {
return authenticateCategory(authRequest);
}
throw new NotFoundException(
"Could not be found suitable authentication processor for ["
+ authRequest.getPrincipal() + "]");
}
@EventListener(CategoryUpdatedEvent.class)
public void categoryUpdatedListener(CategoryUpdatedEvent event) {
Category category = event.getCategory();
categoryAuthentication.clearByResourceId(category.getId());
}
@EventListener(PostUpdatedEvent.class)
public void postUpdatedListener(PostUpdatedEvent event) {
Post post = event.getPost();
postAuthentication.clearByResourceId(post.getId());
}
private PostAuthentication authenticatePost(ContentAuthenticationRequest authRequest) {
Post post = postService.getById(authRequest.getId());
if (StringUtils.isNotBlank(post.getPassword())) {
if (StringUtils.equals(post.getPassword(), authRequest.getPassword())) {
postAuthentication.setAuthenticated(post.getId(), true);
return postAuthentication;
} else {
throw new AuthenticationException("密码不正确");
}
} else {
List<Category> encryptedCategories = postCategoryService.listCategoriesBy(post.getId())
.stream()
.filter(category -> categoryService.isPrivate(category.getId()))
.collect(Collectors.toList());
// The post has no password and does not belong to any encryption categories.
// Return it directly
if (CollectionUtils.isEmpty(encryptedCategories)) {
return postAuthentication;
}
// Try all categories until the password is correct
for (Category category : encryptedCategories) {
if (StringUtils.equals(category.getPassword(), authRequest.getPassword())) {
postAuthentication.setAuthenticated(post.getId(), true);
return postAuthentication;
}
}
throw new AuthenticationException("密码不正确");
}
}
private CategoryAuthentication authenticateCategory(ContentAuthenticationRequest authRequest) {
Category category = categoryService.getById(authRequest.getId());
if (category.getPassword() == null) {
String parentPassword = categoryService.lookupFirstEncryptedBy(category.getId())
.map(Category::getPassword)
.orElse(null);
if (parentPassword == null) {
return categoryAuthentication;
}
category.setPassword(parentPassword);
}
if (StringUtils.equals(category.getPassword(), authRequest.getPassword())) {
categoryAuthentication.setAuthenticated(category.getId(), true);
return categoryAuthentication;
}
// Finds the first encrypted parent category to authenticate
Category parentCategory =
categoryService.lookupFirstEncryptedBy(authRequest.getId())
.orElseThrow(() -> new AuthenticationException("密码不正确"));
if (!Objects.equals(parentCategory.getPassword(), authRequest.getPassword())) {
throw new AuthenticationException("密码不正确");
}
categoryAuthentication.setAuthenticated(category.getId(), true);
return categoryAuthentication;
}
}

View File

@ -0,0 +1,54 @@
package run.halo.app.controller.content.auth;
import lombok.Data;
/**
* Authentication request for {@link ContentAuthenticationManager}.
*
* @author guqing
* @date 2022-02-24
*/
@Data
public class ContentAuthenticationRequest implements ContentAuthentication {
private Integer id;
private String password;
private String principal;
@Override
public Object getPrincipal() {
return this.principal;
}
@Override
public boolean isAuthenticated(Integer resourceId) {
return false;
}
@Override
public void setAuthenticated(Integer resourceId, boolean isAuthenticated) {
throw new UnsupportedOperationException();
}
@Override
public void clearByResourceId(Integer resourceId) {
throw new UnsupportedOperationException();
}
/**
* Creates a {@link ContentAuthenticationRequest}.
*
* @param id resource id
* @param password resource password
* @param principal authentication principal
* @return a {@link ContentAuthenticationRequest} instance.
*/
public static ContentAuthenticationRequest of(Integer id, String password, String principal) {
ContentAuthenticationRequest request = new ContentAuthenticationRequest();
request.setId(id);
request.setPassword(password);
request.setPrincipal(principal);
return request;
}
}

View File

@ -0,0 +1,79 @@
package run.halo.app.controller.content.auth;
import java.util.concurrent.TimeUnit;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;
import run.halo.app.cache.AbstractStringCacheStore;
import run.halo.app.model.entity.Post;
import run.halo.app.model.enums.EncryptTypeEnum;
import run.halo.app.service.PostService;
/**
* Authentication for post.
*
* @author guqing
* @date 2022-02-24
*/
@Component
public class PostAuthentication implements ContentAuthentication {
private final PostService postService;
private final AbstractStringCacheStore cacheStore;
public PostAuthentication(PostService postService,
AbstractStringCacheStore cacheStore) {
this.postService = postService;
this.cacheStore = cacheStore;
}
@Override
public Object getPrincipal() {
return EncryptTypeEnum.POST.getName();
}
@Override
public boolean isAuthenticated(Integer postId) {
Post post = postService.getById(postId);
if (post.getPassword() == null) {
return true;
}
String sessionId = getSessionId();
// No session is represent a client request
if (StringUtils.isEmpty(sessionId)) {
return false;
}
String cacheKey =
buildCacheKey(sessionId, getPrincipal().toString(), String.valueOf(postId));
return cacheStore.get(cacheKey).isPresent();
}
@Override
public void setAuthenticated(Integer resourceId, boolean isAuthenticated) {
String sessionId = getSessionId();
// No session is represent a client request
if (StringUtils.isEmpty(sessionId)) {
return;
}
String cacheKey =
buildCacheKey(sessionId, getPrincipal().toString(), String.valueOf(resourceId));
if (isAuthenticated) {
cacheStore.putAny(cacheKey, StringUtils.EMPTY, 1, TimeUnit.DAYS);
return;
}
cacheStore.delete(cacheKey);
}
@Override
public void clearByResourceId(Integer resourceId) {
String resourceCachePrefix =
StringUtils.joinWith(":", CACHE_PREFIX, getPrincipal(), resourceId);
cacheStore.toMap().forEach((key, value) -> {
if (StringUtils.startsWith(key, resourceCachePrefix)) {
cacheStore.delete(key);
}
});
}
}

View File

@ -13,13 +13,13 @@ import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.ui.Model; import org.springframework.ui.Model;
import run.halo.app.controller.content.auth.CategoryAuthentication;
import run.halo.app.model.dto.CategoryDTO; import run.halo.app.model.dto.CategoryDTO;
import run.halo.app.model.entity.Category; import run.halo.app.model.entity.Category;
import run.halo.app.model.entity.Post; import run.halo.app.model.entity.Post;
import run.halo.app.model.enums.EncryptTypeEnum; import run.halo.app.model.enums.EncryptTypeEnum;
import run.halo.app.model.enums.PostStatus; import run.halo.app.model.enums.PostStatus;
import run.halo.app.model.vo.PostListVO; import run.halo.app.model.vo.PostListVO;
import run.halo.app.service.AuthenticationService;
import run.halo.app.service.CategoryService; import run.halo.app.service.CategoryService;
import run.halo.app.service.OptionService; import run.halo.app.service.OptionService;
import run.halo.app.service.PostCategoryService; import run.halo.app.service.PostCategoryService;
@ -45,20 +45,20 @@ public class CategoryModel {
private final OptionService optionService; private final OptionService optionService;
private final AuthenticationService authenticationService; private final CategoryAuthentication categoryAuthentication;
public CategoryModel(CategoryService categoryService, public CategoryModel(CategoryService categoryService,
ThemeService themeService, ThemeService themeService,
PostCategoryService postCategoryService, PostCategoryService postCategoryService,
PostService postService, PostService postService,
OptionService optionService, OptionService optionService,
AuthenticationService authenticationService) { CategoryAuthentication categoryAuthentication) {
this.categoryService = categoryService; this.categoryService = categoryService;
this.themeService = themeService; this.themeService = themeService;
this.postCategoryService = postCategoryService; this.postCategoryService = postCategoryService;
this.postService = postService; this.postService = postService;
this.optionService = optionService; this.optionService = optionService;
this.authenticationService = authenticationService; this.categoryAuthentication = categoryAuthentication;
} }
/** /**
@ -85,9 +85,9 @@ public class CategoryModel {
public String listPost(Model model, String slug, Integer page) { public String listPost(Model model, String slug, Integer page) {
// Get category by slug // Get category by slug
final Category category = categoryService.getBySlugOfNonNull(slug, true); final Category category = categoryService.getBySlugOfNonNull(slug);
if (!authenticationService.categoryAuthentication(category.getId(), null)) { if (!categoryAuthentication.isAuthenticated(category.getId())) {
model.addAttribute("slug", category.getSlug()); model.addAttribute("slug", category.getSlug());
model.addAttribute("type", EncryptTypeEnum.CATEGORY.getName()); model.addAttribute("type", EncryptTypeEnum.CATEGORY.getName());
if (themeService.templateExists(POST_PASSWORD_TEMPLATE + SUFFIX_FTL)) { if (themeService.templateExists(POST_PASSWORD_TEMPLATE + SUFFIX_FTL)) {

View File

@ -13,6 +13,7 @@ import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.ui.Model; import org.springframework.ui.Model;
import run.halo.app.cache.AbstractStringCacheStore; import run.halo.app.cache.AbstractStringCacheStore;
import run.halo.app.controller.content.auth.PostAuthentication;
import run.halo.app.exception.ForbiddenException; import run.halo.app.exception.ForbiddenException;
import run.halo.app.exception.NotFoundException; import run.halo.app.exception.NotFoundException;
import run.halo.app.model.entity.Category; import run.halo.app.model.entity.Category;
@ -25,7 +26,6 @@ import run.halo.app.model.enums.EncryptTypeEnum;
import run.halo.app.model.enums.PostStatus; import run.halo.app.model.enums.PostStatus;
import run.halo.app.model.vo.ArchiveYearVO; import run.halo.app.model.vo.ArchiveYearVO;
import run.halo.app.model.vo.PostListVO; import run.halo.app.model.vo.PostListVO;
import run.halo.app.service.AuthenticationService;
import run.halo.app.service.CategoryService; import run.halo.app.service.CategoryService;
import run.halo.app.service.OptionService; import run.halo.app.service.OptionService;
import run.halo.app.service.PostCategoryService; import run.halo.app.service.PostCategoryService;
@ -63,7 +63,7 @@ public class PostModel {
private final AbstractStringCacheStore cacheStore; private final AbstractStringCacheStore cacheStore;
private final AuthenticationService authenticationService; private final PostAuthentication postAuthentication;
public PostModel(PostService postService, public PostModel(PostService postService,
ThemeService themeService, ThemeService themeService,
@ -74,7 +74,7 @@ public class PostModel {
TagService tagService, TagService tagService,
OptionService optionService, OptionService optionService,
AbstractStringCacheStore cacheStore, AbstractStringCacheStore cacheStore,
AuthenticationService authenticationService) { PostAuthentication postAuthentication) {
this.postService = postService; this.postService = postService;
this.themeService = themeService; this.themeService = themeService;
this.postCategoryService = postCategoryService; this.postCategoryService = postCategoryService;
@ -84,7 +84,7 @@ public class PostModel {
this.tagService = tagService; this.tagService = tagService;
this.optionService = optionService; this.optionService = optionService;
this.cacheStore = cacheStore; this.cacheStore = cacheStore;
this.authenticationService = authenticationService; this.postAuthentication = postAuthentication;
} }
public String content(Post post, String token, Model model) { public String content(Post post, String token, Model model) {
@ -105,7 +105,7 @@ public class PostModel {
// Drafts are not allowed bo be accessed by outsiders. // Drafts are not allowed bo be accessed by outsiders.
throw new NotFoundException("查询不到该文章的信息"); throw new NotFoundException("查询不到该文章的信息");
} else if (PostStatus.INTIMATE.equals(post.getStatus()) } else if (PostStatus.INTIMATE.equals(post.getStatus())
&& !authenticationService.postAuthentication(post, null) && !postAuthentication.isAuthenticated(post.getId())
) { ) {
// Encrypted articles must has the correct password before they can be accessed. // Encrypted articles must has the correct password before they can be accessed.
@ -133,7 +133,7 @@ public class PostModel {
postService.getNextPost(post).ifPresent( postService.getNextPost(post).ifPresent(
nextPost -> model.addAttribute("nextPost", postService.convertToDetailVo(nextPost))); nextPost -> model.addAttribute("nextPost", postService.convertToDetailVo(nextPost)));
List<Category> categories = postCategoryService.listCategoriesBy(post.getId(), false); List<Category> categories = postCategoryService.listCategoriesBy(post.getId());
List<Tag> tags = postTagService.listTagsBy(post.getId()); List<Tag> tags = postTagService.listTagsBy(post.getId());
List<PostMeta> metas = postMetaService.listBy(post.getId()); List<PostMeta> metas = postMetaService.listBy(post.getId());

View File

@ -51,7 +51,7 @@ public class CategoryTagDirective implements TemplateDirectiveModel {
switch (method) { switch (method) {
case "list": case "list":
env.setVariable("categories", builder.build().wrap(postCategoryService env.setVariable("categories", builder.build().wrap(postCategoryService
.listCategoryWithPostCountDto(Sort.by(ASC, "priority"), false))); .listCategoryWithPostCountDto(Sort.by(ASC, "priority"))));
break; break;
case "tree": case "tree":
env.setVariable("categories", builder.build() env.setVariable("categories", builder.build()

View File

@ -0,0 +1,24 @@
package run.halo.app.event.category;
import org.springframework.context.ApplicationEvent;
import run.halo.app.model.entity.Category;
/**
* Category updated event.
*
* @author guqing
* @date 2022-02-24
*/
public class CategoryUpdatedEvent extends ApplicationEvent {
private final Category category;
public CategoryUpdatedEvent(Object source, Category category) {
super(source);
this.category = category;
}
public Category getCategory() {
return category;
}
}

View File

@ -0,0 +1,24 @@
package run.halo.app.event.post;
import org.springframework.context.ApplicationEvent;
import run.halo.app.model.entity.Post;
/**
* Post updated event.
*
* @author guqing
* @date 2022-02-24
*/
public class PostUpdatedEvent extends ApplicationEvent {
private final Post post;
public PostUpdatedEvent(Object source, Post post) {
super(source);
this.post = post;
}
public Post getPost() {
return post;
}
}

View File

@ -0,0 +1,79 @@
package run.halo.app.listener.post;
import java.util.List;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
import run.halo.app.event.category.CategoryUpdatedEvent;
import run.halo.app.event.post.PostUpdatedEvent;
import run.halo.app.model.entity.Category;
import run.halo.app.model.entity.Post;
import run.halo.app.model.enums.PostStatus;
import run.halo.app.service.CategoryService;
import run.halo.app.service.PostCategoryService;
import run.halo.app.service.PostService;
/**
* Post status management.
*
* @author guqing
* @date 2022-02-28
*/
@Component
public class PostRefreshStatusListener {
private final PostService postService;
private final CategoryService categoryService;
private final PostCategoryService postCategoryService;
public PostRefreshStatusListener(PostService postService,
CategoryService categoryService,
PostCategoryService postCategoryService) {
this.postService = postService;
this.categoryService = categoryService;
this.postCategoryService = postCategoryService;
}
/**
* If the current category is encrypted, refresh all post referencing the category to
* INTIMATE status.
*
* @param event category updated event
*/
@EventListener(CategoryUpdatedEvent.class)
public void categoryUpdatedListener(CategoryUpdatedEvent event) {
Category category = event.getCategory();
if (!categoryService.existsById(category.getId())) {
return;
}
boolean isPrivate = categoryService.isPrivate(category.getId());
if (!isPrivate) {
return;
}
List<Post> posts = postCategoryService.listPostBy(category.getId());
posts.forEach(post -> {
post.setStatus(PostStatus.INTIMATE);
});
postService.updateInBatch(posts);
}
/**
* If the post belongs to any encryption category, set the status to INTIMATE.
*
* @param event post updated event
*/
@EventListener(PostUpdatedEvent.class)
public void postUpdatedListener(PostUpdatedEvent event) {
Post post = event.getPost();
if (!postService.existsById(post.getId())) {
return;
}
boolean isPrivate = postCategoryService.listByPostId(post.getId())
.stream()
.anyMatch(postCategory -> categoryService.isPrivate(postCategory.getCategoryId()));
if (isPrivate) {
post.setStatus(PostStatus.INTIMATE);
postService.update(post);
}
}
}

View File

@ -7,6 +7,7 @@ import lombok.Data;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import run.halo.app.model.dto.base.InputConverter; import run.halo.app.model.dto.base.InputConverter;
import run.halo.app.model.entity.Category; import run.halo.app.model.entity.Category;
import run.halo.app.model.support.NotAllowSpaceOnly;
import run.halo.app.utils.SlugUtils; import run.halo.app.utils.SlugUtils;
/** /**
@ -36,6 +37,7 @@ public class CategoryParam implements InputConverter<Category> {
private String thumbnail; private String thumbnail;
@Size(max = 255, message = "分类密码的字符长度不能超过 {max}") @Size(max = 255, message = "分类密码的字符长度不能超过 {max}")
@NotAllowSpaceOnly(message = "密码开头和结尾不能包含空字符串")
private String password; private String password;
private Integer parentId = 0; private Integer parentId = 0;

View File

@ -12,6 +12,7 @@ import run.halo.app.model.dto.base.InputConverter;
import run.halo.app.model.entity.Post; import run.halo.app.model.entity.Post;
import run.halo.app.model.entity.PostMeta; import run.halo.app.model.entity.PostMeta;
import run.halo.app.model.enums.PostEditorType; import run.halo.app.model.enums.PostEditorType;
import run.halo.app.model.support.NotAllowSpaceOnly;
import run.halo.app.utils.SlugUtils; import run.halo.app.utils.SlugUtils;
/** /**
@ -47,6 +48,7 @@ public class PostParam extends BasePostParam implements InputConverter<Post> {
@Override @Override
@Size(max = 255, message = "文章密码的字符长度不能超过 {max}") @Size(max = 255, message = "文章密码的字符长度不能超过 {max}")
@NotAllowSpaceOnly(message = "密码开头和结尾不能包含空字符串")
public String getPassword() { public String getPassword() {
return super.getPassword(); return super.getPassword();
} }

View File

@ -0,0 +1,25 @@
package run.halo.app.model.support;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import javax.validation.Constraint;
import javax.validation.Payload;
/**
* Not allow space only validate annotation.
*
* @author guqing
* @date 2022-02-28
*/
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = NotAllowSpaceOnlyConstraintValidator.class)
@Target(value = {ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER})
public @interface NotAllowSpaceOnly {
String message() default "开头和结尾不允许包含空格";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}

View File

@ -0,0 +1,24 @@
package run.halo.app.model.support;
import java.util.Objects;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import org.apache.commons.lang3.StringUtils;
/**
* Not allow space only validator but allow "".
*
* @author guqing
* @date 2022-02-28
*/
public class NotAllowSpaceOnlyConstraintValidator implements
ConstraintValidator<NotAllowSpaceOnly, String> {
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
if (Objects.equals(value, "")) {
return true;
}
return StringUtils.equals(StringUtils.trim(value), value);
}
}

View File

@ -1,31 +0,0 @@
package run.halo.app.service;
import run.halo.app.model.entity.Post;
/**
* Authentication service
*
* @author ZhiXiang Yuan
* @date 2021/01/20 17:39
*/
public interface AuthenticationService {
/**
* post authentication
*
* @param post post
* @param password password
* @return authentication success or fail
*/
boolean postAuthentication(Post post, String password);
/**
* category authentication
*
* @param categoryId category id
* @param password password
* @return authentication success or fail
*/
boolean categoryAuthentication(Integer categoryId, String password);
}

View File

@ -1,65 +0,0 @@
package run.halo.app.service;
import java.util.Set;
/**
* @author ZhiXiang Yuan
* @date 2021/01/20 17:40
*/
public interface AuthorizationService {
/**
* Build post token
*
* @param postId post id
* @return token
*/
static String buildPostToken(Integer postId) {
return "POST:" + postId;
}
/**
* Build category token
*
* @param categoryId category id
* @return token
*/
static String buildCategoryToken(Integer categoryId) {
return "CATEGORY:" + categoryId;
}
/**
* Post authorization
*
* @param postId post id
*/
void postAuthorization(Integer postId);
/**
* CategoryAuthorization
*
* @param categoryId category id
*/
void categoryAuthorization(Integer categoryId);
/**
* Get access permission store
*
* @return access permission store
*/
Set<String> getAccessPermissionStore();
/**
* Delete article authorization
*
* @param postId post id
*/
void deletePostAuthorization(Integer postId);
/**
* Delete category Authorization
*
* @param categoryId category id
*/
void deleteCategoryAuthorization(Integer categoryId);
}

View File

@ -1,8 +1,7 @@
package run.halo.app.service; package run.halo.app.service;
import io.swagger.models.auth.In;
import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Optional;
import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort;
import org.springframework.lang.NonNull; import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable; import org.springframework.lang.Nullable;
@ -58,16 +57,6 @@ public interface CategoryService extends CrudService<Category, Integer> {
@NonNull @NonNull
Category getBySlugOfNonNull(String slug); Category getBySlugOfNonNull(String slug);
/**
* Get category by slug
*
* @param slug slug
* @param queryEncryptCategory whether to query encryption category
* @return Category
*/
@NonNull
Category getBySlugOfNonNull(String slug, boolean queryEncryptCategory);
/** /**
* Get Category by name. * Get Category by name.
* *
@ -85,14 +74,6 @@ public interface CategoryService extends CrudService<Category, Integer> {
@Transactional @Transactional
void removeCategoryAndPostCategoryBy(Integer categoryId); void removeCategoryAndPostCategoryBy(Integer categoryId);
/**
* Refresh post status, when the post is under the encryption category or is has a password,
* it is changed to private, otherwise it is changed to public.
*
* @param affectedPostIdList affected post id list
*/
void refreshPostStatus(List<Integer> affectedPostIdList);
/** /**
* List categories by parent id. * List categories by parent id.
* *
@ -109,34 +90,6 @@ public interface CategoryService extends CrudService<Category, Integer> {
*/ */
List<Category> listAllByParentId(@NonNull Integer id); List<Category> listAllByParentId(@NonNull Integer id);
/**
* List all category not encrypt.
*
* @param sort sort
* @param queryEncryptCategory whether to query encryption category
* @return list of category.
*/
@NonNull
List<Category> listAll(Sort sort, boolean queryEncryptCategory);
/**
* List all category not encrypt.
*
* @param queryEncryptCategory whether to query encryption category
* @return list of category.
*/
List<Category> listAll(boolean queryEncryptCategory);
/**
* List all by ids
*
* @param ids ids
* @param queryEncryptCategory whether to query encryption category
* @return List
*/
@NonNull
List<Category> listAllByIds(Collection<Integer> ids, boolean queryEncryptCategory);
/** /**
* Converts to category dto. * Converts to category dto.
* *
@ -155,23 +108,23 @@ public interface CategoryService extends CrudService<Category, Integer> {
@NonNull @NonNull
List<CategoryDTO> convertTo(@Nullable List<Category> categories); List<CategoryDTO> convertTo(@Nullable List<Category> categories);
/**
* Filter encrypt category
*
* @param categories this categories is not a category list tree
* @return category list
*/
@NonNull
List<Category> filterEncryptCategory(@Nullable List<Category> categories);
/** /**
* Determine whether the category is encrypted. * Determine whether the category is encrypted.
* *
* @param categoryId category id * @param categoryId category id
* @return whether to encrypt * @return whether to encrypt
*/ */
@NonNull boolean isPrivate(Integer categoryId);
Boolean categoryHasEncrypt(Integer categoryId);
/**
* This method will first query all categories and create a tree, then start from the node
* whose ID is <code>categoryId</code> and recursively look up the first encryption category.
*
* @param categoryId categoryId to look up
* @return encrypted immediate parent category If it is found,otherwise an empty
* {@code Optional}.
*/
Optional<Category> lookupFirstEncryptedBy(Integer categoryId);
/** /**
* Use <code>categories</code> to build a category tree. * Use <code>categories</code> to build a category tree.

View File

@ -36,26 +36,14 @@ public interface PostCategoryService extends CrudService<PostCategory, Integer>
@NonNull @NonNull
List<Category> listCategoriesBy(@NonNull Integer postId); List<Category> listCategoriesBy(@NonNull Integer postId);
/**
* Lists category by post id.
*
* @param postId post id must not be null
* @param queryEncryptCategory whether to query encryption category
* @return a list of category
*/
@NonNull
List<Category> listCategoriesBy(@NonNull Integer postId, @NonNull boolean queryEncryptCategory);
/** /**
* List category list map by post id collection. * List category list map by post id collection.
* *
* @param postIds post id collection * @param postIds post id collection
* @param queryEncryptCategory whether to query encryption category
* @return a category list map (key: postId, value: a list of category) * @return a category list map (key: postId, value: a list of category)
*/ */
@NonNull @NonNull
Map<Integer, List<Category>> listCategoryListMap( Map<Integer, List<Category>> listCategoryListMap(@Nullable Collection<Integer> postIds);
@Nullable Collection<Integer> postIds, @NonNull boolean queryEncryptCategory);
/** /**
* Lists post by category id. * Lists post by category id.
@ -202,12 +190,10 @@ public interface PostCategoryService extends CrudService<PostCategory, Integer>
* Lists category with post count. * Lists category with post count.
* *
* @param sort sort info * @param sort sort info
* @param queryEncryptCategory whether to query encryption category
* @return a list of category dto * @return a list of category dto
*/ */
@NonNull @NonNull
List<CategoryWithPostCountDTO> listCategoryWithPostCountDto( List<CategoryWithPostCountDTO> listCategoryWithPostCountDto(@NonNull Sort sort);
@NonNull Sort sort, @NonNull boolean queryEncryptCategory);
/** /**
* Lists by category id. * Lists by category id.

View File

@ -286,15 +286,6 @@ public interface PostService extends BasePostService<Post> {
@NonNull @NonNull
List<PostListVO> convertToListVo(@NonNull List<Post> posts); List<PostListVO> convertToListVo(@NonNull List<Post> posts);
/**
* Converts to a list of post list vo.
*
* @param posts post must not be null
* @param queryEncryptCategory whether to query encryption category
* @return a list of post list vo
*/
List<PostListVO> convertToListVo(List<Post> posts, boolean queryEncryptCategory);
/** /**
* Publish a post visit event. * Publish a post visit event.
* *

View File

@ -224,15 +224,6 @@ public interface BasePostService<POST extends BasePost> extends CrudService<POST
@NonNull @NonNull
POST createOrUpdateBy(@NonNull POST post); POST createOrUpdateBy(@NonNull POST post);
/**
* Filters post content if the password is not blank.
*
* @param post original post must not be null
* @return filtered post
*/
@NonNull
POST filterIfEncrypt(@NonNull POST post);
/** /**
* Convert POST to minimal dto. * Convert POST to minimal dto.
* *

View File

@ -1,110 +0,0 @@
package run.halo.app.service.impl;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import run.halo.app.model.entity.Category;
import run.halo.app.model.entity.Post;
import run.halo.app.repository.CategoryRepository;
import run.halo.app.repository.PostCategoryRepository;
import run.halo.app.service.AuthenticationService;
import run.halo.app.service.AuthorizationService;
/**
* @author ZhiXiang Yuan
* @date 2021/01/20 17:56
*/
@Service
public class AuthenticationServiceImpl implements AuthenticationService {
private final CategoryRepository categoryRepository;
private final AuthorizationService authorizationService;
private final PostCategoryRepository postCategoryRepository;
public AuthenticationServiceImpl(PostCategoryRepository postCategoryRepository,
CategoryRepository categoryRepository,
AuthorizationService authorizationService
) {
this.postCategoryRepository = postCategoryRepository;
this.categoryRepository = categoryRepository;
this.authorizationService = authorizationService;
}
@Override
public boolean postAuthentication(Post post, String password) {
Set<String> accessPermissionStore = authorizationService.getAccessPermissionStore();
if (StringUtils.isNotBlank(post.getPassword())) {
if (accessPermissionStore.contains(AuthorizationService.buildPostToken(post.getId()))) {
return true;
}
if (post.getPassword().equals(password)) {
authorizationService.postAuthorization(post.getId());
return true;
}
return false;
}
Set<Integer> allCategoryIdSet = postCategoryRepository
.findAllCategoryIdsByPostId(post.getId());
if (allCategoryIdSet.isEmpty()) {
return true;
}
for (Integer categoryId : allCategoryIdSet) {
if (categoryAuthentication(categoryId, password)) {
return true;
}
}
return false;
}
@Override
public boolean categoryAuthentication(Integer categoryId, String password) {
Map<Integer, Category> idToCategoryMap = categoryRepository.findAll().stream()
.collect(Collectors.toMap(Category::getId, Function.identity()));
Set<String> accessPermissionStore = authorizationService.getAccessPermissionStore();
return doCategoryAuthentication(
idToCategoryMap, accessPermissionStore, categoryId, password);
}
private boolean doCategoryAuthentication(Map<Integer, Category> idToCategoryMap,
Set<String> accessPermissionStore,
Integer categoryId, String password) {
Category category = idToCategoryMap.get(categoryId);
if (StringUtils.isNotBlank(category.getPassword())) {
if (accessPermissionStore.contains(
AuthorizationService.buildCategoryToken(category.getId()))) {
return true;
}
if (category.getPassword().equals(password)) {
authorizationService.categoryAuthorization(category.getId());
return true;
}
return false;
}
if (category.getParentId() == 0) {
return true;
}
return doCategoryAuthentication(
idToCategoryMap, accessPermissionStore, category.getParentId(), password);
}
}

View File

@ -1,108 +0,0 @@
package run.halo.app.service.impl;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import javax.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import run.halo.app.cache.AbstractStringCacheStore;
import run.halo.app.service.AuthorizationService;
import run.halo.app.utils.JsonUtils;
/**
* @author ZhiXiang Yuan
* @author guqing
* @date 2021/01/21 11:28
*/
@Slf4j
@Service
public class AuthorizationServiceImpl implements AuthorizationService {
private static final String ACCESS_PERMISSION_PREFIX = "ACCESS_PERMISSION: ";
private final AbstractStringCacheStore cacheStore;
public AuthorizationServiceImpl(AbstractStringCacheStore cacheStore) {
this.cacheStore = cacheStore;
}
@Override
public void postAuthorization(Integer postId) {
doAuthorization(AuthorizationService.buildPostToken(postId));
}
@Override
public void categoryAuthorization(Integer categoryId) {
doAuthorization(AuthorizationService.buildCategoryToken(categoryId));
}
@Override
public Set<String> getAccessPermissionStore() {
return cacheStore.getAny(buildAccessPermissionKey(), Set.class).orElseGet(HashSet::new);
}
@Override
public void deletePostAuthorization(Integer postId) {
doDeleteAuthorization(AuthorizationService.buildPostToken(postId));
}
@Override
public void deleteCategoryAuthorization(Integer categoryId) {
doDeleteAuthorization(AuthorizationService.buildCategoryToken(categoryId));
}
private void doDeleteAuthorization(String value) {
Set<String> accessStore = getAccessPermissionStore();
accessStore.remove(value);
cacheStore.putAny(buildAccessPermissionKey(), accessStore, 1, TimeUnit.DAYS);
for (Entry<String, String> entry : cacheStore.toMap().entrySet()) {
String key = entry.getKey();
if (!key.startsWith(ACCESS_PERMISSION_PREFIX)) {
continue;
}
Set<String> valueSet = jsonToValueSet(entry.getValue());
if (valueSet.contains(value)) {
valueSet.remove(value);
cacheStore.putAny(key, valueSet, 1, TimeUnit.DAYS);
}
}
}
private Set<String> jsonToValueSet(String json) {
try {
return JsonUtils.DEFAULT_JSON_MAPPER.readValue(json,
new TypeReference<LinkedHashSet<String>>() {
});
} catch (JsonProcessingException e) {
log.warn("Failed to convert json to authorization cache value set: [{}]", json, e);
}
return Collections.emptySet();
}
private void doAuthorization(String value) {
Set<String> accessStore = getAccessPermissionStore();
accessStore.add(value);
cacheStore.putAny(buildAccessPermissionKey(), accessStore, 1, TimeUnit.DAYS);
}
private String buildAccessPermissionKey() {
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder
.getRequestAttributes();
HttpServletRequest request = requestAttributes.getRequest();
return ACCESS_PERMISSION_PREFIX + request.getSession().getId();
}
}

View File

@ -348,7 +348,7 @@ public class BackupServiceImpl implements BackupService {
data.put("version", HaloConst.HALO_VERSION); data.put("version", HaloConst.HALO_VERSION);
data.put("export_date", DateUtils.now()); data.put("export_date", DateUtils.now());
data.put("attachments", attachmentService.listAll()); data.put("attachments", attachmentService.listAll());
data.put("categories", categoryService.listAll(true)); data.put("categories", categoryService.listAll());
data.put("comment_black_list", commentBlackListService.listAll()); data.put("comment_black_list", commentBlackListService.listAll());
data.put("journals", journalService.listAll()); data.put("journals", journalService.listAll());
data.put("journal_comments", journalCommentService.listAll()); data.put("journal_comments", journalCommentService.listAll());

View File

@ -329,23 +329,6 @@ public abstract class BasePostServiceImpl<POST extends BasePost>
return savedPost; return savedPost;
} }
@Override
public POST filterIfEncrypt(POST post) {
Assert.notNull(post, "Post must not be null");
if (StringUtils.isNotBlank(post.getPassword())) {
String tip = "The post is encrypted by author";
post.setSummary(tip);
Content postContent = new Content();
postContent.setContent(tip);
postContent.setOriginalContent(tip);
post.setContent(PatchedContent.of(postContent));
}
return post;
}
@Override @Override
public BasePostMinimalDTO convertToMinimal(POST post) { public BasePostMinimalDTO convertToMinimal(POST post) {
Assert.notNull(post, "Post must not be null"); Assert.notNull(post, "Post must not be null");

View File

@ -2,25 +2,20 @@ package run.halo.app.service.impl;
import static run.halo.app.model.support.HaloConst.URL_SEPARATOR; import static run.halo.app.model.support.HaloConst.URL_SEPARATOR;
import com.google.common.base.Objects;
import java.util.ArrayDeque; import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.Queue; import java.util.Queue;
import java.util.Set; import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Lazy;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort;
import org.springframework.data.domain.Sort.Order; import org.springframework.data.domain.Sort.Order;
import org.springframework.lang.NonNull; import org.springframework.lang.NonNull;
@ -28,22 +23,16 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils; import org.springframework.util.CollectionUtils;
import run.halo.app.event.category.CategoryUpdatedEvent;
import run.halo.app.exception.AlreadyExistsException; import run.halo.app.exception.AlreadyExistsException;
import run.halo.app.exception.NotFoundException; import run.halo.app.exception.NotFoundException;
import run.halo.app.exception.UnsupportedException;
import run.halo.app.model.dto.CategoryDTO; import run.halo.app.model.dto.CategoryDTO;
import run.halo.app.model.entity.Category; import run.halo.app.model.entity.Category;
import run.halo.app.model.entity.Post;
import run.halo.app.model.entity.PostCategory;
import run.halo.app.model.enums.PostStatus;
import run.halo.app.model.vo.CategoryVO; import run.halo.app.model.vo.CategoryVO;
import run.halo.app.repository.CategoryRepository; import run.halo.app.repository.CategoryRepository;
import run.halo.app.service.AuthenticationService;
import run.halo.app.service.AuthorizationService;
import run.halo.app.service.CategoryService; import run.halo.app.service.CategoryService;
import run.halo.app.service.OptionService; import run.halo.app.service.OptionService;
import run.halo.app.service.PostCategoryService; import run.halo.app.service.PostCategoryService;
import run.halo.app.service.PostService;
import run.halo.app.service.base.AbstractCrudService; import run.halo.app.service.base.AbstractCrudService;
import run.halo.app.utils.BeanUtils; import run.halo.app.utils.BeanUtils;
import run.halo.app.utils.HaloUtils; import run.halo.app.utils.HaloUtils;
@ -68,29 +57,17 @@ public class CategoryServiceImpl extends AbstractCrudService<Category, Integer>
private final OptionService optionService; private final OptionService optionService;
private final AuthorizationService authorizationService; private final ApplicationContext applicationContext;
private PostService postService;
private final AuthenticationService authenticationService;
public CategoryServiceImpl(CategoryRepository categoryRepository, public CategoryServiceImpl(CategoryRepository categoryRepository,
PostCategoryService postCategoryService, PostCategoryService postCategoryService,
OptionService optionService, OptionService optionService,
AuthenticationService authenticationService, ApplicationContext applicationContext) {
AuthorizationService authorizationService) {
super(categoryRepository); super(categoryRepository);
this.categoryRepository = categoryRepository; this.categoryRepository = categoryRepository;
this.postCategoryService = postCategoryService; this.postCategoryService = postCategoryService;
this.optionService = optionService; this.optionService = optionService;
this.authenticationService = authenticationService; this.applicationContext = applicationContext;
this.authorizationService = authorizationService;
}
@Lazy
@Autowired
public void setPostService(PostService postService) {
this.postService = postService;
} }
@Override @Override
@ -126,6 +103,13 @@ public class CategoryServiceImpl extends AbstractCrudService<Category, Integer>
return super.create(category); return super.create(category);
} }
@Override
public Category update(Category category) {
Category updated = super.update(category);
applicationContext.publishEvent(new CategoryUpdatedEvent(this, category));
return updated;
}
@Override @Override
public List<CategoryVO> listAsTree(Sort sort) { public List<CategoryVO> listAsTree(Sort sort) {
Assert.notNull(sort, "Sort info must not be null"); Assert.notNull(sort, "Sort info must not be null");
@ -167,6 +151,11 @@ public class CategoryServiceImpl extends AbstractCrudService<Category, Integer>
return fullPath.toString(); return fullPath.toString();
} }
@Override
public Category getBySlug(String slug) {
return categoryRepository.getBySlug(slug).orElse(null);
}
@NonNull @NonNull
private CategoryVO convertToCategoryVo(Category category) { private CategoryVO convertToCategoryVo(Category category) {
Assert.notNull(category, "The category must not be null."); Assert.notNull(category, "The category must not be null.");
@ -175,60 +164,16 @@ public class CategoryServiceImpl extends AbstractCrudService<Category, Integer>
return categoryVo; return categoryVo;
} }
@Override
public Category getBySlug(String slug) {
Optional<Category> bySlug = categoryRepository.getBySlug(slug);
if (bySlug.isEmpty()) {
return null;
}
Category category = bySlug.get();
if (authenticationService.categoryAuthentication(category.getId(), null)) {
return category;
}
return null;
}
@Override @Override
public Category getBySlugOfNonNull(String slug) { public Category getBySlugOfNonNull(String slug) {
return categoryRepository
Category category = categoryRepository
.getBySlug(slug) .getBySlug(slug)
.orElseThrow(() -> new NotFoundException("查询不到该分类的信息").setErrorData(slug)); .orElseThrow(() -> new NotFoundException("查询不到该分类的信息").setErrorData(slug));
if (authenticationService.categoryAuthentication(category.getId(), null)) {
return category;
}
throw new NotFoundException("查询不到该分类的信息").setErrorData(slug);
}
@Override
public Category getBySlugOfNonNull(String slug, boolean queryEncryptCategory) {
if (queryEncryptCategory) {
return categoryRepository.getBySlug(slug)
.orElseThrow(() -> new NotFoundException("查询不到该分类的信息").setErrorData(slug));
} else {
return this.getBySlugOfNonNull(slug);
}
} }
@Override @Override
public Category getByName(String name) { public Category getByName(String name) {
Optional<Category> byName = categoryRepository.getByName(name); return categoryRepository.getByName(name).orElse(null);
if (byName.isEmpty()) {
return null;
}
Category category = byName.get();
if (authenticationService.categoryAuthentication(category.getId(), null)) {
return category;
}
return null;
} }
@Override @Override
@ -243,42 +188,11 @@ public class CategoryServiceImpl extends AbstractCrudService<Category, Integer>
} }
// Remove category // Remove category
removeById(categoryId); Category category = removeById(categoryId);
// Remove post categories // Remove post categories
List<Integer> affectedPostIdList = postCategoryService.removeByCategoryId(categoryId) postCategoryService.removeByCategoryId(categoryId);
.stream().map(PostCategory::getPostId).collect(Collectors.toList());
refreshPostStatus(affectedPostIdList);
}
@Override
public void refreshPostStatus(List<Integer> affectedPostIdList) {
if (CollectionUtils.isEmpty(affectedPostIdList)) {
return;
}
for (Integer postId : affectedPostIdList) {
Post post = postService.getById(postId);
post.setStatus(null);
if (StringUtils.isNotBlank(post.getPassword())) {
post.setStatus(PostStatus.INTIMATE);
} else {
postCategoryService.listByPostId(postId)
.stream().map(PostCategory::getCategoryId)
.filter(this::categoryHasEncrypt)
.findAny()
.ifPresent(id -> post.setStatus(PostStatus.INTIMATE));
}
if (post.getStatus() == null) {
post.setStatus(PostStatus.PUBLISHED);
}
postService.update(post);
}
applicationContext.publishEvent(new CategoryUpdatedEvent(this, category));
} }
@Override @Override
@ -336,7 +250,7 @@ public class CategoryServiceImpl extends AbstractCrudService<Category, Integer>
Queue<CategoryVO> queue = new ArrayDeque<>(categoryVos); Queue<CategoryVO> queue = new ArrayDeque<>(categoryVos);
while (!queue.isEmpty()) { while (!queue.isEmpty()) {
CategoryVO category = queue.poll(); CategoryVO category = queue.poll();
if (Objects.equal(category.getId(), categoryId)) { if (Objects.equals(category.getId(), categoryId)) {
return Optional.of(category); return Optional.of(category);
} }
if (HaloUtils.isNotEmpty(category.getChildren())) { if (HaloUtils.isNotEmpty(category.getChildren())) {
@ -369,266 +283,8 @@ public class CategoryServiceImpl extends AbstractCrudService<Category, Integer>
} }
@Override @Override
public List<Category> filterEncryptCategory(List<Category> categories) { public boolean isPrivate(Integer categoryId) {
if (CollectionUtils.isEmpty(categories)) { return lookupFirstEncryptedBy(categoryId).isPresent();
return Collections.emptyList();
}
// list to tree, no password desensitise is required here
List<CategoryVO> categoryTree = listToTree(categories);
// filter encrypt category
doFilterEncryptCategory(categoryTree);
List<Category> collectorList = new ArrayList<>();
collectAllChild(collectorList, categoryTree, true);
for (Category category : collectorList) {
category.setPassword(null);
}
return collectorList;
}
/**
* do filter encrypt category
*
* @param categoryList category list
*/
private void doFilterEncryptCategory(List<CategoryVO> categoryList) {
if (CollectionUtils.isEmpty(categoryList)) {
return;
}
for (CategoryVO categoryVO : categoryList) {
if (!authenticationService.categoryAuthentication(categoryVO.getId(), null)) {
// if parent category is not certified, the child category is not displayed.
categoryVO.setChildren(null);
} else {
doFilterEncryptCategory(categoryVO.getChildren());
}
}
}
/**
* Collect all child from tree
*
* @param collectorList collector
* @param childrenList contains categories of children
* @param doNotCollectEncryptedCategory true is not collect, false is collect
*/
private void collectAllChild(List<Category> collectorList,
List<CategoryVO> childrenList,
Boolean doNotCollectEncryptedCategory) {
if (CollectionUtils.isEmpty(childrenList)) {
return;
}
for (CategoryVO categoryVO : childrenList) {
Category category = new Category();
BeanUtils.updateProperties(categoryVO, category);
collectorList.add(category);
if (doNotCollectEncryptedCategory
&& !authenticationService.categoryAuthentication(category.getId(), null)) {
continue;
}
if (HaloUtils.isNotEmpty(categoryVO.getChildren())) {
collectAllChild(collectorList,
categoryVO.getChildren(), doNotCollectEncryptedCategory);
}
}
}
/**
* Collect sub-categories under the specified category.
*
* @param collectorList collector
* @param childrenList contains categories of children
* @param categoryId category id
* @param doNotCollectEncryptedCategory true is not collect, false is collect
*/
private void collectAllChildByCategoryId(List<Category> collectorList,
List<CategoryVO> childrenList,
Integer categoryId,
Boolean doNotCollectEncryptedCategory) {
if (CollectionUtils.isEmpty(childrenList)) {
return;
}
for (CategoryVO categoryVO : childrenList) {
if (categoryVO.getId().equals(categoryId)) {
collectAllChild(collectorList,
categoryVO.getChildren(), doNotCollectEncryptedCategory);
break;
}
}
}
@Override
public List<Category> listAll(Sort sort, boolean queryEncryptCategory) {
if (queryEncryptCategory) {
return super.listAll(sort);
} else {
return this.listAll(sort);
}
}
@Override
public List<Category> listAll(boolean queryEncryptCategory) {
if (queryEncryptCategory) {
return super.listAll();
} else {
return this.listAll();
}
}
@Override
public List<Category> listAll() {
return filterEncryptCategory(super.listAll());
}
@Override
public List<Category> listAll(Sort sort) {
return filterEncryptCategory(super.listAll(sort));
}
@Override
public Page<Category> listAll(Pageable pageable) {
// To prevent developers from querying encrypted categories,
// so paging query operations are not supported here. If you
// really need to use this method, refactor this method to do memory paging.
throw new UnsupportedException("Does not support business layer paging query.");
}
@Override
public List<Category> listAllByIds(Collection<Integer> integers, boolean queryEncryptCategory) {
if (queryEncryptCategory) {
return super.listAllByIds(integers);
} else {
return this.listAllByIds(integers);
}
}
@Override
public List<Category> listAllByIds(Collection<Integer> integers) {
return filterEncryptCategory(super.listAllByIds(integers));
}
@Override
public List<Category> listAllByIds(Collection<Integer> integers, Sort sort) {
return filterEncryptCategory(super.listAllByIds(integers, sort));
}
@Override
@Transactional
public Category update(Category category) {
Category update = super.update(category);
if (StringUtils.isNotBlank(category.getPassword())) {
doEncryptPost(category);
} else {
doDecryptPost(category);
}
// Remove authorization every time an category is updated.
authorizationService.deleteCategoryAuthorization(category.getId());
return update;
}
/**
* Encrypting a category requires encrypting all articles under the category
*
* @param category need encrypt category
*/
private void doEncryptPost(Category category) {
// list to tree with password
List<CategoryVO> categoryTree = listToTree(super.listAll());
List<Category> collectorList = new ArrayList<>();
collectAllChildByCategoryId(collectorList,
categoryTree, category.getId(), true);
Optional.of(collectorList.stream().map(Category::getId).collect(Collectors.toList()))
.map(categoryIdList -> {
categoryIdList.add(category.getId());
return categoryIdList;
})
.map(postCategoryService::listByCategoryIdList)
.filter(postCategoryList -> !postCategoryList.isEmpty())
.map(postCategoryList -> postCategoryList
.stream().map(PostCategory::getPostId).distinct().collect(Collectors.toList()))
.filter(postIdList -> !postIdList.isEmpty())
.map(postIdList -> postService.listAllByIds(postIdList))
.filter(postList -> !postList.isEmpty())
.map(postList -> postList.stream()
.filter(post -> PostStatus.PUBLISHED.equals(post.getStatus()))
.map(Post::getId).collect(Collectors.toList()))
.filter(postIdList -> !postIdList.isEmpty())
.map(postIdList -> postService.updateStatusByIds(postIdList, PostStatus.INTIMATE));
}
private void doDecryptPost(Category category) {
List<Category> allCategoryList = super.listAll();
Map<Integer, Category> idToCategoryMap = allCategoryList.stream().collect(
Collectors.toMap(Category::getId, Function.identity()));
if (doCategoryHasEncrypt(idToCategoryMap, category.getParentId())) {
// If the parent category is encrypted, there is no need to update the encryption status
return;
}
// with password
List<CategoryVO> categoryTree = listToTree(allCategoryList);
List<Category> collectorList = new ArrayList<>();
// Only collect unencrypted sub-categories under the category.
collectAllChildByCategoryId(collectorList,
categoryTree, category.getId(), false);
// Collect the currently decrypted category
collectorList.add(category);
Optional.of(collectorList.stream().map(Category::getId).collect(Collectors.toList()))
.map(postCategoryService::listByCategoryIdList)
.filter(postCategoryList -> !postCategoryList.isEmpty())
.map(postCategoryList -> postCategoryList
.stream().map(PostCategory::getPostId).distinct().collect(Collectors.toList()))
.filter(postIdList -> !postIdList.isEmpty())
.map(postIdList -> postService.listAllByIds(postIdList))
.filter(postList -> !postList.isEmpty())
.map(postList -> postList.stream()
.filter(post -> StringUtils.isBlank(post.getPassword()))
.filter(post -> PostStatus.INTIMATE.equals(post.getStatus()))
.map(Post::getId).collect(Collectors.toList()))
.filter(postIdList -> !postIdList.isEmpty())
.map(postIdList -> postService.updateStatusByIds(postIdList, PostStatus.PUBLISHED));
}
@Override
public Boolean categoryHasEncrypt(Integer categoryId) {
List<Category> allCategoryList = super.listAll();
Map<Integer, Category> idToCategoryMap = allCategoryList.stream().collect(
Collectors.toMap(Category::getId, Function.identity()));
return doCategoryHasEncrypt(idToCategoryMap, categoryId);
} }
@Override @Override
@ -658,30 +314,38 @@ public class CategoryServiceImpl extends AbstractCrudService<Category, Integer>
.collect(Collectors.toList()); .collect(Collectors.toList());
} }
@Override
public Optional<Category> lookupFirstEncryptedBy(Integer categoryId) {
List<Category> categories = listAll();
Map<Integer, Category> categoryMap =
ServiceUtils.convertToMap(categories, Category::getId);
return Optional.ofNullable(findFirstEncryptedCategoryBy(categoryMap, categoryId));
}
/** /**
* Find whether the parent category is encrypted. * Find whether the parent category is encrypted.
* If it is found, the result will be returned immediately.
* Otherwise, it will be found recursively according to parentId.
* *
* @param idToCategoryMap find category by id * @param idToCategoryMap find category by id
* @param categoryId category id * @param categoryId category id
* @return whether to encrypt * @return whether to encrypt
*/ */
private boolean doCategoryHasEncrypt( private Category findFirstEncryptedCategoryBy(Map<Integer, Category> idToCategoryMap,
Map<Integer, Category> idToCategoryMap, Integer categoryId) { Integer categoryId) {
if (categoryId == 0) {
return false;
}
Category category = idToCategoryMap.get(categoryId); Category category = idToCategoryMap.get(categoryId);
if (categoryId == 0 || category == null) {
return null;
}
if (StringUtils.isNotBlank(category.getPassword())) { if (StringUtils.isNotBlank(category.getPassword())) {
return true; return category;
} }
return doCategoryHasEncrypt(idToCategoryMap, category.getParentId()); return findFirstEncryptedCategoryBy(idToCategoryMap, category.getParentId());
} }
@Override @Override
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public List<Category> updateInBatch(Collection<Category> categories) { public List<Category> updateInBatch(Collection<Category> categories) {
@ -696,8 +360,12 @@ public class CategoryServiceImpl extends AbstractCrudService<Category, Integer>
.map(categoryToUpdate -> { .map(categoryToUpdate -> {
Category categoryParam = idCategoryParamMap.get(categoryToUpdate.getId()); Category categoryParam = idCategoryParamMap.get(categoryToUpdate.getId());
BeanUtils.updateProperties(categoryParam, categoryToUpdate); BeanUtils.updateProperties(categoryParam, categoryToUpdate);
return update(categoryToUpdate); Category categoryUpdated = update(categoryToUpdate);
applicationContext.publishEvent(new CategoryUpdatedEvent(this, categoryUpdated));
return categoryUpdated;
}) })
.collect(Collectors.toList()); .collect(Collectors.toList());
} }
} }

View File

@ -76,23 +76,18 @@ public class PostCategoryServiceImpl extends AbstractCrudService<PostCategory, I
@Override @Override
public List<Category> listCategoriesBy(Integer postId) { public List<Category> listCategoriesBy(Integer postId) {
return listCategoriesBy(postId, false);
}
@Override
public List<Category> listCategoriesBy(Integer postId, boolean queryEncryptCategory) {
Assert.notNull(postId, "Post id must not be null"); Assert.notNull(postId, "Post id must not be null");
// Find all category ids // Find all category ids
Set<Integer> categoryIds = postCategoryRepository.findAllCategoryIdsByPostId(postId); Set<Integer> categoryIds = postCategoryRepository.findAllCategoryIdsByPostId(postId);
return categoryService.listAllByIds(categoryIds, queryEncryptCategory); return categoryService.listAllByIds(categoryIds);
} }
@Override @Override
public Map<Integer, List<Category>> listCategoryListMap( public Map<Integer, List<Category>> listCategoryListMap(
Collection<Integer> postIds, boolean queryEncryptCategory) { Collection<Integer> postIds) {
if (CollectionUtils.isEmpty(postIds)) { if (CollectionUtils.isEmpty(postIds)) {
return Collections.emptyMap(); return Collections.emptyMap();
} }
@ -105,7 +100,7 @@ public class PostCategoryServiceImpl extends AbstractCrudService<PostCategory, I
ServiceUtils.fetchProperty(postCategories, PostCategory::getCategoryId); ServiceUtils.fetchProperty(postCategories, PostCategory::getCategoryId);
// Find all categories // Find all categories
List<Category> categories = categoryService.listAllByIds(categoryIds, queryEncryptCategory); List<Category> categories = categoryService.listAllByIds(categoryIds);
// Convert to category map // Convert to category map
Map<Integer, Category> categoryMap = ServiceUtils.convertToMap(categories, Category::getId); Map<Integer, Category> categoryMap = ServiceUtils.convertToMap(categories, Category::getId);
@ -309,10 +304,9 @@ public class PostCategoryServiceImpl extends AbstractCrudService<PostCategory, I
} }
@Override @Override
public List<CategoryWithPostCountDTO> listCategoryWithPostCountDto(@NonNull Sort sort, public List<CategoryWithPostCountDTO> listCategoryWithPostCountDto(@NonNull Sort sort) {
boolean queryEncryptCategory) {
Assert.notNull(sort, "Sort info must not be null"); Assert.notNull(sort, "Sort info must not be null");
List<Category> categories = categoryService.listAll(sort, queryEncryptCategory); List<Category> categories = categoryService.listAll(sort);
List<CategoryVO> categoryTreeVo = categoryService.listToTree(categories); List<CategoryVO> categoryTreeVo = categoryService.listToTree(categories);
populatePostIds(categoryTreeVo); populatePostIds(categoryTreeVo);

View File

@ -22,6 +22,7 @@ import javax.persistence.criteria.Subquery;
import javax.validation.constraints.NotNull; import javax.validation.constraints.NotNull;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.ApplicationEventPublisher;
import org.springframework.data.domain.Page; import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Pageable;
@ -34,6 +35,7 @@ import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils; import org.springframework.util.CollectionUtils;
import run.halo.app.event.logger.LogEvent; import run.halo.app.event.logger.LogEvent;
import run.halo.app.event.post.PostUpdatedEvent;
import run.halo.app.event.post.PostVisitEvent; import run.halo.app.event.post.PostVisitEvent;
import run.halo.app.exception.NotFoundException; import run.halo.app.exception.NotFoundException;
import run.halo.app.model.dto.post.BasePostMinimalDTO; import run.halo.app.model.dto.post.BasePostMinimalDTO;
@ -61,7 +63,6 @@ import run.halo.app.model.vo.PostListVO;
import run.halo.app.model.vo.PostMarkdownVO; import run.halo.app.model.vo.PostMarkdownVO;
import run.halo.app.repository.PostRepository; import run.halo.app.repository.PostRepository;
import run.halo.app.repository.base.BasePostRepository; import run.halo.app.repository.base.BasePostRepository;
import run.halo.app.service.AuthorizationService;
import run.halo.app.service.CategoryService; import run.halo.app.service.CategoryService;
import run.halo.app.service.ContentPatchLogService; import run.halo.app.service.ContentPatchLogService;
import run.halo.app.service.ContentService; import run.halo.app.service.ContentService;
@ -113,10 +114,10 @@ public class PostServiceImpl extends BasePostServiceImpl<Post> implements PostSe
private final OptionService optionService; private final OptionService optionService;
private final AuthorizationService authorizationService;
private final ContentPatchLogService postContentPatchLogService; private final ContentPatchLogService postContentPatchLogService;
private final ApplicationContext applicationContext;
public PostServiceImpl(BasePostRepository<Post> basePostRepository, public PostServiceImpl(BasePostRepository<Post> basePostRepository,
OptionService optionService, OptionService optionService,
PostRepository postRepository, PostRepository postRepository,
@ -127,9 +128,9 @@ public class PostServiceImpl extends BasePostServiceImpl<Post> implements PostSe
PostCommentService postCommentService, PostCommentService postCommentService,
ApplicationEventPublisher eventPublisher, ApplicationEventPublisher eventPublisher,
PostMetaService postMetaService, PostMetaService postMetaService,
AuthorizationService authorizationService,
ContentService contentService, ContentService contentService,
ContentPatchLogService contentPatchLogService) { ContentPatchLogService contentPatchLogService,
ApplicationContext applicationContext) {
super(basePostRepository, optionService, contentService, contentPatchLogService); super(basePostRepository, optionService, contentService, contentPatchLogService);
this.postRepository = postRepository; this.postRepository = postRepository;
this.tagService = tagService; this.tagService = tagService;
@ -140,9 +141,9 @@ public class PostServiceImpl extends BasePostServiceImpl<Post> implements PostSe
this.eventPublisher = eventPublisher; this.eventPublisher = eventPublisher;
this.postMetaService = postMetaService; this.postMetaService = postMetaService;
this.optionService = optionService; this.optionService = optionService;
this.authorizationService = authorizationService;
this.postContentService = contentService; this.postContentService = contentService;
this.postContentPatchLogService = contentPatchLogService; this.postContentPatchLogService = contentPatchLogService;
this.applicationContext = applicationContext;
} }
@Override @Override
@ -565,7 +566,7 @@ public class PostServiceImpl extends BasePostServiceImpl<Post> implements PostSe
List<Tag> tags = postTagService.listTagsBy(post.getId()); List<Tag> tags = postTagService.listTagsBy(post.getId());
// List categories // List categories
List<Category> categories = postCategoryService List<Category> categories = postCategoryService
.listCategoriesBy(post.getId(), queryEncryptCategory); .listCategoriesBy(post.getId());
// List metas // List metas
List<PostMeta> metas = postMetaService.listBy(post.getId()); List<PostMeta> metas = postMetaService.listBy(post.getId());
// Convert to detail vo // Convert to detail vo
@ -633,7 +634,7 @@ public class PostServiceImpl extends BasePostServiceImpl<Post> implements PostSe
// Get category list map // Get category list map
Map<Integer, List<Category>> categoryListMap = postCategoryService Map<Integer, List<Category>> categoryListMap = postCategoryService
.listCategoryListMap(postIds, queryEncryptCategory); .listCategoryListMap(postIds);
// Get comment count // Get comment count
Map<Integer, Long> commentCountMap = postCommentService.countByStatusAndPostIds( Map<Integer, Long> commentCountMap = postCommentService.countByStatusAndPostIds(
@ -685,11 +686,6 @@ public class PostServiceImpl extends BasePostServiceImpl<Post> implements PostSe
@Override @Override
public List<PostListVO> convertToListVo(List<Post> posts) { public List<PostListVO> convertToListVo(List<Post> posts) {
return convertToListVo(posts, false);
}
@Override
public List<PostListVO> convertToListVo(List<Post> posts, boolean queryEncryptCategory) {
Assert.notNull(posts, "Post page must not be null"); Assert.notNull(posts, "Post page must not be null");
Set<Integer> postIds = ServiceUtils.fetchProperty(posts, Post::getId); Set<Integer> postIds = ServiceUtils.fetchProperty(posts, Post::getId);
@ -699,7 +695,7 @@ public class PostServiceImpl extends BasePostServiceImpl<Post> implements PostSe
// Get category list map // Get category list map
Map<Integer, List<Category>> categoryListMap = postCategoryService Map<Integer, List<Category>> categoryListMap = postCategoryService
.listCategoryListMap(postIds, queryEncryptCategory); .listCategoryListMap(postIds);
// Get comment count // Get comment count
Map<Integer, Long> commentCountMap = Map<Integer, Long> commentCountMap =
@ -885,21 +881,9 @@ public class PostServiceImpl extends BasePostServiceImpl<Post> implements PostSe
Set<Integer> categoryIds, Set<PostMeta> metas) { Set<Integer> categoryIds, Set<PostMeta> metas) {
Assert.notNull(post, "Post param must not be null"); Assert.notNull(post, "Post param must not be null");
// Create or update post // if password is not empty
Boolean needEncrypt = Optional.ofNullable(categoryIds)
.filter(HaloUtils::isNotEmpty)
.map(categoryIdSet -> {
for (Integer categoryId : categoryIdSet) {
if (categoryService.categoryHasEncrypt(categoryId)) {
return true;
}
}
return false;
}).orElse(Boolean.FALSE);
// if password is not empty or parent category has encrypt, change status to intimate
if (post.getStatus() != PostStatus.DRAFT if (post.getStatus() != PostStatus.DRAFT
&& (StringUtils.isNotEmpty(post.getPassword()) || needEncrypt) && (StringUtils.isNotEmpty(post.getPassword()))
) { ) {
post.setStatus(PostStatus.INTIMATE); post.setStatus(PostStatus.INTIMATE);
} }
@ -914,7 +898,7 @@ public class PostServiceImpl extends BasePostServiceImpl<Post> implements PostSe
List<Tag> tags = tagService.listAllByIds(tagIds); List<Tag> tags = tagService.listAllByIds(tagIds);
// List all categories // List all categories
List<Category> categories = categoryService.listAllByIds(categoryIds, true); List<Category> categories = categoryService.listAllByIds(categoryIds);
// Create post tags // Create post tags
List<PostTag> postTags = postTagService.mergeOrCreateByIfAbsent(post.getId(), List<PostTag> postTags = postTagService.mergeOrCreateByIfAbsent(post.getId(),
@ -934,8 +918,8 @@ public class PostServiceImpl extends BasePostServiceImpl<Post> implements PostSe
.createOrUpdateByPostId(post.getId(), metas); .createOrUpdateByPostId(post.getId(), metas);
log.debug("Created post metas: [{}]", postMetaList); log.debug("Created post metas: [{}]", postMetaList);
// Remove authorization every time an post is created or updated. // Publish post updated event.
authorizationService.deletePostAuthorization(post.getId()); applicationContext.publishEvent(new PostUpdatedEvent(this, post));
// get draft content by head patch log id // get draft content by head patch log id
Content postContent = postContentService.getById(post.getId()); Content postContent = postContentService.getById(post.getId());
@ -945,27 +929,6 @@ public class PostServiceImpl extends BasePostServiceImpl<Post> implements PostSe
return convertTo(post, tags, categories, postMetaList); return convertTo(post, tags, categories, postMetaList);
} }
@Override
@Transactional
public Post updateStatus(PostStatus status, Integer postId) {
super.updateStatus(status, postId);
if (PostStatus.PUBLISHED.equals(status)) {
// When the update status is published, it is necessary to determine whether
// the post status should be converted to a intimate post
categoryService.refreshPostStatus(Collections.singletonList(postId));
}
return getById(postId);
}
@Override
@Transactional
public List<Post> updateStatusByIds(List<Integer> ids, PostStatus status) {
if (CollectionUtils.isEmpty(ids)) {
return Collections.emptyList();
}
return ids.stream().map(id -> updateStatus(status, id)).collect(Collectors.toList());
}
@Override @Override
public void publishVisitEvent(Integer postId) { public void publishVisitEvent(Integer postId) {
eventPublisher.publishEvent(new PostVisitEvent(this, postId)); eventPublisher.publishEvent(new PostVisitEvent(this, postId));

View File

@ -160,6 +160,7 @@
<span class="top"></span> <span class="top"></span>
<span class="left"></span> <span class="left"></span>
</div> </div>
<div style="margin-top: 8px;color: red;">${errorMsg!}</div>
<div class="submit-input"> <div class="submit-input">
<button type="submit">验证</button> <button type="submit">验证</button>
</div> </div>

View File

@ -0,0 +1,81 @@
package run.halo.app.controller.content;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.when;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import run.halo.app.cache.InMemoryCacheStore;
import run.halo.app.controller.content.auth.CategoryAuthentication;
import run.halo.app.model.entity.Category;
import run.halo.app.model.enums.EncryptTypeEnum;
import run.halo.app.service.CategoryService;
/**
* @author guqing
* @date 2022-02-25
*/
@ExtendWith(SpringExtension.class)
public class CategoryAuthenticationTest {
private CategoryAuthentication categoryAuthentication;
@MockBean
private CategoryService categoryService;
private final InMemoryCacheStore inMemoryCacheStore = new InMemoryCacheStore();
@BeforeEach
public void setUp() {
categoryAuthentication = new CategoryAuthentication(categoryService, inMemoryCacheStore);
Category category = new Category();
category.setId(1);
category.setSlug("slug-1");
category.setName("name-1");
category.setDescription("description-1");
category.setPassword("123");
when(categoryService.getById(1)).thenReturn(category);
}
@Test
public void isAuthenticated() {
MockHttpServletRequest request = new MockHttpServletRequest();
request.setRequestedSessionId("mock_session_id");
RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(request));
boolean authenticated = categoryAuthentication.isAuthenticated(1);
assertThat(authenticated).isFalse();
categoryAuthentication.setAuthenticated(1, true);
assertThat(categoryAuthentication.isAuthenticated(1)).isTrue();
categoryAuthentication.clearByResourceId(1);
assertThat(categoryAuthentication.isAuthenticated(1)).isFalse();
}
@Test
public void buildCacheKeyTest() {
String cacheKey = categoryAuthentication.buildCacheKey("mock_session_id",
EncryptTypeEnum.CATEGORY.getName(), "1");
assertThat(cacheKey).isEqualTo("CONTENT_AUTHENTICATED:category:1:mock_session_id");
}
@Test
public void getSessionIdTest() {
String sessionId = categoryAuthentication.getSessionId();
assertThat(sessionId).isEqualTo("");
MockHttpServletRequest request = new MockHttpServletRequest();
request.setRequestedSessionId("mock_session_id");
RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(request));
assertThat(categoryAuthentication.getSessionId()).isEqualTo("mock_session_id");
}
}

View File

@ -0,0 +1,110 @@
package run.halo.app.controller.content;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.Mockito.when;
import java.util.Optional;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import run.halo.app.controller.content.auth.CategoryAuthentication;
import run.halo.app.controller.content.auth.ContentAuthentication;
import run.halo.app.controller.content.auth.ContentAuthenticationManager;
import run.halo.app.controller.content.auth.ContentAuthenticationRequest;
import run.halo.app.controller.content.auth.PostAuthentication;
import run.halo.app.exception.AuthenticationException;
import run.halo.app.model.entity.Category;
import run.halo.app.model.enums.EncryptTypeEnum;
import run.halo.app.service.CategoryService;
import run.halo.app.service.PostCategoryService;
import run.halo.app.service.PostService;
/**
* Test for {@link run.halo.app.controller.content.auth.ContentAuthenticationManager}.
*
* @author guqing
* @date 2022-02-26
*/
@ExtendWith(SpringExtension.class)
public class ContentAuthenticationManagerTest {
@MockBean
private CategoryService categoryService;
@MockBean
private CategoryAuthentication categoryAuthentication;
@MockBean
private PostService postService;
@MockBean
private PostAuthentication postAuthentication;
@MockBean
private PostCategoryService postCategoryService;
private ContentAuthenticationManager contentAuthenticationManager;
@BeforeEach
public void setUp() {
contentAuthenticationManager =
new ContentAuthenticationManager(categoryService, categoryAuthentication, postService,
postAuthentication, postCategoryService);
}
@Test
public void authenticateCategoryTest() {
/*
* category-1()
* | |-category-2()
*/
Category category1 = new Category();
category1.setId(1);
category1.setPassword("123");
category1.setName("category-1");
category1.setSlug("category-1");
category1.setParentId(0);
Category category2 = new Category();
category2.setId(2);
category2.setPassword(null);
category2.setName("category-2");
category2.setSlug("category-2");
category2.setParentId(1);
// piling object
when(categoryService.lookupFirstEncryptedBy(2))
.thenReturn(Optional.of(category1));
when(categoryService.getById(1)).thenReturn(category1);
when(categoryService.getById(2)).thenReturn(category2);
// build parameter
ContentAuthenticationRequest authRequest =
ContentAuthenticationRequest.of(2, "", EncryptTypeEnum.CATEGORY.getName());
// test empty password
assertThatThrownBy(() -> contentAuthenticationManager.authenticate(authRequest))
.isInstanceOf(AuthenticationException.class)
.hasMessage("密码不正确");
// test null password
authRequest.setPassword(null);
assertThatThrownBy(() -> contentAuthenticationManager.authenticate(authRequest))
.isInstanceOf(AuthenticationException.class)
.hasMessage("密码不正确");
// test incorrect password
authRequest.setPassword("ABCD");
assertThatThrownBy(() -> contentAuthenticationManager.authenticate(authRequest))
.isInstanceOf(AuthenticationException.class)
.hasMessage("密码不正确");
// test correct password
authRequest.setPassword("123");
ContentAuthentication authentication =
contentAuthenticationManager.authenticate(authRequest);
assertThat(authentication).isNotNull();
}
}

View File

@ -43,6 +43,34 @@ public class PostParamTest {
assertThat(validate.iterator().next().getMessage()).isEqualTo("排序字段值不能小于 0"); assertThat(validate.iterator().next().getMessage()).isEqualTo("排序字段值不能小于 0");
} }
@Test
public void validatePassword() {
PostParam postParam = new PostParam();
postParam.setTitle("Title");
postParam.setSlug("Slug");
postParam.setPassword(" 123");
postParam.setTopPriority(0);
Set<ConstraintViolation<PostParam>> validate = validator.validate(postParam);
assertThat(validate).isNotNull();
assertThat(validate).hasSize(1);
assertThat(validate.iterator().next().getMessage()).isEqualTo("密码开头和结尾不能包含空字符串");
postParam.setPassword("123 ");
validate = validator.validate(postParam);
assertThat(validate).isNotNull();
assertThat(validate).hasSize(1);
assertThat(validate.iterator().next().getMessage()).isEqualTo("密码开头和结尾不能包含空字符串");
postParam.setPassword("");
validate = validator.validate(postParam);
assertThat(validate).isEmpty();
postParam.setPassword("123 hello");
validate = validator.validate(postParam);
assertThat(validate).isEmpty();
}
@Test @Test
public void convertToTest() { public void convertToTest() {
PostParam postParam = new PostParam(); PostParam postParam = new PostParam();

View File

@ -1,129 +0,0 @@
package run.halo.app.service.impl;
import static org.junit.jupiter.api.Assertions.assertEquals;
import com.fasterxml.jackson.core.JsonProcessingException;
import java.util.Set;
import org.apache.commons.lang3.StringUtils;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpSession;
import org.springframework.mock.web.MockServletContext;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import run.halo.app.cache.InMemoryCacheStore;
import run.halo.app.utils.JsonUtils;
/**
* @author guqing
* @date 2021-11-19
*/
public class AuthorizationServiceImplTest {
private AuthorizationServiceImpl authorizationService;
private InMemoryCacheStore inMemoryCacheStore;
@BeforeEach
public void setUp() {
inMemoryCacheStore = new InMemoryCacheStore();
authorizationService = new AuthorizationServiceImpl(inMemoryCacheStore);
}
@Test
public void deletePostAuthorizationTest() {
inMemoryCacheStore.clear();
RequestContextHolder.setRequestAttributes(mockRequestAttributes("1"));
authorizationService.postAuthorization(1);
authorizationService.postAuthorization(2);
Set<String> permissions = authorizationService.getAccessPermissionStore();
assertEquals("[POST:1, POST:2]", permissions.toString());
authorizationService.deletePostAuthorization(1);
Set<String> permissionsAfterDelete = authorizationService.getAccessPermissionStore();
assertEquals("[POST:2]", permissionsAfterDelete.toString());
RequestContextHolder.resetRequestAttributes();
inMemoryCacheStore.clear();
}
@Test
public void complexityOfDeletePostAuthorizationTest() {
inMemoryCacheStore.clear();
// simulate session of user 1
RequestContextHolder.setRequestAttributes(mockRequestAttributes("1"));
// user 1 accessed two encrypted posts
authorizationService.postAuthorization(1);
authorizationService.postAuthorization(2);
// simulate session of user 2
RequestContextHolder.setRequestAttributes(mockRequestAttributes("2"));
// user 2 accessed two encrypted posts
authorizationService.postAuthorization(2);
authorizationService.postAuthorization(3);
assertEquals(objectToJson(inMemoryCacheStore.toMap()),
"{\"ACCESS_PERMISSION: 2\":\"[\\\"POST:3\\\",\\\"POST:2\\\"]\","
+ "\"ACCESS_PERMISSION: 1\":\"[\\\"POST:1\\\",\\\"POST:2\\\"]\"}");
// simulate the admin user to change the post password
authorizationService.deletePostAuthorization(2);
assertEquals(objectToJson(inMemoryCacheStore.toMap()),
"{\"ACCESS_PERMISSION: 2\":\"[\\\"POST:3\\\"]\","
+ "\"ACCESS_PERMISSION: 1\":\"[\\\"POST:1\\\"]\"}");
RequestContextHolder.resetRequestAttributes();
inMemoryCacheStore.clear();
}
@Test
public void deleteCategoryAuthorizationTest() {
inMemoryCacheStore.clear();
// simulate session of user 1
RequestContextHolder.setRequestAttributes(mockRequestAttributes("1"));
// user 1 accessed two encrypted posts
authorizationService.categoryAuthorization(1);
authorizationService.categoryAuthorization(2);
// simulate session of user 2
RequestContextHolder.setRequestAttributes(mockRequestAttributes("2"));
// user 2 accessed two encrypted categories
authorizationService.categoryAuthorization(1);
authorizationService.categoryAuthorization(3);
assertEquals(objectToJson(inMemoryCacheStore.toMap()),
"{\"ACCESS_PERMISSION: 2\":\"[\\\"CATEGORY:1\\\",\\\"CATEGORY:3\\\"]\","
+ "\"ACCESS_PERMISSION: 1\":\"[\\\"CATEGORY:1\\\",\\\"CATEGORY:2\\\"]\"}");
// simulate the admin user to change the category password of No.1
authorizationService.deleteCategoryAuthorization(1);
assertEquals(objectToJson(inMemoryCacheStore.toMap()),
"{\"ACCESS_PERMISSION: 2\":\"[\\\"CATEGORY:3\\\"]\","
+ "\"ACCESS_PERMISSION: 1\":\"[\\\"CATEGORY:2\\\"]\"}");
RequestContextHolder.resetRequestAttributes();
inMemoryCacheStore.clear();
}
private ServletRequestAttributes mockRequestAttributes(String sessionId) {
MockHttpServletRequest request = new MockHttpServletRequest();
MockServletContext context = new MockServletContext();
MockHttpSession session = new MockHttpSession(context, sessionId);
request.setSession(session);
return new ServletRequestAttributes(request);
}
private String objectToJson(Object o) {
try {
return JsonUtils.objectToJson(o);
} catch (JsonProcessingException e) {
// ignore this
}
return StringUtils.EMPTY;
}
}