mirror of https://github.com/halo-dev/halo
Merge pull request #565 from JohnNiang/fix-backup-function
Fix backup function and create one-time token mechanismpull/571/head
commit
f35648ac75
|
@ -3,6 +3,7 @@ package run.halo.app.config;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
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.autoconfigure.condition.ConditionalOnMissingBean;
|
||||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||||
import org.springframework.boot.web.client.RestTemplateBuilder;
|
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.config.properties.HaloProperties;
|
||||||
import run.halo.app.filter.CorsFilter;
|
import run.halo.app.filter.CorsFilter;
|
||||||
import run.halo.app.filter.LogFilter;
|
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.AdminAuthenticationFilter;
|
||||||
import run.halo.app.security.filter.ApiAuthenticationFilter;
|
import run.halo.app.security.filter.ApiAuthenticationFilter;
|
||||||
import run.halo.app.security.filter.ContentFilter;
|
import run.halo.app.security.filter.ContentFilter;
|
||||||
import run.halo.app.security.handler.ContentAuthenticationFailureHandler;
|
import run.halo.app.security.handler.ContentAuthenticationFailureHandler;
|
||||||
import run.halo.app.security.handler.DefaultAuthenticationFailureHandler;
|
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.OptionService;
|
||||||
import run.halo.app.service.UserService;
|
import run.halo.app.service.UserService;
|
||||||
import run.halo.app.utils.HaloUtils;
|
import run.halo.app.utils.HaloUtils;
|
||||||
|
@ -116,8 +119,10 @@ public class HaloConfiguration {
|
||||||
@Bean
|
@Bean
|
||||||
public FilterRegistrationBean<ContentFilter> contentFilter(HaloProperties haloProperties,
|
public FilterRegistrationBean<ContentFilter> contentFilter(HaloProperties haloProperties,
|
||||||
OptionService optionService,
|
OptionService optionService,
|
||||||
StringCacheStore cacheStore) {
|
StringCacheStore cacheStore,
|
||||||
ContentFilter contentFilter = new ContentFilter(haloProperties, optionService, 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());
|
contentFilter.setFailureHandler(new ContentAuthenticationFailureHandler());
|
||||||
|
|
||||||
String adminPattern = HaloUtils.ensureBoth(haloProperties.getAdminPath(), "/") + "**";
|
String adminPattern = HaloUtils.ensureBoth(haloProperties.getAdminPath(), "/") + "**";
|
||||||
|
@ -142,8 +147,10 @@ public class HaloConfiguration {
|
||||||
public FilterRegistrationBean<ApiAuthenticationFilter> apiAuthenticationFilter(HaloProperties haloProperties,
|
public FilterRegistrationBean<ApiAuthenticationFilter> apiAuthenticationFilter(HaloProperties haloProperties,
|
||||||
ObjectMapper objectMapper,
|
ObjectMapper objectMapper,
|
||||||
OptionService optionService,
|
OptionService optionService,
|
||||||
StringCacheStore cacheStore) {
|
StringCacheStore cacheStore,
|
||||||
ApiAuthenticationFilter apiFilter = new ApiAuthenticationFilter(haloProperties, optionService, cacheStore);
|
OneTimeTokenService oneTimeTokenService,
|
||||||
|
@Value("${spring.profiles.active:prod}") String activeProfile) {
|
||||||
|
ApiAuthenticationFilter apiFilter = new ApiAuthenticationFilter(haloProperties, optionService, cacheStore, oneTimeTokenService, Mode.valueFrom(activeProfile));
|
||||||
apiFilter.addExcludeUrlPatterns(
|
apiFilter.addExcludeUrlPatterns(
|
||||||
"/api/content/*/comments",
|
"/api/content/*/comments",
|
||||||
"/api/content/**/comments/**",
|
"/api/content/**/comments/**",
|
||||||
|
@ -170,8 +177,11 @@ public class HaloConfiguration {
|
||||||
UserService userService,
|
UserService userService,
|
||||||
HaloProperties haloProperties,
|
HaloProperties haloProperties,
|
||||||
ObjectMapper objectMapper,
|
ObjectMapper objectMapper,
|
||||||
OptionService optionService) {
|
OptionService optionService,
|
||||||
AdminAuthenticationFilter adminAuthenticationFilter = new AdminAuthenticationFilter(cacheStore, userService, haloProperties, 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();
|
DefaultAuthenticationFailureHandler failureHandler = new DefaultAuthenticationFailureHandler();
|
||||||
failureHandler.setProductionEnv(haloProperties.isProductionEnv());
|
failureHandler.setProductionEnv(haloProperties.isProductionEnv());
|
||||||
|
@ -188,8 +198,7 @@ public class HaloConfiguration {
|
||||||
"/api/admin/password/code",
|
"/api/admin/password/code",
|
||||||
"/api/admin/password/reset"
|
"/api/admin/password/reset"
|
||||||
);
|
);
|
||||||
adminAuthenticationFilter.setFailureHandler(
|
adminAuthenticationFilter.setFailureHandler(failureHandler);
|
||||||
failureHandler);
|
|
||||||
|
|
||||||
FilterRegistrationBean<AdminAuthenticationFilter> authenticationFilter = new FilterRegistrationBean<>();
|
FilterRegistrationBean<AdminAuthenticationFilter> authenticationFilter = new FilterRegistrationBean<>();
|
||||||
authenticationFilter.setFilter(adminAuthenticationFilter);
|
authenticationFilter.setFilter(adminAuthenticationFilter);
|
||||||
|
|
|
@ -68,10 +68,4 @@ public class HaloProperties {
|
||||||
*/
|
*/
|
||||||
private String cache = "memory";
|
private String cache = "memory";
|
||||||
|
|
||||||
|
|
||||||
public HaloProperties() throws IOException {
|
|
||||||
// Create work directory if not exist
|
|
||||||
Files.createDirectories(Paths.get(workDir));
|
|
||||||
Files.createDirectories(Paths.get(backupDir));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -68,6 +68,7 @@ public class BackupController {
|
||||||
contentType = request.getServletContext().getMimeType(backupResource.getFile().getAbsolutePath());
|
contentType = request.getServletContext().getMimeType(backupResource.getFile().getAbsolutePath());
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
log.warn("Could not determine file type", e);
|
log.warn("Could not determine file type", e);
|
||||||
|
// Ignore this error
|
||||||
}
|
}
|
||||||
|
|
||||||
return ResponseEntity.ok()
|
return ResponseEntity.ok()
|
||||||
|
|
|
@ -31,7 +31,10 @@ public class CorsFilter extends GenericFilterBean {
|
||||||
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
|
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
|
||||||
|
|
||||||
// Set customized header
|
// 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_HEADERS, ALLOW_HEADERS);
|
||||||
httpServletResponse.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS, "GET, POST, PUT, DELETE, OPTIONS");
|
httpServletResponse.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS, "GET, POST, PUT, DELETE, OPTIONS");
|
||||||
httpServletResponse.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS, "true");
|
httpServletResponse.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS, "true");
|
||||||
|
|
|
@ -57,6 +57,7 @@ public class StartedListener implements ApplicationListener<ApplicationStartedEv
|
||||||
public void onApplicationEvent(ApplicationStartedEvent event) {
|
public void onApplicationEvent(ApplicationStartedEvent event) {
|
||||||
this.migrate();
|
this.migrate();
|
||||||
this.initThemes();
|
this.initThemes();
|
||||||
|
this.initDirectory();
|
||||||
this.printStartInfo();
|
this.printStartInfo();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -140,4 +141,24 @@ public class StartedListener implements ApplicationListener<ApplicationStartedEv
|
||||||
|
|
||||||
return fileSystem;
|
return fileSystem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void initDirectory() {
|
||||||
|
Path workPath = Paths.get(haloProperties.getWorkDir());
|
||||||
|
Path backupPath = Paths.get(haloProperties.getBackupDir());
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (Files.notExists(workPath)) {
|
||||||
|
Files.createDirectories(workPath);
|
||||||
|
log.info("Created work directory: [{}]", workPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Files.notExists(backupPath)) {
|
||||||
|
Files.createDirectories(backupPath);
|
||||||
|
log.info("Created backup directory: [{}]", backupPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (IOException ie) {
|
||||||
|
throw new RuntimeException("Failed to initialize directories", ie);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,24 +20,19 @@ public enum Mode {
|
||||||
* Get mode from value.
|
* Get mode from value.
|
||||||
*
|
*
|
||||||
* @param value mode value
|
* @param value mode value
|
||||||
* @return runtime mode or null if the value is mismatch
|
* @return runtime mode
|
||||||
*/
|
*/
|
||||||
@Nullable
|
|
||||||
@JsonCreator
|
@JsonCreator
|
||||||
public static Mode valueFrom(@Nullable String value) {
|
public static Mode valueFrom(@Nullable String value) {
|
||||||
if (StringUtils.isBlank(value) || "prod".equalsIgnoreCase(value)) {
|
if (StringUtils.equalsIgnoreCase("dev", value)) {
|
||||||
return Mode.PRODUCTION;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ("dev".equalsIgnoreCase(value)) {
|
|
||||||
return Mode.DEVELOPMENT;
|
return Mode.DEVELOPMENT;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ("test".equalsIgnoreCase(value)) {
|
if (StringUtils.equalsIgnoreCase("test", value)) {
|
||||||
return Mode.TEST;
|
return Mode.TEST;
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return PRODUCTION;
|
||||||
}
|
}
|
||||||
|
|
||||||
@JsonValue
|
@JsonValue
|
||||||
|
|
|
@ -125,7 +125,12 @@ public class HaloConst {
|
||||||
* Content api token param name
|
* Content api token param name
|
||||||
*/
|
*/
|
||||||
public final static String API_ACCESS_KEY_QUERY_NAME = "api_access_key";
|
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
|
* user_session
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -9,14 +9,16 @@ import org.springframework.util.Assert;
|
||||||
import org.springframework.web.filter.OncePerRequestFilter;
|
import org.springframework.web.filter.OncePerRequestFilter;
|
||||||
import run.halo.app.cache.StringCacheStore;
|
import run.halo.app.cache.StringCacheStore;
|
||||||
import run.halo.app.config.properties.HaloProperties;
|
import run.halo.app.config.properties.HaloProperties;
|
||||||
|
import run.halo.app.exception.BadRequestException;
|
||||||
import run.halo.app.exception.ForbiddenException;
|
import run.halo.app.exception.ForbiddenException;
|
||||||
|
import run.halo.app.exception.HaloException;
|
||||||
import run.halo.app.exception.NotInstallException;
|
import run.halo.app.exception.NotInstallException;
|
||||||
|
import run.halo.app.model.enums.Mode;
|
||||||
import run.halo.app.model.properties.PrimaryProperties;
|
import run.halo.app.model.properties.PrimaryProperties;
|
||||||
import run.halo.app.model.support.HaloConst;
|
|
||||||
import run.halo.app.security.context.SecurityContextHolder;
|
import run.halo.app.security.context.SecurityContextHolder;
|
||||||
import run.halo.app.security.handler.AuthenticationFailureHandler;
|
import run.halo.app.security.handler.AuthenticationFailureHandler;
|
||||||
import run.halo.app.security.handler.DefaultAuthenticationFailureHandler;
|
import run.halo.app.security.handler.DefaultAuthenticationFailureHandler;
|
||||||
import run.halo.app.security.util.SecurityUtils;
|
import run.halo.app.security.service.OneTimeTokenService;
|
||||||
import run.halo.app.service.OptionService;
|
import run.halo.app.service.OptionService;
|
||||||
|
|
||||||
import javax.servlet.FilterChain;
|
import javax.servlet.FilterChain;
|
||||||
|
@ -24,7 +26,13 @@ import javax.servlet.ServletException;
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import javax.servlet.http.HttpServletRequest;
|
||||||
import javax.servlet.http.HttpServletResponse;
|
import javax.servlet.http.HttpServletResponse;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.*;
|
import java.util.Collection;
|
||||||
|
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.
|
* Abstract authentication filter.
|
||||||
|
@ -43,18 +51,26 @@ public abstract class AbstractAuthenticationFilter extends OncePerRequestFilter
|
||||||
|
|
||||||
protected final StringCacheStore cacheStore;
|
protected final StringCacheStore cacheStore;
|
||||||
|
|
||||||
private AuthenticationFailureHandler failureHandler;
|
private OneTimeTokenService oneTimeTokenService;
|
||||||
|
|
||||||
|
private final Mode mode;
|
||||||
|
|
||||||
|
private volatile AuthenticationFailureHandler failureHandler;
|
||||||
/**
|
/**
|
||||||
* Exclude url patterns.
|
* Exclude url patterns.
|
||||||
*/
|
*/
|
||||||
private Set<String> excludeUrlPatterns = new HashSet<>(2);
|
private Set<String> excludeUrlPatterns = new HashSet<>(16);
|
||||||
|
|
||||||
AbstractAuthenticationFilter(HaloProperties haloProperties,
|
AbstractAuthenticationFilter(HaloProperties haloProperties,
|
||||||
OptionService optionService,
|
OptionService optionService,
|
||||||
StringCacheStore cacheStore) {
|
StringCacheStore cacheStore,
|
||||||
|
OneTimeTokenService oneTimeTokenService,
|
||||||
|
Mode mode) {
|
||||||
this.haloProperties = haloProperties;
|
this.haloProperties = haloProperties;
|
||||||
this.optionService = optionService;
|
this.optionService = optionService;
|
||||||
this.cacheStore = cacheStore;
|
this.cacheStore = cacheStore;
|
||||||
|
this.oneTimeTokenService = oneTimeTokenService;
|
||||||
|
this.mode = mode;
|
||||||
|
|
||||||
antPathMatcher = new AntPathMatcher();
|
antPathMatcher = new AntPathMatcher();
|
||||||
}
|
}
|
||||||
|
@ -74,7 +90,7 @@ public abstract class AbstractAuthenticationFilter extends OncePerRequestFilter
|
||||||
protected boolean shouldNotFilter(HttpServletRequest request) {
|
protected boolean shouldNotFilter(HttpServletRequest request) {
|
||||||
Assert.notNull(request, "Http servlet request must not be null");
|
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
|
* @return authentication failure handler
|
||||||
*/
|
*/
|
||||||
@NonNull
|
@NonNull
|
||||||
protected AuthenticationFailureHandler getFailureHandler() {
|
private AuthenticationFailureHandler getFailureHandler() {
|
||||||
if (failureHandler == null) {
|
if (failureHandler == null) {
|
||||||
synchronized (this) {
|
synchronized (this) {
|
||||||
if (failureHandler == null) {
|
if (failureHandler == null) {
|
||||||
|
@ -135,7 +151,7 @@ public abstract class AbstractAuthenticationFilter extends OncePerRequestFilter
|
||||||
*
|
*
|
||||||
* @param failureHandler authentication failure handler
|
* @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");
|
Assert.notNull(failureHandler, "Authentication failure handler must not be null");
|
||||||
|
|
||||||
this.failureHandler = failureHandler;
|
this.failureHandler = failureHandler;
|
||||||
|
@ -146,65 +162,71 @@ public abstract class AbstractAuthenticationFilter extends OncePerRequestFilter
|
||||||
// Check whether the blog is installed or not
|
// Check whether the blog is installed or not
|
||||||
Boolean isInstalled = optionService.getByPropertyOrDefault(PrimaryProperties.IS_INSTALLED, Boolean.class, false);
|
Boolean isInstalled = optionService.getByPropertyOrDefault(PrimaryProperties.IS_INSTALLED, Boolean.class, false);
|
||||||
|
|
||||||
if (!isInstalled) {
|
if (!isInstalled && mode != Mode.TEST) {
|
||||||
// If not installed
|
// If not installed
|
||||||
getFailureHandler().onFailure(request, response, new NotInstallException("当前博客还没有初始化"));
|
getFailureHandler().onFailure(request, response, new NotInstallException("当前博客还没有初始化"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (shouldNotFilter(request)) {
|
|
||||||
filterChain.doFilter(request, response);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (checkForTempToken(request)) {
|
|
||||||
filterChain.doFilter(request, response);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Check the one-time-token
|
||||||
|
if (isSufficientOneTimeToken(request)) {
|
||||||
|
filterChain.doFilter(request, response);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Do authenticate
|
// Do authenticate
|
||||||
doAuthenticate(request, response, filterChain);
|
doAuthenticate(request, response, filterChain);
|
||||||
|
} catch (HaloException e) {
|
||||||
|
getFailureHandler().onFailure(request, response, e);
|
||||||
} finally {
|
} finally {
|
||||||
SecurityContextHolder.clearContext();
|
SecurityContextHolder.clearContext();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean checkForTempToken(HttpServletRequest request) {
|
/**
|
||||||
// Get token from request
|
* Check if the sufficient one-time token is set.
|
||||||
String tempToken = getTokenFromRequest(request, HaloConst.TEMP_TOKEN, HaloConst.TEMP_TOKEN);
|
*
|
||||||
|
* @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.isEmpty(tempToken)) {
|
if (StringUtils.isBlank(oneTimeToken)) {
|
||||||
|
// If no one-time token is not provided, skip
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
String tempTokenKey = SecurityUtils.buildTempTokenKey(tempToken);
|
// Get allowed uri
|
||||||
// Check the token
|
String allowedUri = oneTimeTokenService.get(oneTimeToken)
|
||||||
Optional<Integer> tokenCountOptional = cacheStore.getAny(tempTokenKey, Integer.class);
|
.orElseThrow(() -> new BadRequestException("The one-time token does not exist").setErrorData(oneTimeToken));
|
||||||
|
|
||||||
if (!tokenCountOptional.isPresent()) {
|
// Get request uri
|
||||||
// If the token is not found
|
String requestUri = request.getRequestURI();
|
||||||
throw new ForbiddenException("The temporary token has been expired").setErrorData(tempToken);
|
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info("Got valid temp token: [{}]", tempToken);
|
// Revoke the token before return
|
||||||
|
oneTimeTokenService.revoke(oneTimeToken);
|
||||||
int count = tokenCountOptional.get();
|
|
||||||
// TODO May cause unsafe thread, fixing next time
|
|
||||||
// Count down
|
|
||||||
count--;
|
|
||||||
if (count <= 0) {
|
|
||||||
// If count is less than 0, then clear this temp token
|
|
||||||
cacheStore.delete(tempTokenKey);
|
|
||||||
} else {
|
|
||||||
// Put the less count
|
|
||||||
cacheStore.put(tempTokenKey, String.valueOf(count));
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
String getTokenFromRequest(@NonNull HttpServletRequest request, @NonNull String tokenQueryName, @NonNull String tokenHeaderName) {
|
/**
|
||||||
|
* 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.notNull(request, "Http servlet request must not be null");
|
||||||
Assert.hasText(tokenQueryName, "Token query name must not be blank");
|
Assert.hasText(tokenQueryName, "Token query name must not be blank");
|
||||||
Assert.hasText(tokenHeaderName, "Token header name must not be blank");
|
Assert.hasText(tokenHeaderName, "Token header name must not be blank");
|
||||||
|
@ -215,7 +237,6 @@ public abstract class AbstractAuthenticationFilter extends OncePerRequestFilter
|
||||||
// Get from param
|
// Get from param
|
||||||
if (StringUtils.isBlank(accessKey)) {
|
if (StringUtils.isBlank(accessKey)) {
|
||||||
accessKey = request.getParameter(tokenQueryName);
|
accessKey = request.getParameter(tokenQueryName);
|
||||||
|
|
||||||
log.debug("Got access key from parameter: [{}: {}]", tokenQueryName, accessKey);
|
log.debug("Got access key from parameter: [{}: {}]", tokenQueryName, accessKey);
|
||||||
} else {
|
} else {
|
||||||
log.debug("Got access key from header: [{}: {}]", tokenHeaderName, accessKey);
|
log.debug("Got access key from header: [{}: {}]", tokenHeaderName, accessKey);
|
||||||
|
|
|
@ -7,9 +7,11 @@ import run.halo.app.cache.StringCacheStore;
|
||||||
import run.halo.app.config.properties.HaloProperties;
|
import run.halo.app.config.properties.HaloProperties;
|
||||||
import run.halo.app.exception.AuthenticationException;
|
import run.halo.app.exception.AuthenticationException;
|
||||||
import run.halo.app.model.entity.User;
|
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.authentication.AuthenticationImpl;
|
||||||
import run.halo.app.security.context.SecurityContextHolder;
|
import run.halo.app.security.context.SecurityContextHolder;
|
||||||
import run.halo.app.security.context.SecurityContextImpl;
|
import run.halo.app.security.context.SecurityContextImpl;
|
||||||
|
import run.halo.app.security.service.OneTimeTokenService;
|
||||||
import run.halo.app.security.support.UserDetail;
|
import run.halo.app.security.support.UserDetail;
|
||||||
import run.halo.app.security.util.SecurityUtils;
|
import run.halo.app.security.util.SecurityUtils;
|
||||||
import run.halo.app.service.OptionService;
|
import run.halo.app.service.OptionService;
|
||||||
|
@ -35,14 +37,15 @@ public class AdminAuthenticationFilter extends AbstractAuthenticationFilter {
|
||||||
|
|
||||||
private final HaloProperties haloProperties;
|
private final HaloProperties haloProperties;
|
||||||
|
|
||||||
|
|
||||||
private final UserService userService;
|
private final UserService userService;
|
||||||
|
|
||||||
public AdminAuthenticationFilter(StringCacheStore cacheStore,
|
public AdminAuthenticationFilter(StringCacheStore cacheStore,
|
||||||
UserService userService,
|
UserService userService,
|
||||||
HaloProperties haloProperties,
|
HaloProperties haloProperties,
|
||||||
OptionService optionService) {
|
OptionService optionService,
|
||||||
super(haloProperties, optionService, cacheStore);
|
OneTimeTokenService oneTimeTokenService,
|
||||||
|
Mode mode) {
|
||||||
|
super(haloProperties, optionService, cacheStore, oneTimeTokenService, mode);
|
||||||
this.userService = userService;
|
this.userService = userService;
|
||||||
this.haloProperties = haloProperties;
|
this.haloProperties = haloProperties;
|
||||||
}
|
}
|
||||||
|
@ -64,16 +67,14 @@ public class AdminAuthenticationFilter extends AbstractAuthenticationFilter {
|
||||||
String token = getTokenFromRequest(request);
|
String token = getTokenFromRequest(request);
|
||||||
|
|
||||||
if (StringUtils.isBlank(token)) {
|
if (StringUtils.isBlank(token)) {
|
||||||
getFailureHandler().onFailure(request, response, new AuthenticationException("未登录,请登陆后访问"));
|
throw new AuthenticationException("未登录,请登陆后访问");
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get user id from cache
|
// Get user id from cache
|
||||||
Optional<Integer> optionalUserId = cacheStore.getAny(SecurityUtils.buildTokenAccessKey(token), Integer.class);
|
Optional<Integer> optionalUserId = cacheStore.getAny(SecurityUtils.buildTokenAccessKey(token), Integer.class);
|
||||||
|
|
||||||
if (!optionalUserId.isPresent()) {
|
if (!optionalUserId.isPresent()) {
|
||||||
getFailureHandler().onFailure(request, response, new AuthenticationException("Token 已过期或不存在").setErrorData(token));
|
throw new AuthenticationException("Token 已过期或不存在").setErrorData(token);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the user
|
// Get the user
|
||||||
|
|
|
@ -7,8 +7,10 @@ import run.halo.app.cache.StringCacheStore;
|
||||||
import run.halo.app.config.properties.HaloProperties;
|
import run.halo.app.config.properties.HaloProperties;
|
||||||
import run.halo.app.exception.AuthenticationException;
|
import run.halo.app.exception.AuthenticationException;
|
||||||
import run.halo.app.exception.ForbiddenException;
|
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.ApiProperties;
|
||||||
import run.halo.app.model.properties.CommentProperties;
|
import run.halo.app.model.properties.CommentProperties;
|
||||||
|
import run.halo.app.security.service.OneTimeTokenService;
|
||||||
import run.halo.app.service.OptionService;
|
import run.halo.app.service.OptionService;
|
||||||
|
|
||||||
import javax.servlet.FilterChain;
|
import javax.servlet.FilterChain;
|
||||||
|
@ -33,8 +35,10 @@ public class ApiAuthenticationFilter extends AbstractAuthenticationFilter {
|
||||||
|
|
||||||
public ApiAuthenticationFilter(HaloProperties haloProperties,
|
public ApiAuthenticationFilter(HaloProperties haloProperties,
|
||||||
OptionService optionService,
|
OptionService optionService,
|
||||||
StringCacheStore cacheStore) {
|
StringCacheStore cacheStore,
|
||||||
super(haloProperties, optionService, cacheStore);
|
OneTimeTokenService oneTimeTokenService,
|
||||||
|
Mode mode) {
|
||||||
|
super(haloProperties, optionService, cacheStore, oneTimeTokenService, mode);
|
||||||
this.optionService = optionService;
|
this.optionService = optionService;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -50,8 +54,7 @@ public class ApiAuthenticationFilter extends AbstractAuthenticationFilter {
|
||||||
Boolean apiEnabled = optionService.getByPropertyOrDefault(ApiProperties.API_ENABLED, Boolean.class, false);
|
Boolean apiEnabled = optionService.getByPropertyOrDefault(ApiProperties.API_ENABLED, Boolean.class, false);
|
||||||
|
|
||||||
if (!apiEnabled) {
|
if (!apiEnabled) {
|
||||||
getFailureHandler().onFailure(request, response, new ForbiddenException("API has been disabled by blogger currently"));
|
throw new ForbiddenException("API has been disabled by blogger currently");
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get access key
|
// Get access key
|
||||||
|
@ -59,8 +62,7 @@ public class ApiAuthenticationFilter extends AbstractAuthenticationFilter {
|
||||||
|
|
||||||
if (StringUtils.isBlank(accessKey)) {
|
if (StringUtils.isBlank(accessKey)) {
|
||||||
// If the access key is missing
|
// If the access key is missing
|
||||||
getFailureHandler().onFailure(request, response, new AuthenticationException("Missing API access key"));
|
throw new AuthenticationException("Missing API access key");
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get access key from option
|
// Get access key from option
|
||||||
|
@ -68,14 +70,12 @@ public class ApiAuthenticationFilter extends AbstractAuthenticationFilter {
|
||||||
|
|
||||||
if (!optionalAccessKey.isPresent()) {
|
if (!optionalAccessKey.isPresent()) {
|
||||||
// If the access key is not set
|
// If the access key is not set
|
||||||
getFailureHandler().onFailure(request, response, new AuthenticationException("API access key hasn't been set by blogger"));
|
throw new AuthenticationException("API access key hasn't been set by blogger");
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!StringUtils.equals(accessKey, optionalAccessKey.get())) {
|
if (!StringUtils.equals(accessKey, optionalAccessKey.get())) {
|
||||||
// If the access key is mismatch
|
// If the access key is mismatch
|
||||||
getFailureHandler().onFailure(request, response, new AuthenticationException("API access key is mismatch"));
|
throw new AuthenticationException("API access key is mismatch").setErrorData(accessKey);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Do filter
|
// Do filter
|
||||||
|
|
|
@ -2,6 +2,8 @@ package run.halo.app.security.filter;
|
||||||
|
|
||||||
import run.halo.app.cache.StringCacheStore;
|
import run.halo.app.cache.StringCacheStore;
|
||||||
import run.halo.app.config.properties.HaloProperties;
|
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 run.halo.app.service.OptionService;
|
||||||
|
|
||||||
import javax.servlet.FilterChain;
|
import javax.servlet.FilterChain;
|
||||||
|
@ -20,8 +22,10 @@ public class ContentFilter extends AbstractAuthenticationFilter {
|
||||||
|
|
||||||
public ContentFilter(HaloProperties haloProperties,
|
public ContentFilter(HaloProperties haloProperties,
|
||||||
OptionService optionService,
|
OptionService optionService,
|
||||||
StringCacheStore cacheStore) {
|
StringCacheStore cacheStore,
|
||||||
super(haloProperties, optionService, cacheStore);
|
OneTimeTokenService oneTimeTokenService,
|
||||||
|
Mode mode) {
|
||||||
|
super(haloProperties, optionService, cacheStore, oneTimeTokenService, mode);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -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<String> 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);
|
||||||
|
}
|
|
@ -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<String> 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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -26,9 +26,6 @@ public class SecurityUtils {
|
||||||
|
|
||||||
private final static String REFRESH_TOKEN_CACHE_PREFIX = "halo.admin.refresh_token.";
|
private final static String REFRESH_TOKEN_CACHE_PREFIX = "halo.admin.refresh_token.";
|
||||||
|
|
||||||
private final static String TEMP_TOKEN_CACHE_PREFIX = "halo.temp.token.";
|
|
||||||
|
|
||||||
|
|
||||||
private SecurityUtils() {
|
private SecurityUtils() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -60,10 +57,4 @@ public class SecurityUtils {
|
||||||
return TOKEN_REFRESH_CACHE_PREFIX + refreshToken;
|
return TOKEN_REFRESH_CACHE_PREFIX + refreshToken;
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
|
||||||
public static String buildTempTokenKey(@NonNull String tempToken) {
|
|
||||||
Assert.hasText(tempToken, "Temporary token must not be blank");
|
|
||||||
|
|
||||||
return TEMP_TOKEN_CACHE_PREFIX + tempToken;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,6 @@ import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.commons.io.FileUtils;
|
import org.apache.commons.io.FileUtils;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.apache.commons.lang3.time.DateFormatUtils;
|
import org.apache.commons.lang3.time.DateFormatUtils;
|
||||||
import org.apache.http.client.utils.URIBuilder;
|
|
||||||
import org.json.JSONObject;
|
import org.json.JSONObject;
|
||||||
import org.springframework.core.io.Resource;
|
import org.springframework.core.io.Resource;
|
||||||
import org.springframework.core.io.UrlResource;
|
import org.springframework.core.io.UrlResource;
|
||||||
|
@ -15,7 +14,6 @@ import org.springframework.stereotype.Service;
|
||||||
import org.springframework.util.Assert;
|
import org.springframework.util.Assert;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
import org.yaml.snakeyaml.Yaml;
|
import org.yaml.snakeyaml.Yaml;
|
||||||
import run.halo.app.cache.StringCacheStore;
|
|
||||||
import run.halo.app.config.properties.HaloProperties;
|
import run.halo.app.config.properties.HaloProperties;
|
||||||
import run.halo.app.exception.NotFoundException;
|
import run.halo.app.exception.NotFoundException;
|
||||||
import run.halo.app.exception.ServiceException;
|
import run.halo.app.exception.ServiceException;
|
||||||
|
@ -24,7 +22,7 @@ import run.halo.app.model.dto.post.BasePostDetailDTO;
|
||||||
import run.halo.app.model.entity.Post;
|
import run.halo.app.model.entity.Post;
|
||||||
import run.halo.app.model.entity.Tag;
|
import run.halo.app.model.entity.Tag;
|
||||||
import run.halo.app.model.support.HaloConst;
|
import run.halo.app.model.support.HaloConst;
|
||||||
import run.halo.app.security.util.SecurityUtils;
|
import run.halo.app.security.service.OneTimeTokenService;
|
||||||
import run.halo.app.service.BackupService;
|
import run.halo.app.service.BackupService;
|
||||||
import run.halo.app.service.OptionService;
|
import run.halo.app.service.OptionService;
|
||||||
import run.halo.app.service.PostService;
|
import run.halo.app.service.PostService;
|
||||||
|
@ -34,7 +32,6 @@ import run.halo.app.utils.HaloUtils;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.MalformedURLException;
|
import java.net.MalformedURLException;
|
||||||
import java.net.URISyntaxException;
|
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.NoSuchFileException;
|
import java.nio.file.NoSuchFileException;
|
||||||
|
@ -42,17 +39,10 @@ import java.nio.file.Path;
|
||||||
import java.nio.file.Paths;
|
import java.nio.file.Paths;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
import java.util.ArrayList;
|
import java.util.*;
|
||||||
import java.util.LinkedHashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
import static run.halo.app.model.support.HaloConst.TEMP_TOKEN;
|
|
||||||
import static run.halo.app.model.support.HaloConst.TEMP_TOKEN_EXPIRATION;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Backup service implementation.
|
* Backup service implementation.
|
||||||
*
|
*
|
||||||
|
@ -64,34 +54,40 @@ import static run.halo.app.model.support.HaloConst.TEMP_TOKEN_EXPIRATION;
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class BackupServiceImpl implements BackupService {
|
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 static final String LINE_SEPARATOR = System.getProperty("line.separator");
|
||||||
|
|
||||||
private final PostService postService;
|
private final PostService postService;
|
||||||
|
|
||||||
private final PostTagService postTagService;
|
private final PostTagService postTagService;
|
||||||
|
|
||||||
private final OptionService optionService;
|
private final OptionService optionService;
|
||||||
private final StringCacheStore cacheStore;
|
|
||||||
|
private final OneTimeTokenService oneTimeTokenService;
|
||||||
|
|
||||||
private final HaloProperties haloProperties;
|
private final HaloProperties haloProperties;
|
||||||
|
|
||||||
public BackupServiceImpl(PostService postService,
|
public BackupServiceImpl(PostService postService,
|
||||||
PostTagService postTagService,
|
PostTagService postTagService,
|
||||||
OptionService optionService,
|
OptionService optionService,
|
||||||
StringCacheStore stringCacheStore,
|
OneTimeTokenService oneTimeTokenService,
|
||||||
HaloProperties haloProperties) {
|
HaloProperties haloProperties) {
|
||||||
this.postService = postService;
|
this.postService = postService;
|
||||||
this.postTagService = postTagService;
|
this.postTagService = postTagService;
|
||||||
this.optionService = optionService;
|
this.optionService = optionService;
|
||||||
this.cacheStore = stringCacheStore;
|
this.oneTimeTokenService = oneTimeTokenService;
|
||||||
this.haloProperties = haloProperties;
|
this.haloProperties = haloProperties;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sanitizes the specified file name.
|
* Sanitizes the specified file name.
|
||||||
*
|
*
|
||||||
* @param unsanitized the specified file name
|
* @param unSanitized the specified file name
|
||||||
* @return sanitized file name
|
* @return sanitized file name
|
||||||
*/
|
*/
|
||||||
public static String sanitizeFilename(final String unsanitized) {
|
public static String sanitizeFilename(final String unSanitized) {
|
||||||
return unsanitized.
|
return unSanitized.
|
||||||
replaceAll("[^(a-zA-Z0-9\\u4e00-\\u9fa5\\.)]", "").
|
replaceAll("[^(a-zA-Z0-9\\u4e00-\\u9fa5\\.)]", "").
|
||||||
replaceAll("[\\?\\\\/:|<>\\*\\[\\]\\(\\)\\$%\\{\\}@~\\.]", "").
|
replaceAll("[\\?\\\\/:|<>\\*\\[\\]\\(\\)\\$%\\{\\}@~\\.]", "").
|
||||||
replaceAll("\\s", "");
|
replaceAll("\\s", "");
|
||||||
|
@ -191,8 +187,14 @@ public class BackupServiceImpl implements BackupService {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<BackupDTO> listHaloBackups() {
|
public List<BackupDTO> listHaloBackups() {
|
||||||
|
// Ensure the parent folder exist
|
||||||
|
Path backupParentPath = Paths.get(haloProperties.getBackupDir());
|
||||||
|
if (Files.notExists(backupParentPath)) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
// Build backup dto
|
// Build backup dto
|
||||||
try (Stream<Path> subPathStream = Files.list(Paths.get(haloProperties.getBackupDir()))) {
|
try (Stream<Path> subPathStream = Files.list(backupParentPath)) {
|
||||||
return subPathStream
|
return subPathStream
|
||||||
.filter(backupPath -> StringUtils.startsWithIgnoreCase(backupPath.getFileName().toString(), HaloConst.HALO_BACKUP_PREFIX))
|
.filter(backupPath -> StringUtils.startsWithIgnoreCase(backupPath.getFileName().toString(), HaloConst.HALO_BACKUP_PREFIX))
|
||||||
.map(this::buildBackupDto)
|
.map(this::buildBackupDto)
|
||||||
|
@ -236,19 +238,32 @@ public class BackupServiceImpl implements BackupService {
|
||||||
public Resource loadFileAsResource(String fileName) {
|
public Resource loadFileAsResource(String fileName) {
|
||||||
Assert.hasText(fileName, "Backup file name must not be blank");
|
Assert.hasText(fileName, "Backup file name must not be blank");
|
||||||
|
|
||||||
// Get backup file path
|
Path backupParentPath = Paths.get(haloProperties.getBackupDir());
|
||||||
Path backupFilePath = Paths.get(haloProperties.getBackupDir(), fileName).normalize();
|
|
||||||
try {
|
try {
|
||||||
|
if (Files.notExists(backupParentPath)) {
|
||||||
|
// Create backup parent path if it does not exists
|
||||||
|
Files.createDirectories(backupParentPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get backup file path
|
||||||
|
Path backupFilePath = Paths.get(haloProperties.getBackupDir(), fileName).normalize();
|
||||||
|
|
||||||
|
// Check directory traversal
|
||||||
|
run.halo.app.utils.FileUtils.checkDirectoryTraversal(backupParentPath, backupFilePath);
|
||||||
|
|
||||||
// Build url resource
|
// Build url resource
|
||||||
Resource backupResource = new UrlResource(backupFilePath.toUri());
|
Resource backupResource = new UrlResource(backupFilePath.toUri());
|
||||||
if (!backupResource.exists()) {
|
if (!backupResource.exists()) {
|
||||||
// If the backup resouce is not exist
|
// If the backup resource is not exist
|
||||||
throw new NotFoundException("The file " + fileName + " was not found");
|
throw new NotFoundException("The file " + fileName + " was not found");
|
||||||
}
|
}
|
||||||
// Return the backup resource
|
// Return the backup resource
|
||||||
return backupResource;
|
return backupResource;
|
||||||
} catch (MalformedURLException e) {
|
} catch (MalformedURLException e) {
|
||||||
throw new NotFoundException("The file " + fileName + " was not found", e);
|
throw new NotFoundException("The file " + fileName + " was not found", e);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new ServiceException("Failed to create backup parent path: " + backupParentPath, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -271,8 +286,6 @@ public class BackupServiceImpl implements BackupService {
|
||||||
backup.setFileSize(Files.size(backupPath));
|
backup.setFileSize(Files.size(backupPath));
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
throw new ServiceException("Failed to access file " + backupPath, e);
|
throw new ServiceException("Failed to access file " + backupPath, e);
|
||||||
} catch (URISyntaxException e) {
|
|
||||||
throw new ServiceException("Failed to generate download link for file: " + backupPath, e);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return backup;
|
return backup;
|
||||||
|
@ -284,36 +297,21 @@ public class BackupServiceImpl implements BackupService {
|
||||||
* @param filename filename must not be blank
|
* @param filename filename must not be blank
|
||||||
* @return download url
|
* @return download url
|
||||||
*/
|
*/
|
||||||
private String buildDownloadUrl(@NonNull String filename) throws URISyntaxException {
|
@NonNull
|
||||||
|
private String buildDownloadUrl(@NonNull String filename) {
|
||||||
Assert.hasText(filename, "File name must not be blank");
|
Assert.hasText(filename, "File name must not be blank");
|
||||||
|
|
||||||
// Composite http url
|
// Composite http url
|
||||||
String backupFullUrl = HaloUtils.compositeHttpUrl(optionService.getBlogBaseUrl(), "api/admin/backups/halo", filename);
|
String backupUri = BACKUP_RESOURCE_BASE_URI + HaloUtils.URL_SEPARATOR + filename;
|
||||||
|
|
||||||
// Get temp token
|
// Get a one-time token
|
||||||
String tempToken = cacheStore.get(buildBackupTokenKey(filename)).orElseGet(() -> {
|
String oneTimeToken = oneTimeTokenService.create(backupUri);
|
||||||
String token = buildTempToken(1);
|
|
||||||
// Cache this projection
|
|
||||||
cacheStore.putIfAbsent(buildBackupTokenKey(filename), token, TEMP_TOKEN_EXPIRATION.toDays(), TimeUnit.DAYS);
|
|
||||||
return token;
|
|
||||||
});
|
|
||||||
|
|
||||||
return new URIBuilder(backupFullUrl).addParameter(TEMP_TOKEN, tempToken).toString();
|
// Build full url
|
||||||
|
return HaloUtils.compositeHttpUrl(optionService.getBlogBaseUrl(), backupUri)
|
||||||
|
+ "?"
|
||||||
|
+ HaloConst.ONE_TIME_TOKEN_QUERY_NAME
|
||||||
|
+ "=" + oneTimeToken;
|
||||||
}
|
}
|
||||||
|
|
||||||
private String buildBackupTokenKey(String backupFileName) {
|
|
||||||
return BACKUP_TOKEN_KEY_PREFIX + backupFileName;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String buildTempToken(@NonNull Object value) {
|
|
||||||
Assert.notNull(value, "Temp token value must not be null");
|
|
||||||
|
|
||||||
// Generate temp token
|
|
||||||
String tempToken = HaloUtils.randomUUIDWithoutDash();
|
|
||||||
|
|
||||||
// Cache the token
|
|
||||||
cacheStore.putIfAbsent(SecurityUtils.buildTempTokenKey(tempToken), value.toString(), TEMP_TOKEN_EXPIRATION.toDays(), TimeUnit.DAYS);
|
|
||||||
|
|
||||||
return tempToken;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
server:
|
server:
|
||||||
port: 8090
|
port: 18090
|
||||||
forward-headers-strategy: native
|
forward-headers-strategy: native
|
||||||
compression:
|
compression:
|
||||||
enabled: false
|
enabled: false
|
||||||
|
@ -59,4 +59,5 @@ logging:
|
||||||
|
|
||||||
halo:
|
halo:
|
||||||
doc-disabled: false
|
doc-disabled: false
|
||||||
|
auth-enable: false
|
||||||
workDir: ${user.home}/halo-test/
|
workDir: ${user.home}/halo-test/
|
||||||
|
|
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue