Move Builder to Authentication

Leaving the Builder in Authentication allows
authentication implementations to implement Builder
without needing to implement BuildableAuthentication.

Issue gh-18052
This commit is contained in:
Josh Cummings 2025-10-15 09:20:42 -06:00
parent 4102007119
commit 21ff7688cc
3 changed files with 120 additions and 150 deletions

View File

@ -28,7 +28,6 @@ import org.jspecify.annotations.Nullable;
import org.springframework.security.core.AuthenticatedPrincipal; import org.springframework.security.core.AuthenticatedPrincipal;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.security.core.BuildableAuthentication;
import org.springframework.security.core.CredentialsContainer; import org.springframework.security.core.CredentialsContainer;
import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.core.authority.AuthorityUtils;
@ -199,15 +198,15 @@ public abstract class AbstractAuthenticationToken implements Authentication, Cre
} }
/** /**
* A common abstract implementation of {@link BuildableAuthentication.Builder}. It * A common abstract implementation of {@link Authentication.Builder}. It implements
* implements the builder methods that correspond to the {@link Authentication} * the builder methods that correspond to the {@link Authentication} methods that
* methods that {@link AbstractAuthenticationToken} implements * {@link AbstractAuthenticationToken} implements
* *
* @param <B> * @param <B>
* @since 7.0 * @since 7.0
*/ */
protected abstract static class AbstractAuthenticationBuilder<B extends AbstractAuthenticationBuilder<B>> protected abstract static class AbstractAuthenticationBuilder<B extends AbstractAuthenticationBuilder<B>>
implements BuildableAuthentication.Builder<B> { implements Authentication.Builder<B> {
private boolean authenticated; private boolean authenticated;

View File

@ -19,6 +19,9 @@ package org.springframework.security.core;
import java.io.Serializable; import java.io.Serializable;
import java.security.Principal; import java.security.Principal;
import java.util.Collection; import java.util.Collection;
import java.util.Set;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import org.jspecify.annotations.Nullable; import org.jspecify.annotations.Nullable;
@ -136,4 +139,112 @@ public interface Authentication extends Principal, Serializable {
*/ */
void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException; void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
/**
* A builder based on a given {@link BuildableAuthentication} instance
*
* @author Josh Cummings
* @since 7.0
*/
interface Builder<B extends Builder<B>> {
/**
* Apply this authentication instance
* <p>
* By default, merges the authorities in the provided {@code authentication} with
* the authentication being built. Only those authorities that haven't already
* been specified to the builder will be added.
* </p>
* @param authentication the {@link Authentication} to appluy
* @return the {@link Builder} for additional configuration
* @see BuildableAuthentication#getAuthorities
*/
default B authentication(Authentication authentication) {
return authorities((a) -> {
Set<String> newAuthorities = a.stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.toUnmodifiableSet());
for (GrantedAuthority currentAuthority : authentication.getAuthorities()) {
if (!newAuthorities.contains(currentAuthority.getAuthority())) {
a.add(currentAuthority);
}
}
});
}
/**
* Mutate the authorities with this {@link Consumer}.
* <p>
* Note that since a non-empty set of authorities implies an
* {@link Authentication} is authenticated, this method also marks the
* authentication as {@link #authenticated} by default.
* </p>
* @param authorities a consumer that receives the full set of authorities
* @return the {@link Builder} for additional configuration
* @see Authentication#getAuthorities
*/
B authorities(Consumer<Collection<GrantedAuthority>> authorities);
/**
* Use this credential.
* <p>
* Note that since some credentials are insecure to store, this method is
* implemented as unsupported by default. Only implement or use this method if you
* support secure storage of the credential or if your implementation also
* implements {@link CredentialsContainer} and the credentials are thereby erased.
* </p>
* @param credentials the credentials to use
* @return the {@link Builder} for additional configuration
* @see Authentication#getCredentials
*/
default B credentials(@Nullable Object credentials) {
throw new UnsupportedOperationException(
String.format("%s does not store credentials", this.getClass().getSimpleName()));
}
/**
* Use this details object.
* <p>
* Implementations may choose to use these {@code details} in combination with any
* principal from the pre-existing {@link Authentication} instance.
* </p>
* @param details the details to use
* @return the {@link Builder} for additional configuration
* @see Authentication#getDetails
*/
B details(@Nullable Object details);
/**
* Use this principal.
* <p>
* Note that in many cases, the principal is strongly-typed. Implementations may
* choose to do a type check and are not necessarily expected to allow any object
* as a principal.
* </p>
* <p>
* Implementations may choose to use this {@code principal} in combination with
* any principal from the pre-existing {@link Authentication} instance.
* </p>
* @param principal the principal to use
* @return the {@link Builder} for additional configuration
* @see Authentication#getPrincipal
*/
B principal(@Nullable Object principal);
/**
* Mark this authentication as authenticated or not
* @param authenticated whether this is an authenticated {@link Authentication}
* instance
* @return the {@link Builder} for additional configuration
* @see Authentication#isAuthenticated
*/
B authenticated(boolean authenticated);
/**
* Build an {@link Authentication} instance
* @return the {@link Authentication} instance
*/
Authentication build();
}
} }

