Support extending username password authentication (#4265)

#### What type of PR is this?

/kind feature
/area core
/area plugin

#### What this PR does / why we need it:

Plugin developers are able to define own UsernamePasswordAuthenticationManager to take charge of username password authentication. 

1. If the manager fails to handle, the default authentication manager will be used.
2. If the manager returns `Mono.empty()`, the default authentication manager will be used.

For example:

```java
@Component
public class LdapAuthenticationManager
    extends UserDetailsRepositoryReactiveAuthenticationManager
    implements UsernamePasswordAuthenticationManager {

    public LdapAuthenticationManager(ReactiveUserDetailsService userDetailsService) {
        super(userDetailsService);
    }

    @Override
    protected Mono<UserDetails> retrieveUser(String username) {
        return super.retrieveUser(username);
    }
}
```

#### Which issue(s) this PR fixes:

See https://github.com/halo-dev/halo/issues/4207#issuecomment-1643042348 for more.

#### Does this PR introduce a user-facing change?

```release-note
提供用户名密码认证扩展
```
pull/4290/head
John Niang 2023-07-24 17:26:14 +08:00 committed by GitHub
parent 0d19ccdb8a
commit 4505fcfd16
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 86 additions and 4 deletions

View File

@ -0,0 +1,18 @@
package run.halo.app.security.authentication.login;
import org.pf4j.ExtensionPoint;
import org.springframework.security.authentication.ReactiveAuthenticationManager;
/**
* An extension point for username password authentication.
* Any non-authentication exception occurs, the default authentication will be used.
* If you want to skip authentication, please return Mono.empty() directly, the default
* authentication will be used.
*
* @author johnniang
* @since 2.8
*/
public interface UsernamePasswordAuthenticationManager
extends ReactiveAuthenticationManager, ExtensionPoint {
}

View File

@ -40,6 +40,7 @@ import org.springframework.web.server.WebFilterChain;
import reactor.core.publisher.Mono;
import run.halo.app.infra.exception.RateLimitExceededException;
import run.halo.app.infra.utils.IpAddressUtils;
import run.halo.app.plugin.extensionpoint.ExtensionGetter;
import run.halo.app.security.AdditionalWebFilter;
/**
@ -71,11 +72,14 @@ public class UsernamePasswordAuthenticator implements AdditionalWebFilter {
private final RateLimiterRegistry rateLimiterRegistry;
private final MessageSource messageSource;
private final ExtensionGetter extensionGetter;
public UsernamePasswordAuthenticator(ServerResponse.Context context,
ObservationRegistry observationRegistry, ReactiveUserDetailsService userDetailsService,
ReactiveUserDetailsPasswordService passwordService, PasswordEncoder passwordEncoder,
ServerSecurityContextRepository securityContextRepository, CryptoService cryptoService,
RateLimiterRegistry rateLimiterRegistry, MessageSource messageSource) {
RateLimiterRegistry rateLimiterRegistry, MessageSource messageSource,
ExtensionGetter extensionGetter) {
this.context = context;
this.observationRegistry = observationRegistry;
this.userDetailsService = userDetailsService;
@ -85,6 +89,7 @@ public class UsernamePasswordAuthenticator implements AdditionalWebFilter {
this.cryptoService = cryptoService;
this.rateLimiterRegistry = rateLimiterRegistry;
this.messageSource = messageSource;
this.extensionGetter = extensionGetter;
this.authenticationWebFilter =
new UsernamePasswordAuthenticationWebFilter(authenticationManager());
@ -113,12 +118,17 @@ public class UsernamePasswordAuthenticator implements AdditionalWebFilter {
}
ReactiveAuthenticationManager authenticationManager() {
var manager = new UserDetailsRepositoryReactiveAuthenticationManager(userDetailsService);
manager.setPasswordEncoder(passwordEncoder);
manager.setUserDetailsPasswordService(passwordService);
var manager = new UsernamePasswordDelegatingAuthenticationManager(extensionGetter,
defaultAuthenticationManager());
return new ObservationReactiveAuthenticationManager(observationRegistry, manager);
}
ReactiveAuthenticationManager defaultAuthenticationManager() {
var manager = new UserDetailsRepositoryReactiveAuthenticationManager(userDetailsService);
manager.setPasswordEncoder(passwordEncoder);
manager.setUserDetailsPasswordService(passwordService);
return manager;
}
private <T> RateLimiterOperator<T> createIPBasedRateLimiter(ServerWebExchange exchange) {
var clientIp = IpAddressUtils.getClientIp(exchange.getRequest());

View File

@ -0,0 +1,43 @@
package run.halo.app.security.authentication.login;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.ReactiveAuthenticationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import reactor.core.publisher.Mono;
import run.halo.app.plugin.extensionpoint.ExtensionGetter;
@Slf4j
public class UsernamePasswordDelegatingAuthenticationManager
implements ReactiveAuthenticationManager {
private final ExtensionGetter extensionGetter;
private final ReactiveAuthenticationManager defaultAuthenticationManager;
public UsernamePasswordDelegatingAuthenticationManager(ExtensionGetter extensionGetter,
ReactiveAuthenticationManager defaultAuthenticationManager) {
this.extensionGetter = extensionGetter;
this.defaultAuthenticationManager = defaultAuthenticationManager;
}
@Override
public Mono<Authentication> authenticate(Authentication authentication) {
return extensionGetter
.getEnabledExtensionByDefinition(UsernamePasswordAuthenticationManager.class)
.next()
.flatMap(authenticationManager -> authenticationManager.authenticate(authentication)
.doOnError(t -> log.error(
"failed to authenticate with {}, fallback to default username password "
+ "authentication.", authenticationManager.getClass(), t)
)
.onErrorResume(
t -> !(t instanceof AuthenticationException),
t -> Mono.empty()
)
)
.switchIfEmpty(
Mono.defer(() -> defaultAuthenticationManager.authenticate(authentication))
);
}
}

View File

@ -41,3 +41,14 @@ spec:
displayName: CommentWidget
type: SINGLETON
description: "Provides an extension point for the comment widget on the theme-side."
---
apiVersion: plugin.halo.run/v1alpha1
kind: ExtensionPointDefinition
metadata:
name: username-password-authentication-manager
spec:
className: run.halo.app.security.authentication.login.UsernamePasswordAuthenticationManager
displayName: Username password authentication manager
type: SINGLETON
description: "Provides a way to extend the username password authentication."