Skip to content

Commit b09e9f1

Browse files
committed
Add Reactive Messaging AuthenticationPrincipalArgumentResolver
Fixes gh-7363
1 parent 9f18c2e commit b09e9f1

File tree

4 files changed

+1016
-0
lines changed

4 files changed

+1016
-0
lines changed

messaging/spring-security-messaging.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ dependencies {
1010

1111
optional project(':spring-security-web')
1212
optional 'org.springframework:spring-websocket'
13+
optional 'io.projectreactor:reactor-core'
1314
optional 'javax.servlet:javax.servlet-api'
1415

1516
testCompile project(path: ':spring-security-core', configuration: 'tests')
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
/*
2+
* Copyright 2019 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.security.messaging.handler.invocation.reactive;
18+
19+
import org.reactivestreams.Publisher;
20+
import org.springframework.core.MethodParameter;
21+
import org.springframework.core.ReactiveAdapter;
22+
import org.springframework.core.ReactiveAdapterRegistry;
23+
import org.springframework.core.ResolvableType;
24+
import org.springframework.core.annotation.AnnotationUtils;
25+
import org.springframework.expression.BeanResolver;
26+
import org.springframework.expression.Expression;
27+
import org.springframework.expression.ExpressionParser;
28+
import org.springframework.expression.spel.standard.SpelExpressionParser;
29+
import org.springframework.expression.spel.support.StandardEvaluationContext;
30+
import org.springframework.messaging.Message;
31+
import org.springframework.messaging.handler.invocation.reactive.HandlerMethodArgumentResolver;
32+
import org.springframework.security.core.Authentication;
33+
import org.springframework.security.core.annotation.AuthenticationPrincipal;
34+
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
35+
import org.springframework.security.core.context.SecurityContext;
36+
import org.springframework.stereotype.Controller;
37+
import org.springframework.util.Assert;
38+
import org.springframework.util.StringUtils;
39+
import reactor.core.publisher.Mono;
40+
41+
import java.lang.annotation.Annotation;
42+
43+
/**
44+
* Allows resolving the {@link Authentication#getPrincipal()} using the
45+
* {@link AuthenticationPrincipal} annotation. For example, the following
46+
* {@link Controller}:
47+
*
48+
* <pre>
49+
* &#64;Controller
50+
* public class MyController {
51+
* &#64;MessageMapping("/im")
52+
* public void im(@AuthenticationPrincipal CustomUser customUser) {
53+
* // do something with CustomUser
54+
* }
55+
* }
56+
* </pre>
57+
*
58+
* <p>
59+
* Will resolve the CustomUser argument using {@link Authentication#getPrincipal()} from
60+
* the {@link ReactiveSecurityContextHolder}. If the {@link Authentication} or
61+
* {@link Authentication#getPrincipal()} is null, it will return null. If the types do not
62+
* match, null will be returned unless
63+
* {@link AuthenticationPrincipal#errorOnInvalidType()} is true in which case a
64+
* {@link ClassCastException} will be thrown.
65+
*
66+
* <p>
67+
* Alternatively, users can create a custom meta annotation as shown below:
68+
*
69+
* <pre>
70+
* &#064;Target({ ElementType.PARAMETER })
71+
* &#064;Retention(RetentionPolicy.RUNTIME)
72+
* &#064;AuthenticationPrincipal
73+
* public @interface CurrentUser {
74+
* }
75+
* </pre>
76+
*
77+
* <p>
78+
* The custom annotation can then be used instead. For example:
79+
*
80+
* <pre>
81+
* &#64;Controller
82+
* public class MyController {
83+
* &#64;MessageMapping("/im")
84+
* public void im(@CurrentUser CustomUser customUser) {
85+
* // do something with CustomUser
86+
* }
87+
* }
88+
* </pre>
89+
* @author Rob Winch
90+
* @since 5.2
91+
*/
92+
public class AuthenticationPrincipalArgumentResolver
93+
implements HandlerMethodArgumentResolver {
94+
95+
private ExpressionParser parser = new SpelExpressionParser();
96+
97+
private BeanResolver beanResolver;
98+
99+
private ReactiveAdapterRegistry adapterRegistry = ReactiveAdapterRegistry
100+
.getSharedInstance();
101+
102+
/**
103+
* Sets the {@link BeanResolver} to be used on the expressions
104+
* @param beanResolver the {@link BeanResolver} to use
105+
*/
106+
public void setBeanResolver(BeanResolver beanResolver) {
107+
this.beanResolver = beanResolver;
108+
}
109+
110+
/**
111+
* Sets the {@link ReactiveAdapterRegistry} to be used.
112+
* @param adapterRegistry the {@link ReactiveAdapterRegistry} to use. Cannot be null. Default is
113+
* {@link ReactiveAdapterRegistry#getSharedInstance()}
114+
*/
115+
public void setAdapterRegistry(ReactiveAdapterRegistry adapterRegistry) {
116+
Assert.notNull(adapterRegistry, "adapterRegistry cannot be null");
117+
this.adapterRegistry = adapterRegistry;
118+
}
119+
120+
@Override
121+
public boolean supportsParameter(MethodParameter parameter) {
122+
return findMethodAnnotation(AuthenticationPrincipal.class, parameter) != null;
123+
}
124+
125+
public Mono<Object> resolveArgument(MethodParameter parameter, Message<?> message) {
126+
ReactiveAdapter adapter = this.adapterRegistry
127+
.getAdapter(parameter.getParameterType());
128+
return ReactiveSecurityContextHolder.getContext()
129+
.map(SecurityContext::getAuthentication).flatMap(a -> {
130+
Object p = resolvePrincipal(parameter, a.getPrincipal());
131+
Mono<Object> principal = Mono.justOrEmpty(p);
132+
return adapter == null ?
133+
principal :
134+
Mono.just(adapter.fromPublisher(principal));
135+
});
136+
}
137+
138+
private Object resolvePrincipal(MethodParameter parameter, Object principal) {
139+
AuthenticationPrincipal authPrincipal = findMethodAnnotation(
140+
AuthenticationPrincipal.class, parameter);
141+
142+
String expressionToParse = authPrincipal.expression();
143+
if (StringUtils.hasLength(expressionToParse)) {
144+
StandardEvaluationContext context = new StandardEvaluationContext();
145+
context.setRootObject(principal);
146+
context.setVariable("this", principal);
147+
context.setBeanResolver(this.beanResolver);
148+
149+
Expression expression = this.parser.parseExpression(expressionToParse);
150+
principal = expression.getValue(context);
151+
}
152+
153+
if (isInvalidType(parameter, principal)) {
154+
155+
if (authPrincipal.errorOnInvalidType()) {
156+
throw new ClassCastException(
157+
principal + " is not assignable to " + parameter
158+
.getParameterType());
159+
}
160+
else {
161+
return null;
162+
}
163+
}
164+
165+
return principal;
166+
}
167+
168+
private boolean isInvalidType(MethodParameter parameter, Object principal) {
169+
if (principal == null) {
170+
return false;
171+
}
172+
Class<?> typeToCheck = parameter.getParameterType();
173+
boolean isParameterPublisher = Publisher.class
174+
.isAssignableFrom(parameter.getParameterType());
175+
if (isParameterPublisher) {
176+
ResolvableType resolvableType = ResolvableType.forMethodParameter(parameter);
177+
Class<?> genericType = resolvableType.resolveGeneric(0);
178+
if (genericType == null) {
179+
return false;
180+
}
181+
typeToCheck = genericType;
182+
}
183+
return !typeToCheck.isAssignableFrom(principal.getClass());
184+
}
185+
186+
/**
187+
* Obtains the specified {@link Annotation} on the specified {@link MethodParameter}.
188+
*
189+
* @param annotationClass the class of the {@link Annotation} to find on the
190+
* {@link MethodParameter}
191+
* @param parameter the {@link MethodParameter} to search for an {@link Annotation}
192+
* @return the {@link Annotation} that was found or null.
193+
*/
194+
private <T extends Annotation> T findMethodAnnotation(Class<T> annotationClass,
195+
MethodParameter parameter) {
196+
T annotation = parameter.getParameterAnnotation(annotationClass);
197+
if (annotation != null) {
198+
return annotation;
199+
}
200+
Annotation[] annotationsToSearch = parameter.getParameterAnnotations();
201+
for (Annotation toSearch : annotationsToSearch) {
202+
annotation = AnnotationUtils
203+
.findAnnotation(toSearch.annotationType(), annotationClass);
204+
if (annotation != null) {
205+
return annotation;
206+
}
207+
}
208+
return null;
209+
}
210+
}

0 commit comments

Comments
 (0)