View File

@ -16,42 +16,11 @@
package org.springframework.security.core; package org.springframework.security.core;
import java.util.Collection;
import java.util.Set;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import org.jspecify.annotations.Nullable;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.context.SecurityContextHolder;
/** /**
* Represents the token for an authentication request or for an authenticated principal * An {@link Authentication} that is also buildable.
* once the request has been processed by the
* {@link AuthenticationManager#authenticate(Authentication)} method.
* <p>
* Once the request has been authenticated, the <tt>Authentication</tt> will usually be
* stored in a thread-local <tt>SecurityContext</tt> managed by the
* {@link SecurityContextHolder} by the authentication mechanism which is being used. An
* explicit authentication can be achieved, without using one of Spring Security's
* authentication mechanisms, by creating an <tt>Authentication</tt> instance and using
* the code:
* *
* <pre> * @author Josh Cummings
* SecurityContext context = SecurityContextHolder.createEmptyContext(); * @since 7.0
* context.setAuthentication(anAuthentication);
* SecurityContextHolder.setContext(context);
* </pre>
*
* Note that unless the <tt>Authentication</tt> has the <tt>authenticated</tt> property
* set to <tt>true</tt>, it will still be authenticated by any security interceptor (for
* method or web invocations) which encounters it.
* <p>
* In most cases, the framework transparently takes care of managing the security context
* and authentication objects for you.
*
* @author Ben Alex
*/ */
public interface BuildableAuthentication extends Authentication { public interface BuildableAuthentication extends Authentication {
@ -69,118 +38,9 @@ public interface BuildableAuthentication extends Authentication {
* {@link Authentication} interface and that custom information is often contained in * {@link Authentication} interface and that custom information is often contained in
* the {@link Authentication#getPrincipal} value. * the {@link Authentication#getPrincipal} value.
* </p> * </p>
* @return an {@link Builder} for building a new {@link BuildableAuthentication} based * @return an {@link Builder} for building a new {@link Authentication} based on this
* on this instance * instance
* @since 7.0
*/ */
Builder<?> toBuilder(); Builder<?> toBuilder();
/**
* A builder based on a given {@link BuildableAuthentication} instance
*
* @author Josh Cummings
* @since 7.0
*/
interface Builder<B extends Builder<B>> {
/**
* Apply this authentication instance
* <p>
* By default, merges the authorities in the provided {@code authentication} with
* the authentication being built. Only those authorities that haven't already
* been specified to the builder will be added.
* </p>
* @param authentication the {@link Authentication} to appluy
* @return the {@link Builder} for additional configuration
* @see BuildableAuthentication#getAuthorities
*/
default B authentication(Authentication authentication) {
return authorities((a) -> {
Set<String> newAuthorities = a.stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.toUnmodifiableSet());
for (GrantedAuthority currentAuthority : authentication.getAuthorities()) {
if (!newAuthorities.contains(currentAuthority.getAuthority())) {
a.add(currentAuthority);
}
}
});
}
/**
* Mutate the authorities with this {@link Consumer}.
* <p>
* Note that since a non-empty set of authorities implies an
* {@link Authentication} is authenticated, this method also marks the
* authentication as {@link #authenticated} by default.
* </p>
* @param authorities a consumer that receives the full set of authorities
* @return the {@link Builder} for additional configuration
* @see Authentication#getAuthorities
*/
B authorities(Consumer<Collection<GrantedAuthority>> authorities);
/**
* Use this credential.
* <p>
* Note that since some credentials are insecure to store, this method is
* implemented as unsupported by default. Only implement or use this method if you
* support secure storage of the credential or if your implementation also
* implements {@link CredentialsContainer} and the credentials are thereby erased.
* </p>
* @param credentials the credentials to use
* @return the {@link Builder} for additional configuration
* @see Authentication#getCredentials
*/
default B credentials(@Nullable Object credentials) {
throw new UnsupportedOperationException(
String.format("%s does not store credentials", this.getClass().getSimpleName()));
}
/**
* Use this details object.
* <p>
* Implementations may choose to use these {@code details} in combination with any
* principal from the pre-existing {@link Authentication} instance.
* </p>
* @param details the details to use
* @return the {@link Builder} for additional configuration
* @see Authentication#getDetails
*/
B details(@Nullable Object details);
/**
* Use this principal.
* <p>
* Note that in many cases, the principal is strongly-typed. Implementations may
* choose to do a type check and are not necessarily expected to allow any object
* as a principal.
* </p>
* <p>
* Implementations may choose to use this {@code principal} in combination with
* any principal from the pre-existing {@link Authentication} instance.
* </p>
* @param principal the principal to use
* @return the {@link Builder} for additional configuration
* @see Authentication#getPrincipal
*/
B principal(@Nullable Object principal);
/**
* Mark this authentication as authenticated or not
* @param authenticated whether this is an authenticated {@link Authentication}
* instance
* @return the {@link Builder} for additional configuration
* @see Authentication#isAuthenticated
*/
B authenticated(boolean authenticated);
/**
* Build an {@link Authentication} instance
* @return the {@link Authentication} instance
*/
Authentication build();
}
} }