Skip to content

Commit

Permalink
Merge remote-tracking branch 'upstream/main'
Browse files Browse the repository at this point in the history
  • Loading branch information
ruibaby committed Feb 27, 2024
2 parents 68aeb2f + b132597 commit ee6b050
Show file tree
Hide file tree
Showing 14 changed files with 272 additions and 5 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package run.halo.app.security;

import org.pf4j.ExtensionPoint;
import org.springframework.web.server.WebFilter;

/**
* Security web filter for anonymous authentication.
*
* @author johnniang
*/
public interface AnonymousAuthenticationSecurityWebFilter extends WebFilter, ExtensionPoint {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package run.halo.app.security;

import org.pf4j.ExtensionPoint;
import org.springframework.web.server.WebFilter;

/**
* Security web filter for normal authentication.
*
* @author johnniang
*/
public interface AuthenticationSecurityWebFilter extends WebFilter, ExtensionPoint {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package run.halo.app.security;

import org.pf4j.ExtensionPoint;
import org.springframework.web.server.WebFilter;

/**
* Security web filter for form login.
*
* @author johnniang
*/
public interface FormLoginSecurityWebFilter extends WebFilter, ExtensionPoint {

}
2 changes: 1 addition & 1 deletion application/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import de.undercouch.gradle.tasks.download.Download
import org.gradle.crypto.checksum.Checksum

plugins {
id 'org.springframework.boot' version '3.2.2'
id 'org.springframework.boot' version '3.2.3'
id 'io.spring.dependency-management' version '1.1.0'
id "com.gorylenko.gradle-git-properties" version "2.3.2"
id "checkstyle"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
* @since 2.0.0
*/
@Component
@Deprecated(forRemoval = true)
public class ExtensionComponentsFinder {
public static final String SYSTEM_PLUGIN_ID = "system";
private final PluginManager pluginManager;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ private Mono<Unstructured> createOrUpdate(Unstructured unstructured) {
.doOnNext(old -> {
unstructured.getMetadata().setVersion(old.getMetadata().getVersion());
})
.map(ignored -> unstructured)
.flatMap(client::update)
.switchIfEmpty(Mono.defer(() -> client.create(unstructured)));
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package run.halo.app.plugin.extensionpoint;

import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Set;
Expand Down Expand Up @@ -86,6 +87,15 @@ public <T extends ExtensionPoint> Flux<T> getEnabledExtensionByDefinition(
});
}

@Override
public <T extends ExtensionPoint> Flux<T> getExtensions(Class<T> extensionPointClass) {
var extensions = new ArrayList<>(pluginManager.getExtensions(extensionPointClass));
applicationContext.getBeanProvider(extensionPointClass)
.orderedStream()
.forEach(extensions::add);
return Flux.fromIterable(extensions);
}

@NonNull
<T extends ExtensionPoint> List<T> getAllExtensions(Class<T> extensionPoint) {
Stream<T> pluginExtsStream = pluginManager.getExtensions(extensionPoint)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,13 @@ public interface ExtensionGetter {
* the {@link ExtensionPointDefinition}.
*/
<T extends ExtensionPoint> Flux<T> getEnabledExtensionByDefinition(Class<T> extensionPoint);

/**
* Get all extensions according to extension point class.
*
* @param extensionPointClass extension point class
* @param <T> type of extension point
* @return a bunch of extension points.
*/
<T extends ExtensionPoint> Flux<T> getExtensions(Class<T> extensionPointClass);
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,14 @@
import static org.springframework.web.reactive.function.server.RequestPredicates.GET;
import static org.springframework.web.reactive.function.server.RequestPredicates.accept;

import java.io.IOException;
import java.time.Instant;
import java.util.List;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.pf4j.PluginManager;
import org.springframework.boot.autoconfigure.web.WebProperties;
import org.springframework.context.ApplicationContext;
import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.core.io.Resource;
Expand All @@ -18,11 +21,13 @@
import org.springframework.lang.Nullable;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.RouterFunctions;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import org.springframework.web.util.pattern.PathPatternParser;
import reactor.core.publisher.Mono;
import run.halo.app.core.extension.ReverseProxy;
import run.halo.app.core.extension.ReverseProxy.FileReverseProxyProvider;
import run.halo.app.core.extension.ReverseProxy.ReverseProxyRule;
Expand All @@ -45,6 +50,7 @@ public class ReverseProxyRouterFunctionFactory {

private final PluginManager pluginManager;
private final ApplicationContext applicationContext;
private final WebProperties webProperties;

/**
* <p>Create {@link RouterFunction} according to the {@link ReverseProxy} custom resource
Expand Down Expand Up @@ -76,8 +82,23 @@ private RouterFunction<ServerResponse> createReverseProxyRouterFunction(
if (!resource.exists()) {
return ServerResponse.notFound().build();
}
return ServerResponse.ok()
.bodyValue(resource);
var cacheProperties = webProperties.getResources().getCache();
var useLastModified = cacheProperties.isUseLastModified();
var bodyBuilder = ServerResponse.ok()
.cacheControl(cacheProperties.getCachecontrol().toHttpCacheControl());
try {
if (useLastModified) {
var lastModified = Instant.ofEpochMilli(resource.lastModified());
return request.checkNotModified(lastModified)
.switchIfEmpty(Mono.defer(
() -> bodyBuilder.lastModified(lastModified)
.body(BodyInserters.fromResource(resource)))
);
}
return bodyBuilder.body(BodyInserters.fromResource(resource));
} catch (IOException e) {
throw new RuntimeException(e);
}
});
}).reduce(RouterFunction::and).orElse(null);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package run.halo.app.security;

import static org.springframework.security.config.web.server.SecurityWebFiltersOrder.ANONYMOUS_AUTHENTICATION;
import static org.springframework.security.config.web.server.SecurityWebFiltersOrder.AUTHENTICATION;
import static org.springframework.security.config.web.server.SecurityWebFiltersOrder.FORM_LOGIN;

import lombok.Setter;
import org.pf4j.ExtensionPoint;
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.web.server.WebFilterChainProxy;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import reactor.core.publisher.Mono;
import run.halo.app.plugin.extensionpoint.ExtensionGetter;
import run.halo.app.security.authentication.SecurityConfigurer;

@Component
public class SecurityWebFiltersConfigurer implements SecurityConfigurer {

private final ExtensionGetter extensionGetter;

public SecurityWebFiltersConfigurer(ExtensionGetter extensionGetter) {
this.extensionGetter = extensionGetter;
}

@Override
public void configure(ServerHttpSecurity http) {
http
.addFilterAt(
new SecurityWebFilterChainProxy(FormLoginSecurityWebFilter.class), FORM_LOGIN
)
.addFilterAt(
new SecurityWebFilterChainProxy(AuthenticationSecurityWebFilter.class),
AUTHENTICATION
)
.addFilterAt(
new SecurityWebFilterChainProxy(AnonymousAuthenticationSecurityWebFilter.class),
ANONYMOUS_AUTHENTICATION
);
}

public class SecurityWebFilterChainProxy implements WebFilter {

@Setter
private WebFilterChainProxy.WebFilterChainDecorator filterChainDecorator;

private final Class<? extends ExtensionPoint> extensionPointClass;

public SecurityWebFilterChainProxy(Class<? extends ExtensionPoint> extensionPointClass) {
this.extensionPointClass = extensionPointClass;
this.filterChainDecorator = new WebFilterChainProxy.DefaultWebFilterChainDecorator();
}

@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
return extensionGetter.getExtensions(this.extensionPointClass)
.sort(AnnotationAwareOrderComparator.INSTANCE)
.cast(WebFilter.class)
.collectList()
.map(filters -> filterChainDecorator.decorate(chain, filters))
.flatMap(decoratedChain -> decoratedChain.filter(exchange));
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,6 @@ public class TwoFactorAuthSettings {
* @return true if 2FA is enabled and configured, false otherwise.
*/
public boolean isAvailable() {
return enabled && (emailVerified || totpConfigured);
return enabled && totpConfigured;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package run.halo.app.security.authentication.twofactor;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.params.provider.Arguments.arguments;

import java.util.stream.Stream;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;

class TwoFactorAuthSettingsTest {

@ParameterizedTest
@MethodSource("isAvailableCases")
void isAvailableTest(TwoFactorAuthSettings settings, boolean expectAvailable) {
assertEquals(expectAvailable, settings.isAvailable());
}

static Stream<Arguments> isAvailableCases() {
return Stream.of(
arguments(settings(false, true, true), false),
arguments(settings(false, false, false), false),
arguments(settings(false, false, true), false),
arguments(settings(false, true, false), false),
arguments(settings(true, true, true), true),
arguments(settings(true, false, false), false),
arguments(settings(true, false, true), true),
arguments(settings(true, true, false), false)
);
}

static TwoFactorAuthSettings settings(boolean enabled, boolean emailVerified,
boolean totpConfigured) {
var settings = new TwoFactorAuthSettings();
settings.setEnabled(enabled);
settings.setEmailVerified(emailVerified);
settings.setTotpConfigured(totpConfigured);
return settings;
}

}
77 changes: 77 additions & 0 deletions docs/extension-points/authentication.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# Halo 认证扩展点

此前,Halo 提供了 AdditionalWebFilter 作为扩展点供插件扩展认证相关的功能。但是近期我们明确了 AdditionalWebFilter
的使用用途,故不再作为认证的扩展点。

目前,Halo 提供了三种认证扩展点:表单登录认证、普通认证和匿名认证。

## 表单登录(FormLogin)

示例如下:

```java
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilterChain;
import reactor.core.publisher.Mono;
import run.halo.app.security.FormLoginSecurityWebFilter;

@Component
public class MyFormLoginSecurityWebFilter implements FormLoginSecurityWebFilter {

@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
// Do your logic here
return chain.filter(exchange);
}
}

```
## 普通认证(Authentication)

示例如下:

```java
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilterChain;
import reactor.core.publisher.Mono;
import run.halo.app.security.AuthenticationSecurityWebFilter;

@Component
public class MyAuthenticationSecurityWebFilter implements AuthenticationSecurityWebFilter {

@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
// Do your logic here
return chain.filter(exchange);
}
}
```

## 匿名认证(Anonymous Authentication

示例如下:

```java
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilterChain;
import reactor.core.publisher.Mono;
import run.halo.app.security.AnonymousAuthenticationSecurityWebFilter;

@Component
public class MyAnonymousAuthenticationSecurityWebFilter
implements AnonymousAuthenticationSecurityWebFilter {

@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
// Do your logic here
return chain.filter(exchange);
}
}
```

我们在实现扩展点的时候需要注意:如果当前请求不满足认证条件,请一定要调用 `chain.filter(exchange)`,给其他 filter 留下机会。

后续会根据需求实现其他认证相关的扩展点。
2 changes: 1 addition & 1 deletion platform/application/build.gradle
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import org.springframework.boot.gradle.plugin.SpringBootPlugin

plugins {
id 'org.springframework.boot' version '3.2.2' apply false
id 'org.springframework.boot' version '3.2.3' apply false
id 'java-platform'
id 'halo.publish'
id 'signing'
Expand Down

0 comments on commit ee6b050

Please sign in to comment.