-
Notifications
You must be signed in to change notification settings - Fork 5.9k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add Reactive Messaging AuthenticationPrincipalArgumentResolver
Fixes gh-7363
- Loading branch information
Showing
4 changed files
with
1,016 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
210 changes: 210 additions & 0 deletions
210
...curity/messaging/handler/invocation/reactive/AuthenticationPrincipalArgumentResolver.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,210 @@ | ||
/* | ||
* Copyright 2019 the original author or authors. | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* https://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
package org.springframework.security.messaging.handler.invocation.reactive; | ||
|
||
import org.reactivestreams.Publisher; | ||
import org.springframework.core.MethodParameter; | ||
import org.springframework.core.ReactiveAdapter; | ||
import org.springframework.core.ReactiveAdapterRegistry; | ||
import org.springframework.core.ResolvableType; | ||
import org.springframework.core.annotation.AnnotationUtils; | ||
import org.springframework.expression.BeanResolver; | ||
import org.springframework.expression.Expression; | ||
import org.springframework.expression.ExpressionParser; | ||
import org.springframework.expression.spel.standard.SpelExpressionParser; | ||
import org.springframework.expression.spel.support.StandardEvaluationContext; | ||
import org.springframework.messaging.Message; | ||
import org.springframework.messaging.handler.invocation.reactive.HandlerMethodArgumentResolver; | ||
import org.springframework.security.core.Authentication; | ||
import org.springframework.security.core.annotation.AuthenticationPrincipal; | ||
import org.springframework.security.core.context.ReactiveSecurityContextHolder; | ||
import org.springframework.security.core.context.SecurityContext; | ||
import org.springframework.stereotype.Controller; | ||
import org.springframework.util.Assert; | ||
import org.springframework.util.StringUtils; | ||
import reactor.core.publisher.Mono; | ||
|
||
import java.lang.annotation.Annotation; | ||
|
||
/** | ||
* Allows resolving the {@link Authentication#getPrincipal()} using the | ||
* {@link AuthenticationPrincipal} annotation. For example, the following | ||
* {@link Controller}: | ||
* | ||
* <pre> | ||
* @Controller | ||
* public class MyController { | ||
* @MessageMapping("/im") | ||
* public void im(@AuthenticationPrincipal CustomUser customUser) { | ||
* // do something with CustomUser | ||
* } | ||
* } | ||
* </pre> | ||
* | ||
* <p> | ||
* Will resolve the CustomUser argument using {@link Authentication#getPrincipal()} from | ||
* the {@link ReactiveSecurityContextHolder}. If the {@link Authentication} or | ||
* {@link Authentication#getPrincipal()} is null, it will return null. If the types do not | ||
* match, null will be returned unless | ||
* {@link AuthenticationPrincipal#errorOnInvalidType()} is true in which case a | ||
* {@link ClassCastException} will be thrown. | ||
* | ||
* <p> | ||
* Alternatively, users can create a custom meta annotation as shown below: | ||
* | ||
* <pre> | ||
* @Target({ ElementType.PARAMETER }) | ||
* @Retention(RetentionPolicy.RUNTIME) | ||
* @AuthenticationPrincipal | ||
* public @interface CurrentUser { | ||
* } | ||
* </pre> | ||
* | ||
* <p> | ||
* The custom annotation can then be used instead. For example: | ||
* | ||
* <pre> | ||
* @Controller | ||
* public class MyController { | ||
* @MessageMapping("/im") | ||
* public void im(@CurrentUser CustomUser customUser) { | ||
* // do something with CustomUser | ||
* } | ||
* } | ||
* </pre> | ||
* @author Rob Winch | ||
* @since 5.2 | ||
*/ | ||
public class AuthenticationPrincipalArgumentResolver | ||
implements HandlerMethodArgumentResolver { | ||
|
||
private ExpressionParser parser = new SpelExpressionParser(); | ||
|
||
private BeanResolver beanResolver; | ||
|
||
private ReactiveAdapterRegistry adapterRegistry = ReactiveAdapterRegistry | ||
.getSharedInstance(); | ||
|
||
/** | ||
* Sets the {@link BeanResolver} to be used on the expressions | ||
* @param beanResolver the {@link BeanResolver} to use | ||
*/ | ||
public void setBeanResolver(BeanResolver beanResolver) { | ||
this.beanResolver = beanResolver; | ||
} | ||
|
||
/** | ||
* Sets the {@link ReactiveAdapterRegistry} to be used. | ||
* @param adapterRegistry the {@link ReactiveAdapterRegistry} to use. Cannot be null. Default is | ||
* {@link ReactiveAdapterRegistry#getSharedInstance()} | ||
*/ | ||
public void setAdapterRegistry(ReactiveAdapterRegistry adapterRegistry) { | ||
Assert.notNull(adapterRegistry, "adapterRegistry cannot be null"); | ||
this.adapterRegistry = adapterRegistry; | ||
} | ||
|
||
@Override | ||
public boolean supportsParameter(MethodParameter parameter) { | ||
return findMethodAnnotation(AuthenticationPrincipal.class, parameter) != null; | ||
} | ||
|
||
public Mono<Object> resolveArgument(MethodParameter parameter, Message<?> message) { | ||
ReactiveAdapter adapter = this.adapterRegistry | ||
.getAdapter(parameter.getParameterType()); | ||
return ReactiveSecurityContextHolder.getContext() | ||
.map(SecurityContext::getAuthentication).flatMap(a -> { | ||
Object p = resolvePrincipal(parameter, a.getPrincipal()); | ||
Mono<Object> principal = Mono.justOrEmpty(p); | ||
return adapter == null ? | ||
principal : | ||
Mono.just(adapter.fromPublisher(principal)); | ||
}); | ||
} | ||
|
||
private Object resolvePrincipal(MethodParameter parameter, Object principal) { | ||
AuthenticationPrincipal authPrincipal = findMethodAnnotation( | ||
AuthenticationPrincipal.class, parameter); | ||
|
||
String expressionToParse = authPrincipal.expression(); | ||
if (StringUtils.hasLength(expressionToParse)) { | ||
StandardEvaluationContext context = new StandardEvaluationContext(); | ||
context.setRootObject(principal); | ||
context.setVariable("this", principal); | ||
context.setBeanResolver(this.beanResolver); | ||
|
||
Expression expression = this.parser.parseExpression(expressionToParse); | ||
principal = expression.getValue(context); | ||
} | ||
|
||
if (isInvalidType(parameter, principal)) { | ||
|
||
if (authPrincipal.errorOnInvalidType()) { | ||
throw new ClassCastException( | ||
principal + " is not assignable to " + parameter | ||
.getParameterType()); | ||
} | ||
else { | ||
return null; | ||
} | ||
} | ||
|
||
return principal; | ||
} | ||
|
||
private boolean isInvalidType(MethodParameter parameter, Object principal) { | ||
if (principal == null) { | ||
return false; | ||
} | ||
Class<?> typeToCheck = parameter.getParameterType(); | ||
boolean isParameterPublisher = Publisher.class | ||
.isAssignableFrom(parameter.getParameterType()); | ||
if (isParameterPublisher) { | ||
ResolvableType resolvableType = ResolvableType.forMethodParameter(parameter); | ||
Class<?> genericType = resolvableType.resolveGeneric(0); | ||
if (genericType == null) { | ||
return false; | ||
} | ||
typeToCheck = genericType; | ||
} | ||
return !typeToCheck.isAssignableFrom(principal.getClass()); | ||
} | ||
|
||
/** | ||
* Obtains the specified {@link Annotation} on the specified {@link MethodParameter}. | ||
* | ||
* @param annotationClass the class of the {@link Annotation} to find on the | ||
* {@link MethodParameter} | ||
* @param parameter the {@link MethodParameter} to search for an {@link Annotation} | ||
* @return the {@link Annotation} that was found or null. | ||
*/ | ||
private <T extends Annotation> T findMethodAnnotation(Class<T> annotationClass, | ||
MethodParameter parameter) { | ||
T annotation = parameter.getParameterAnnotation(annotationClass); | ||
if (annotation != null) { | ||
return annotation; | ||
} | ||
Annotation[] annotationsToSearch = parameter.getParameterAnnotations(); | ||
for (Annotation toSearch : annotationsToSearch) { | ||
annotation = AnnotationUtils | ||
.findAnnotation(toSearch.annotationType(), annotationClass); | ||
if (annotation != null) { | ||
return annotation; | ||
} | ||
} | ||
return null; | ||
} | ||
} |
Oops, something went wrong.