diff --git a/src/main/java/run/halo/app/config/HaloConfiguration.java b/src/main/java/run/halo/app/config/HaloConfiguration.java index eb67f0bf2..5cc5b1bed 100644 --- a/src/main/java/run/halo/app/config/HaloConfiguration.java +++ b/src/main/java/run/halo/app/config/HaloConfiguration.java @@ -3,6 +3,7 @@ package run.halo.app.config; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.web.client.RestTemplateBuilder; @@ -19,11 +20,13 @@ import run.halo.app.cache.StringCacheStore; import run.halo.app.config.properties.HaloProperties; import run.halo.app.filter.CorsFilter; import run.halo.app.filter.LogFilter; +import run.halo.app.model.enums.Mode; import run.halo.app.security.filter.AdminAuthenticationFilter; import run.halo.app.security.filter.ApiAuthenticationFilter; import run.halo.app.security.filter.ContentFilter; import run.halo.app.security.handler.ContentAuthenticationFailureHandler; import run.halo.app.security.handler.DefaultAuthenticationFailureHandler; +import run.halo.app.security.service.OneTimeTokenService; import run.halo.app.service.OptionService; import run.halo.app.service.UserService; import run.halo.app.utils.HaloUtils; @@ -116,8 +119,10 @@ public class HaloConfiguration { @Bean public FilterRegistrationBean contentFilter(HaloProperties haloProperties, OptionService optionService, - StringCacheStore cacheStore) { - ContentFilter contentFilter = new ContentFilter(haloProperties, optionService, cacheStore); + StringCacheStore cacheStore, + OneTimeTokenService oneTimeTokenService, + @Value("${spring.profiles.active:prod}") String activeProfile) { + ContentFilter contentFilter = new ContentFilter(haloProperties, optionService, cacheStore, oneTimeTokenService, Mode.valueFrom(activeProfile)); contentFilter.setFailureHandler(new ContentAuthenticationFailureHandler()); String adminPattern = HaloUtils.ensureBoth(haloProperties.getAdminPath(), "/") + "**"; @@ -142,8 +147,10 @@ public class HaloConfiguration { public FilterRegistrationBean apiAuthenticationFilter(HaloProperties haloProperties, ObjectMapper objectMapper, OptionService optionService, - StringCacheStore cacheStore) { - ApiAuthenticationFilter apiFilter = new ApiAuthenticationFilter(haloProperties, optionService, cacheStore); + StringCacheStore cacheStore, + OneTimeTokenService oneTimeTokenService, + @Value("${spring.profiles.active:prod}") String activeProfile) { + ApiAuthenticationFilter apiFilter = new ApiAuthenticationFilter(haloProperties, optionService, cacheStore, oneTimeTokenService, Mode.valueFrom(activeProfile)); apiFilter.addExcludeUrlPatterns( "/api/content/*/comments", "/api/content/**/comments/**", @@ -170,8 +177,11 @@ public class HaloConfiguration { UserService userService, HaloProperties haloProperties, ObjectMapper objectMapper, - OptionService optionService) { - AdminAuthenticationFilter adminAuthenticationFilter = new AdminAuthenticationFilter(cacheStore, userService, haloProperties, optionService); + OptionService optionService, + OneTimeTokenService oneTimeTokenService, + @Value("${spring.profiles.active:prod}") String activeProfile) { + AdminAuthenticationFilter adminAuthenticationFilter = new AdminAuthenticationFilter(cacheStore, userService, + haloProperties, optionService, oneTimeTokenService, Mode.valueFrom(activeProfile)); DefaultAuthenticationFailureHandler failureHandler = new DefaultAuthenticationFailureHandler(); failureHandler.setProductionEnv(haloProperties.isProductionEnv()); @@ -188,8 +198,7 @@ public class HaloConfiguration { "/api/admin/password/code", "/api/admin/password/reset" ); - adminAuthenticationFilter.setFailureHandler( - failureHandler); + adminAuthenticationFilter.setFailureHandler(failureHandler); FilterRegistrationBean authenticationFilter = new FilterRegistrationBean<>(); authenticationFilter.setFilter(adminAuthenticationFilter); diff --git a/src/main/java/run/halo/app/filter/CorsFilter.java b/src/main/java/run/halo/app/filter/CorsFilter.java index 51d653acc..77279f2fd 100644 --- a/src/main/java/run/halo/app/filter/CorsFilter.java +++ b/src/main/java/run/halo/app/filter/CorsFilter.java @@ -31,7 +31,10 @@ public class CorsFilter extends GenericFilterBean { HttpServletResponse httpServletResponse = (HttpServletResponse) response; // Set customized header - httpServletResponse.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, httpServletRequest.getHeader(HttpHeaders.ORIGIN)); + String originHeaderValue = httpServletRequest.getHeader(HttpHeaders.ORIGIN); + if (StringUtils.isNotBlank(originHeaderValue)) { + httpServletResponse.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, originHeaderValue); + } httpServletResponse.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS, ALLOW_HEADERS); httpServletResponse.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS, "GET, POST, PUT, DELETE, OPTIONS"); httpServletResponse.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS, "true"); diff --git a/src/main/java/run/halo/app/model/enums/Mode.java b/src/main/java/run/halo/app/model/enums/Mode.java index f9c8e32b8..6dfc2dcab 100644 --- a/src/main/java/run/halo/app/model/enums/Mode.java +++ b/src/main/java/run/halo/app/model/enums/Mode.java @@ -20,24 +20,19 @@ public enum Mode { * Get mode from value. * * @param value mode value - * @return runtime mode or null if the value is mismatch + * @return runtime mode */ - @Nullable @JsonCreator public static Mode valueFrom(@Nullable String value) { - if (StringUtils.isBlank(value) || "prod".equalsIgnoreCase(value)) { - return Mode.PRODUCTION; - } - - if ("dev".equalsIgnoreCase(value)) { + if (StringUtils.equalsIgnoreCase("dev", value)) { return Mode.DEVELOPMENT; } - if ("test".equalsIgnoreCase(value)) { + if (StringUtils.equalsIgnoreCase("test", value)) { return Mode.TEST; } - return null; + return PRODUCTION; } @JsonValue diff --git a/src/main/java/run/halo/app/model/support/HaloConst.java b/src/main/java/run/halo/app/model/support/HaloConst.java index d8f45327b..8a989afa1 100644 --- a/src/main/java/run/halo/app/model/support/HaloConst.java +++ b/src/main/java/run/halo/app/model/support/HaloConst.java @@ -125,7 +125,12 @@ public class HaloConst { * Content api token param name */ public final static String API_ACCESS_KEY_QUERY_NAME = "api_access_key"; - public final static Duration TEMP_TOKEN_EXPIRATION = Duration.ofDays(7); + + public final static String ONE_TIME_TOKEN_QUERY_NAME = "ott"; + + public final static String ONE_TIME_TOKEN_HEADER_NAME = "ott"; + + /** * user_session */ diff --git a/src/main/java/run/halo/app/security/filter/AbstractAuthenticationFilter.java b/src/main/java/run/halo/app/security/filter/AbstractAuthenticationFilter.java index 2423858fb..1a2e5048d 100644 --- a/src/main/java/run/halo/app/security/filter/AbstractAuthenticationFilter.java +++ b/src/main/java/run/halo/app/security/filter/AbstractAuthenticationFilter.java @@ -9,11 +9,16 @@ import org.springframework.util.Assert; import org.springframework.web.filter.OncePerRequestFilter; import run.halo.app.cache.StringCacheStore; import run.halo.app.config.properties.HaloProperties; +import run.halo.app.exception.BadRequestException; +import run.halo.app.exception.ForbiddenException; +import run.halo.app.exception.HaloException; import run.halo.app.exception.NotInstallException; +import run.halo.app.model.enums.Mode; import run.halo.app.model.properties.PrimaryProperties; import run.halo.app.security.context.SecurityContextHolder; import run.halo.app.security.handler.AuthenticationFailureHandler; import run.halo.app.security.handler.DefaultAuthenticationFailureHandler; +import run.halo.app.security.service.OneTimeTokenService; import run.halo.app.service.OptionService; import javax.servlet.FilterChain; @@ -26,6 +31,9 @@ import java.util.Collections; import java.util.HashSet; import java.util.Set; +import static run.halo.app.model.support.HaloConst.ONE_TIME_TOKEN_HEADER_NAME; +import static run.halo.app.model.support.HaloConst.ONE_TIME_TOKEN_QUERY_NAME; + /** * Abstract authentication filter. * @@ -43,18 +51,26 @@ public abstract class AbstractAuthenticationFilter extends OncePerRequestFilter protected final StringCacheStore cacheStore; - private AuthenticationFailureHandler failureHandler; + private OneTimeTokenService oneTimeTokenService; + + private final Mode mode; + + private volatile AuthenticationFailureHandler failureHandler; /** * Exclude url patterns. */ - private Set excludeUrlPatterns = new HashSet<>(2); + private Set excludeUrlPatterns = new HashSet<>(16); AbstractAuthenticationFilter(HaloProperties haloProperties, OptionService optionService, - StringCacheStore cacheStore) { + StringCacheStore cacheStore, + OneTimeTokenService oneTimeTokenService, + Mode mode) { this.haloProperties = haloProperties; this.optionService = optionService; this.cacheStore = cacheStore; + this.oneTimeTokenService = oneTimeTokenService; + this.mode = mode; antPathMatcher = new AntPathMatcher(); } @@ -74,7 +90,7 @@ public abstract class AbstractAuthenticationFilter extends OncePerRequestFilter protected boolean shouldNotFilter(HttpServletRequest request) { Assert.notNull(request, "Http servlet request must not be null"); - return excludeUrlPatterns.stream().anyMatch(p -> antPathMatcher.match(p, request.getServletPath())); + return excludeUrlPatterns.stream().anyMatch(p -> antPathMatcher.match(p, request.getRequestURI())); } /** @@ -115,7 +131,7 @@ public abstract class AbstractAuthenticationFilter extends OncePerRequestFilter * @return authentication failure handler */ @NonNull - protected AuthenticationFailureHandler getFailureHandler() { + private AuthenticationFailureHandler getFailureHandler() { if (failureHandler == null) { synchronized (this) { if (failureHandler == null) { @@ -135,7 +151,7 @@ public abstract class AbstractAuthenticationFilter extends OncePerRequestFilter * * @param failureHandler authentication failure handler */ - public void setFailureHandler(@NonNull AuthenticationFailureHandler failureHandler) { + public synchronized void setFailureHandler(@NonNull AuthenticationFailureHandler failureHandler) { Assert.notNull(failureHandler, "Authentication failure handler must not be null"); this.failureHandler = failureHandler; @@ -146,25 +162,70 @@ public abstract class AbstractAuthenticationFilter extends OncePerRequestFilter // Check whether the blog is installed or not Boolean isInstalled = optionService.getByPropertyOrDefault(PrimaryProperties.IS_INSTALLED, Boolean.class, false); - if (!isInstalled) { + if (!isInstalled && mode != Mode.TEST) { // If not installed getFailureHandler().onFailure(request, response, new NotInstallException("当前博客还没有初始化")); return; } - if (shouldNotFilter(request)) { - filterChain.doFilter(request, response); - return; - } - try { + // Check the one-time-token + if (isSufficientOneTimeToken(request)) { + filterChain.doFilter(request, response); + return; + } + // Do authenticate doAuthenticate(request, response, filterChain); + } catch (HaloException e) { + getFailureHandler().onFailure(request, response, e); } finally { SecurityContextHolder.clearContext(); } } + /** + * Check if the sufficient one-time token is set. + * + * @param request http servlet request + * @return true if sufficient; false otherwise + */ + private boolean isSufficientOneTimeToken(HttpServletRequest request) { + // Check the param + final String oneTimeToken = getTokenFromRequest(request, ONE_TIME_TOKEN_QUERY_NAME, ONE_TIME_TOKEN_HEADER_NAME); + + if (StringUtils.isBlank(oneTimeToken)) { + // If no one-time token is not provided, skip + return false; + } + + // Get allowed uri + String allowedUri = oneTimeTokenService.get(oneTimeToken) + .orElseThrow(() -> new BadRequestException("The one-time token does not exist").setErrorData(oneTimeToken)); + + // Get request uri + String requestUri = request.getRequestURI(); + + if (!StringUtils.equals(requestUri, allowedUri)) { + // If the request uri mismatches the allowed uri + // TODO using ant path matcher could be better + throw new ForbiddenException("The one-time token does not correspond the request uri").setErrorData(oneTimeToken); + } + + // Revoke the token before return + oneTimeTokenService.revoke(oneTimeToken); + + return true; + } + + /** + * Get token from http servlet request. + * + * @param request http servlet request must not be null + * @param tokenQueryName token query name must not be blank + * @param tokenHeaderName token header name must not be blank + * @return corresponding token + */ protected String getTokenFromRequest(@NonNull HttpServletRequest request, @NonNull String tokenQueryName, @NonNull String tokenHeaderName) { Assert.notNull(request, "Http servlet request must not be null"); Assert.hasText(tokenQueryName, "Token query name must not be blank"); @@ -176,7 +237,6 @@ public abstract class AbstractAuthenticationFilter extends OncePerRequestFilter // Get from param if (StringUtils.isBlank(accessKey)) { accessKey = request.getParameter(tokenQueryName); - log.debug("Got access key from parameter: [{}: {}]", tokenQueryName, accessKey); } else { log.debug("Got access key from header: [{}: {}]", tokenHeaderName, accessKey); diff --git a/src/main/java/run/halo/app/security/filter/AdminAuthenticationFilter.java b/src/main/java/run/halo/app/security/filter/AdminAuthenticationFilter.java index 143da643d..8d45befe9 100644 --- a/src/main/java/run/halo/app/security/filter/AdminAuthenticationFilter.java +++ b/src/main/java/run/halo/app/security/filter/AdminAuthenticationFilter.java @@ -7,9 +7,11 @@ import run.halo.app.cache.StringCacheStore; import run.halo.app.config.properties.HaloProperties; import run.halo.app.exception.AuthenticationException; import run.halo.app.model.entity.User; +import run.halo.app.model.enums.Mode; import run.halo.app.security.authentication.AuthenticationImpl; import run.halo.app.security.context.SecurityContextHolder; import run.halo.app.security.context.SecurityContextImpl; +import run.halo.app.security.service.OneTimeTokenService; import run.halo.app.security.support.UserDetail; import run.halo.app.security.util.SecurityUtils; import run.halo.app.service.OptionService; @@ -35,14 +37,15 @@ public class AdminAuthenticationFilter extends AbstractAuthenticationFilter { private final HaloProperties haloProperties; - private final UserService userService; public AdminAuthenticationFilter(StringCacheStore cacheStore, UserService userService, HaloProperties haloProperties, - OptionService optionService) { - super(haloProperties, optionService, cacheStore); + OptionService optionService, + OneTimeTokenService oneTimeTokenService, + Mode mode) { + super(haloProperties, optionService, cacheStore, oneTimeTokenService, mode); this.userService = userService; this.haloProperties = haloProperties; } @@ -64,16 +67,14 @@ public class AdminAuthenticationFilter extends AbstractAuthenticationFilter { String token = getTokenFromRequest(request); if (StringUtils.isBlank(token)) { - getFailureHandler().onFailure(request, response, new AuthenticationException("未登录,请登陆后访问")); - return; + throw new AuthenticationException("未登录,请登陆后访问"); } // Get user id from cache Optional optionalUserId = cacheStore.getAny(SecurityUtils.buildTokenAccessKey(token), Integer.class); if (!optionalUserId.isPresent()) { - getFailureHandler().onFailure(request, response, new AuthenticationException("Token 已过期或不存在").setErrorData(token)); - return; + throw new AuthenticationException("Token 已过期或不存在").setErrorData(token); } // Get the user diff --git a/src/main/java/run/halo/app/security/filter/ApiAuthenticationFilter.java b/src/main/java/run/halo/app/security/filter/ApiAuthenticationFilter.java index 8c4fe35b1..5ebe5f1c6 100644 --- a/src/main/java/run/halo/app/security/filter/ApiAuthenticationFilter.java +++ b/src/main/java/run/halo/app/security/filter/ApiAuthenticationFilter.java @@ -7,8 +7,10 @@ import run.halo.app.cache.StringCacheStore; import run.halo.app.config.properties.HaloProperties; import run.halo.app.exception.AuthenticationException; import run.halo.app.exception.ForbiddenException; +import run.halo.app.model.enums.Mode; import run.halo.app.model.properties.ApiProperties; import run.halo.app.model.properties.CommentProperties; +import run.halo.app.security.service.OneTimeTokenService; import run.halo.app.service.OptionService; import javax.servlet.FilterChain; @@ -33,8 +35,10 @@ public class ApiAuthenticationFilter extends AbstractAuthenticationFilter { public ApiAuthenticationFilter(HaloProperties haloProperties, OptionService optionService, - StringCacheStore cacheStore) { - super(haloProperties, optionService, cacheStore); + StringCacheStore cacheStore, + OneTimeTokenService oneTimeTokenService, + Mode mode) { + super(haloProperties, optionService, cacheStore, oneTimeTokenService, mode); this.optionService = optionService; } @@ -50,8 +54,7 @@ public class ApiAuthenticationFilter extends AbstractAuthenticationFilter { Boolean apiEnabled = optionService.getByPropertyOrDefault(ApiProperties.API_ENABLED, Boolean.class, false); if (!apiEnabled) { - getFailureHandler().onFailure(request, response, new ForbiddenException("API has been disabled by blogger currently")); - return; + throw new ForbiddenException("API has been disabled by blogger currently"); } // Get access key @@ -59,8 +62,7 @@ public class ApiAuthenticationFilter extends AbstractAuthenticationFilter { if (StringUtils.isBlank(accessKey)) { // If the access key is missing - getFailureHandler().onFailure(request, response, new AuthenticationException("Missing API access key")); - return; + throw new AuthenticationException("Missing API access key"); } // Get access key from option @@ -68,14 +70,12 @@ public class ApiAuthenticationFilter extends AbstractAuthenticationFilter { if (!optionalAccessKey.isPresent()) { // If the access key is not set - getFailureHandler().onFailure(request, response, new AuthenticationException("API access key hasn't been set by blogger")); - return; + throw new AuthenticationException("API access key hasn't been set by blogger"); } if (!StringUtils.equals(accessKey, optionalAccessKey.get())) { // If the access key is mismatch - getFailureHandler().onFailure(request, response, new AuthenticationException("API access key is mismatch")); - return; + throw new AuthenticationException("API access key is mismatch").setErrorData(accessKey); } // Do filter diff --git a/src/main/java/run/halo/app/security/filter/ContentFilter.java b/src/main/java/run/halo/app/security/filter/ContentFilter.java index 530941472..cce447f3d 100644 --- a/src/main/java/run/halo/app/security/filter/ContentFilter.java +++ b/src/main/java/run/halo/app/security/filter/ContentFilter.java @@ -2,6 +2,8 @@ package run.halo.app.security.filter; import run.halo.app.cache.StringCacheStore; import run.halo.app.config.properties.HaloProperties; +import run.halo.app.model.enums.Mode; +import run.halo.app.security.service.OneTimeTokenService; import run.halo.app.service.OptionService; import javax.servlet.FilterChain; @@ -20,8 +22,10 @@ public class ContentFilter extends AbstractAuthenticationFilter { public ContentFilter(HaloProperties haloProperties, OptionService optionService, - StringCacheStore cacheStore) { - super(haloProperties, optionService, cacheStore); + StringCacheStore cacheStore, + OneTimeTokenService oneTimeTokenService, + Mode mode) { + super(haloProperties, optionService, cacheStore, oneTimeTokenService, mode); } @Override diff --git a/src/main/java/run/halo/app/security/service/OneTimeTokenService.java b/src/main/java/run/halo/app/security/service/OneTimeTokenService.java new file mode 100644 index 000000000..1b25dce0e --- /dev/null +++ b/src/main/java/run/halo/app/security/service/OneTimeTokenService.java @@ -0,0 +1,38 @@ +package run.halo.app.security.service; + +import org.springframework.lang.NonNull; + +import java.util.Optional; + +/** + * One-time-token service interface. + * + * @author johnniang + */ +public interface OneTimeTokenService { + + /** + * Get the corresponding uri. + * + * @param oneTimeToken one-time token must not be null + * @return the corresponding uri + */ + @NonNull + Optional get(@NonNull String oneTimeToken); + + /** + * Create one time token. + * + * @param uri request uri. + * @return one time token. + */ + @NonNull + String create(@NonNull String uri); + + /** + * Revoke one time token. + * + * @param oneTimeToken one time token must not be null + */ + void revoke(@NonNull String oneTimeToken); +} diff --git a/src/main/java/run/halo/app/security/service/impl/OneTimeTokenServiceImpl.java b/src/main/java/run/halo/app/security/service/impl/OneTimeTokenServiceImpl.java new file mode 100644 index 000000000..125f1d800 --- /dev/null +++ b/src/main/java/run/halo/app/security/service/impl/OneTimeTokenServiceImpl.java @@ -0,0 +1,60 @@ +package run.halo.app.security.service.impl; + +import org.springframework.stereotype.Service; +import org.springframework.util.Assert; +import run.halo.app.cache.StringCacheStore; +import run.halo.app.security.service.OneTimeTokenService; +import run.halo.app.utils.HaloUtils; + +import java.util.Optional; +import java.util.concurrent.TimeUnit; + +/** + * One-time token service implementation. + * + * @author johnniang + */ +@Service +public class OneTimeTokenServiceImpl implements OneTimeTokenService { + + /** + * One-time token expired day. (unit: day) + */ + public static final int OTT_EXPIRED_DAY = 1; + + private final StringCacheStore cacheStore; + + public OneTimeTokenServiceImpl(StringCacheStore cacheStore) { + this.cacheStore = cacheStore; + } + + @Override + public Optional get(String oneTimeToken) { + Assert.hasText(oneTimeToken, "One-time token must not be blank"); + + // Get from cache store + return cacheStore.get(oneTimeToken); + } + + @Override + public String create(String uri) { + Assert.hasText(uri, "Request uri must not be blank"); + + // Generate ott + String oneTimeToken = HaloUtils.randomUUIDWithoutDash(); + + // Put ott along with request uri + cacheStore.put(oneTimeToken, uri, OTT_EXPIRED_DAY, TimeUnit.DAYS); + + // Return ott + return oneTimeToken; + } + + @Override + public void revoke(String oneTimeToken) { + Assert.hasText(oneTimeToken, "One-time token must not be blank"); + + // Delete the token + cacheStore.delete(oneTimeToken); + } +} diff --git a/src/main/java/run/halo/app/service/impl/BackupServiceImpl.java b/src/main/java/run/halo/app/service/impl/BackupServiceImpl.java index c88733553..4be161a85 100644 --- a/src/main/java/run/halo/app/service/impl/BackupServiceImpl.java +++ b/src/main/java/run/halo/app/service/impl/BackupServiceImpl.java @@ -14,7 +14,6 @@ import org.springframework.stereotype.Service; import org.springframework.util.Assert; import org.springframework.web.multipart.MultipartFile; import org.yaml.snakeyaml.Yaml; -import run.halo.app.cache.StringCacheStore; import run.halo.app.config.properties.HaloProperties; import run.halo.app.exception.NotFoundException; import run.halo.app.exception.ServiceException; @@ -23,6 +22,7 @@ import run.halo.app.model.dto.post.BasePostDetailDTO; import run.halo.app.model.entity.Post; import run.halo.app.model.entity.Tag; import run.halo.app.model.support.HaloConst; +import run.halo.app.security.service.OneTimeTokenService; import run.halo.app.service.BackupService; import run.halo.app.service.OptionService; import run.halo.app.service.PostService; @@ -54,34 +54,40 @@ import java.util.stream.Stream; @Slf4j public class BackupServiceImpl implements BackupService { - public static final String BACKUP_TOKEN_KEY_PREFIX = "backup-token-"; + private static final String BACKUP_RESOURCE_BASE_URI = "/api/admin/backups/halo"; + private static final String LINE_SEPARATOR = System.getProperty("line.separator"); + private final PostService postService; + private final PostTagService postTagService; + private final OptionService optionService; - private final StringCacheStore cacheStore; + + private final OneTimeTokenService oneTimeTokenService; + private final HaloProperties haloProperties; public BackupServiceImpl(PostService postService, PostTagService postTagService, OptionService optionService, - StringCacheStore stringCacheStore, + OneTimeTokenService oneTimeTokenService, HaloProperties haloProperties) { this.postService = postService; this.postTagService = postTagService; this.optionService = optionService; - this.cacheStore = stringCacheStore; + this.oneTimeTokenService = oneTimeTokenService; this.haloProperties = haloProperties; } /** * Sanitizes the specified file name. * - * @param unsanitized the specified file name + * @param unSanitized the specified file name * @return sanitized file name */ - public static String sanitizeFilename(final String unsanitized) { - return unsanitized. + public static String sanitizeFilename(final String unSanitized) { + return unSanitized. replaceAll("[^(a-zA-Z0-9\\u4e00-\\u9fa5\\.)]", ""). replaceAll("[\\?\\\\/:|<>\\*\\[\\]\\(\\)\\$%\\{\\}@~\\.]", ""). replaceAll("\\s", ""); @@ -291,11 +297,21 @@ public class BackupServiceImpl implements BackupService { * @param filename filename must not be blank * @return download url */ + @NonNull private String buildDownloadUrl(@NonNull String filename) { Assert.hasText(filename, "File name must not be blank"); // Composite http url - return HaloUtils.compositeHttpUrl(optionService.getBlogBaseUrl(), "api/admin/backups/halo", filename); + String backupUri = BACKUP_RESOURCE_BASE_URI + HaloUtils.URL_SEPARATOR + filename; + + // Get a one-time token + String oneTimeToken = oneTimeTokenService.create(backupUri); + + // Build full url + return HaloUtils.compositeHttpUrl(optionService.getBlogBaseUrl(), backupUri) + + "?" + + HaloConst.ONE_TIME_TOKEN_QUERY_NAME + + "=" + oneTimeToken; } } diff --git a/src/main/resources/application-test.yaml b/src/main/resources/application-test.yaml index dabe475b4..8bffad038 100755 --- a/src/main/resources/application-test.yaml +++ b/src/main/resources/application-test.yaml @@ -1,5 +1,5 @@ server: - port: 8090 + port: 18090 forward-headers-strategy: native compression: enabled: false @@ -59,4 +59,5 @@ logging: halo: doc-disabled: false + auth-enable: false workDir: ${user.home}/halo-test/ diff --git a/src/test/java/run/halo/app/security/OneTimeTokenTest.java b/src/test/java/run/halo/app/security/OneTimeTokenTest.java new file mode 100644 index 000000000..12bc7b54e --- /dev/null +++ b/src/test/java/run/halo/app/security/OneTimeTokenTest.java @@ -0,0 +1,51 @@ +package run.halo.app.security; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; +import run.halo.app.security.service.OneTimeTokenService; + +import static org.hamcrest.core.Is.is; +import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +@SpringBootTest(webEnvironment = RANDOM_PORT) +@ActiveProfiles("test") +@AutoConfigureMockMvc +class OneTimeTokenTest { + + @Autowired + MockMvc mvc; + + @Autowired + OneTimeTokenService oneTimeTokenService; + + static final String REQUEST_URI = "/api/admin/counts"; + + @Test + void provideNonExistOneTimeTokenTest() throws Exception { + mvc.perform(get(REQUEST_URI + "?ott={ott}", "one-time-token-value")) + .andDo(print()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8)) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.status", is(HttpStatus.BAD_REQUEST.value()))); + } + + @Test + void insertAnOneTimeTokenTest() throws Exception { + // Create ott + String ott = oneTimeTokenService.create(REQUEST_URI); + + mvc.perform(get(REQUEST_URI + "?ott={ott}", ott)) + .andDo(print()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()); + } +}