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) {