diff --git a/core/src/main/java/org/springframework/security/core/annotation/AuthenticationPrincipal.java b/core/src/main/java/org/springframework/security/core/annotation/AuthenticationPrincipal.java index 678e0a2711..330f646737 100644 --- a/core/src/main/java/org/springframework/security/core/annotation/AuthenticationPrincipal.java +++ b/core/src/main/java/org/springframework/security/core/annotation/AuthenticationPrincipal.java @@ -30,9 +30,9 @@ import org.springframework.security.core.Authentication; * @author Rob Winch * @since 4.0 * - * See: - * AuthenticationPrincipalArgumentResolver - * + * See: AuthenticationPrincipalArgumentResolver */ @Target({ ElementType.PARAMETER, ElementType.ANNOTATION_TYPE }) @Retention(RetentionPolicy.RUNTIME) @@ -46,4 +46,33 @@ public @interface AuthenticationPrincipal { * @return */ boolean errorOnInvalidType() default false; + + /** + * If specified will use the provided SpEL expression to resolve the principal. This + * is convenient if users need to transform the result. + * + *

+ * For example, perhaps the user wants to resolve a CustomUser object that is final + * and is leveraging a UserDetailsService. This can be handled by returning an object + * that looks like: + *

+ * + *
+	 * public class CustomUserUserDetails extends User {
+	 *     // ...
+	 *     public CustomUser getCustomUser() {
+	 *         return customUser;
+	 *     }
+	 * }
+	 * 
+ * + * Then the user can specify an annotation that looks like: + * + *
+	 * @AuthenticationPrincipal(expression = "customUser")
+	 * 
+ * + * @return the expression to use. + */ + String expression() default ""; } diff --git a/docs/manual/src/docs/asciidoc/index.adoc b/docs/manual/src/docs/asciidoc/index.adoc index 24767ca6f8..8409cb636d 100644 --- a/docs/manual/src/docs/asciidoc/index.adoc +++ b/docs/manual/src/docs/asciidoc/index.adoc @@ -388,6 +388,7 @@ Here is the list of improvements: * <> * <> provides simple AngularJS & CSRF integration * Added `ForwardAuthenticationFailureHandler` & `ForwardAuthenticationSuccessHandler` +* <> supports expression attribute to support transforming the `Authentication.getPrincipal()` object (i.e. handling immutable custom `User` domain objects) === Authorization Improvements * <> @@ -6630,6 +6631,36 @@ public ModelAndView findMessagesForUser(@AuthenticationPrincipal CustomUser cust } ---- +Sometimes it may be necessary to transform the principal in some way. +For example, if `CustomUser` needed to be final it could not be extended. +In this situation the `UserDetailsService` might returns an `Object` that implements `UserDetails` and provides a method named `getCustomUser` to access `CustomUser`. +For example, it might look like: + +[source,java] +---- +public class CustomUserUserDetails extends User { + // ... + public CustomUser getCustomUser() { + return customUser; + } +} +---- + +We could then access the `CustomUser` using a https://docs.spring.io/spring/docs/current/spring-framework-reference/html/expressions.html[SpEL expression] that uses `Authentication.getPrincipal()` as the root object: + +[source,java] +---- +import org.springframework.security.core.annotation.AuthenticationPrincipal; + +// ... + +@RequestMapping("/messages/inbox") +public ModelAndView findMessagesForUser(@AuthenticationPrincipal(expression = "customUser") CustomUser customUser) { + + // .. find messags for this user and return them ... +} +---- + We can further remove our dependency on Spring Security by making `@AuthenticationPrincipal` a meta annotation on our own annotation. Below we demonstrate how we could do this on an annotation named `@CurrentUser`. NOTE: It is important to realize that in order to remove the dependency on Spring Security, it is the consuming application that would create `@CurrentUser`. This step is not strictly required, but assists in isolating your dependency to Spring Security to a more central location. diff --git a/messaging/src/main/java/org/springframework/security/messaging/context/AuthenticationPrincipalArgumentResolver.java b/messaging/src/main/java/org/springframework/security/messaging/context/AuthenticationPrincipalArgumentResolver.java index 4237a45594..c5f270dd6b 100644 --- a/messaging/src/main/java/org/springframework/security/messaging/context/AuthenticationPrincipalArgumentResolver.java +++ b/messaging/src/main/java/org/springframework/security/messaging/context/AuthenticationPrincipalArgumentResolver.java @@ -19,12 +19,17 @@ import java.lang.annotation.Annotation; import org.springframework.core.MethodParameter; import org.springframework.core.annotation.AnnotationUtils; +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.HandlerMethodArgumentResolver; import org.springframework.security.core.Authentication; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Controller; +import org.springframework.util.StringUtils; /** * Allows resolving the {@link Authentication#getPrincipal()} using the @@ -79,6 +84,8 @@ import org.springframework.stereotype.Controller; public final class AuthenticationPrincipalArgumentResolver implements HandlerMethodArgumentResolver { + private ExpressionParser parser = new SpelExpressionParser(); + /* * (non-Javadoc) * @@ -106,10 +113,22 @@ public final class AuthenticationPrincipalArgumentResolver return null; } Object principal = authentication.getPrincipal(); + + 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); + + Expression expression = this.parser.parseExpression(expressionToParse); + principal = expression.getValue(context); + } + if (principal != null && !parameter.getParameterType().isAssignableFrom(principal.getClass())) { - AuthenticationPrincipal authPrincipal = findMethodAnnotation( - AuthenticationPrincipal.class, parameter); if (authPrincipal.errorOnInvalidType()) { throw new ClassCastException(principal + " is not assignable to " + parameter.getParameterType()); diff --git a/messaging/src/test/java/org/springframework/security/messaging/context/AuthenticationPrincipalArgumentResolverTests.java b/messaging/src/test/java/org/springframework/security/messaging/context/AuthenticationPrincipalArgumentResolverTests.java index d56888f335..c42d5804e7 100644 --- a/messaging/src/test/java/org/springframework/security/messaging/context/AuthenticationPrincipalArgumentResolverTests.java +++ b/messaging/src/test/java/org/springframework/security/messaging/context/AuthenticationPrincipalArgumentResolverTests.java @@ -116,6 +116,24 @@ public class AuthenticationPrincipalArgumentResolverTests { expectedPrincipal); } + @Test + public void resolveArgumentSpel() throws Exception { + CustomUserPrincipal principal = new CustomUserPrincipal(); + setAuthenticationPrincipal(principal); + this.expectedPrincipal = principal.property; + assertThat(this.resolver.resolveArgument(showUserSpel(), null)) + .isEqualTo(this.expectedPrincipal); + } + + @Test + public void resolveArgumentSpelCopy() throws Exception { + CopyUserPrincipal principal = new CopyUserPrincipal("property"); + setAuthenticationPrincipal(principal); + Object resolveArgument = this.resolver.resolveArgument(showUserSpelCopy(), null); + assertThat(resolveArgument).isEqualTo(principal); + assertThat(resolveArgument).isNotSameAs(principal); + } + @Test public void resolveArgumentNullOnInvalidType() throws Exception { setAuthenticationPrincipal(new CustomUserPrincipal()); @@ -170,6 +188,14 @@ public class AuthenticationPrincipalArgumentResolverTests { return getMethodParameter("showUserCustomAnnotation", CustomUserPrincipal.class); } + private MethodParameter showUserSpel() { + return getMethodParameter("showUserSpel", String.class); + } + + private MethodParameter showUserSpelCopy() { + return getMethodParameter("showUserSpelCopy", CopyUserPrincipal.class); + } + private MethodParameter showUserAnnotationObject() { return getMethodParameter("showUserAnnotation", Object.class); } @@ -218,9 +244,62 @@ public class AuthenticationPrincipalArgumentResolverTests { public void showUserAnnotation(@AuthenticationPrincipal Object user) { } + + public void showUserSpel( + @AuthenticationPrincipal(expression = "property") String user) { + } + + public void showUserSpelCopy( + @AuthenticationPrincipal(expression = "new org.springframework.security.messaging.context.AuthenticationPrincipalArgumentResolverTests$CopyUserPrincipal(#this)") CopyUserPrincipal user) { + } } - private static class CustomUserPrincipal { + static class CustomUserPrincipal { + public final String property = "property"; + } + + static class CopyUserPrincipal { + public final String property; + + CopyUserPrincipal(String property) { + this.property = property; + } + + public CopyUserPrincipal(CopyUserPrincipal toCopy) { + this.property = toCopy.property; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + + ((this.property == null) ? 0 : this.property.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + CopyUserPrincipal other = (CopyUserPrincipal) obj; + if (this.property == null) { + if (other.property != null) { + return false; + } + } + else if (!this.property.equals(other.property)) { + return false; + } + return true; + } } private void setAuthenticationPrincipal(Object principal) { diff --git a/web/src/main/java/org/springframework/security/web/method/annotation/AuthenticationPrincipalArgumentResolver.java b/web/src/main/java/org/springframework/security/web/method/annotation/AuthenticationPrincipalArgumentResolver.java index 30acc66c70..b18513d7e7 100644 --- a/web/src/main/java/org/springframework/security/web/method/annotation/AuthenticationPrincipalArgumentResolver.java +++ b/web/src/main/java/org/springframework/security/web/method/annotation/AuthenticationPrincipalArgumentResolver.java @@ -19,10 +19,15 @@ import java.lang.annotation.Annotation; import org.springframework.core.MethodParameter; import org.springframework.core.annotation.AnnotationUtils; +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.security.core.Authentication; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Controller; +import org.springframework.util.StringUtils; import org.springframework.web.bind.support.WebDataBinderFactory; import org.springframework.web.context.request.NativeWebRequest; import org.springframework.web.method.support.HandlerMethodArgumentResolver; @@ -81,6 +86,8 @@ import org.springframework.web.method.support.ModelAndViewContainer; public final class AuthenticationPrincipalArgumentResolver implements HandlerMethodArgumentResolver { + private ExpressionParser parser = new SpelExpressionParser(); + /* * (non-Javadoc) * @@ -109,10 +116,23 @@ public final class AuthenticationPrincipalArgumentResolver return null; } Object principal = authentication.getPrincipal(); + + 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); + + Expression expression = this.parser.parseExpression(expressionToParse); + principal = expression.getValue(context); + } + if (principal != null && !parameter.getParameterType().isAssignableFrom(principal.getClass())) { - AuthenticationPrincipal authPrincipal = findMethodAnnotation( - AuthenticationPrincipal.class, parameter); + if (authPrincipal.errorOnInvalidType()) { throw new ClassCastException(principal + " is not assignable to " + parameter.getParameterType()); diff --git a/web/src/test/java/org/springframework/security/web/method/annotation/AuthenticationPrincipalArgumentResolverTests.java b/web/src/test/java/org/springframework/security/web/method/annotation/AuthenticationPrincipalArgumentResolverTests.java index 053015b829..2243a9f200 100644 --- a/web/src/test/java/org/springframework/security/web/method/annotation/AuthenticationPrincipalArgumentResolverTests.java +++ b/web/src/test/java/org/springframework/security/web/method/annotation/AuthenticationPrincipalArgumentResolverTests.java @@ -119,6 +119,25 @@ public class AuthenticationPrincipalArgumentResolverTests { .isEqualTo(expectedPrincipal); } + @Test + public void resolveArgumentSpel() throws Exception { + CustomUserPrincipal principal = new CustomUserPrincipal(); + setAuthenticationPrincipal(principal); + this.expectedPrincipal = principal.property; + assertThat(this.resolver.resolveArgument(showUserSpel(), null, null, null)) + .isEqualTo(this.expectedPrincipal); + } + + @Test + public void resolveArgumentSpelCopy() throws Exception { + CopyUserPrincipal principal = new CopyUserPrincipal("property"); + setAuthenticationPrincipal(principal); + Object resolveArgument = this.resolver.resolveArgument(showUserSpelCopy(), null, + null, null); + assertThat(resolveArgument).isEqualTo(principal); + assertThat(resolveArgument).isNotSameAs(principal); + } + @Test public void resolveArgumentNullOnInvalidType() throws Exception { setAuthenticationPrincipal(new CustomUserPrincipal()); @@ -175,6 +194,14 @@ public class AuthenticationPrincipalArgumentResolverTests { return getMethodParameter("showUserCustomAnnotation", CustomUserPrincipal.class); } + private MethodParameter showUserSpel() { + return getMethodParameter("showUserSpel", String.class); + } + + private MethodParameter showUserSpelCopy() { + return getMethodParameter("showUserSpelCopy", CopyUserPrincipal.class); + } + private MethodParameter showUserAnnotationObject() { return getMethodParameter("showUserAnnotation", Object.class); } @@ -223,9 +250,62 @@ public class AuthenticationPrincipalArgumentResolverTests { public void showUserAnnotation(@AuthenticationPrincipal Object user) { } + + public void showUserSpel( + @AuthenticationPrincipal(expression = "property") String user) { + } + + public void showUserSpelCopy( + @AuthenticationPrincipal(expression = "new org.springframework.security.web.method.annotation.AuthenticationPrincipalArgumentResolverTests$CopyUserPrincipal(#this)") CopyUserPrincipal user) { + } } - private static class CustomUserPrincipal { + static class CustomUserPrincipal { + public final String property = "property"; + } + + static class CopyUserPrincipal { + public final String property; + + CopyUserPrincipal(String property) { + this.property = property; + } + + public CopyUserPrincipal(CopyUserPrincipal toCopy) { + this.property = toCopy.property; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + + ((this.property == null) ? 0 : this.property.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + CopyUserPrincipal other = (CopyUserPrincipal) obj; + if (this.property == null) { + if (other.property != null) { + return false; + } + } + else if (!this.property.equals(other.property)) { + return false; + } + return true; + } } private void setAuthenticationPrincipal(Object principal) {