2
0
Эх сурвалжийг харах

Add @AuthenticationPrincipal expression

It is now possible to provide a SpEL expression for
@AuthenticationPrincipal. This allows invoking custom logic including
methods on the principal object.

Fixes gh-3859
Rob Winch 9 жил өмнө
parent
commit
9745de9510

+ 32 - 3
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: <a href="{@docRoot}/org/springframework/security/web/method/annotation/AuthenticationPrincipalArgumentResolver.html">
- *  AuthenticationPrincipalArgumentResolver
- *  </a>
+ * See: <a href=
+ * "{@docRoot}/org/springframework/security/web/method/annotation/AuthenticationPrincipalArgumentResolver.html"
+ * > AuthenticationPrincipalArgumentResolver </a>
  */
 @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.
+	 *
+	 * <p>
+	 * 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:
+	 * </p>
+	 *
+	 * <pre>
+	 * public class CustomUserUserDetails extends User {
+	 *     // ...
+	 *     public CustomUser getCustomUser() {
+	 *         return customUser;
+	 *     }
+	 * }
+	 * </pre>
+	 *
+	 * Then the user can specify an annotation that looks like:
+	 *
+	 * <pre>
+	 * &#64;AuthenticationPrincipal(expression = "customUser")
+	 * </pre>
+	 *
+	 * @return the expression to use.
+	 */
+	String expression() default "";
 }

+ 31 - 0
docs/manual/src/docs/asciidoc/index.adoc

@@ -388,6 +388,7 @@ Here is the list of improvements:
 * <<headers-hpkp,HTTP Public Key Pinning (HPKP)>>
 * <<csrf-cookie,CookieCsrfTokenRepository>> provides simple AngularJS & CSRF integration
 * Added `ForwardAuthenticationFailureHandler` & `ForwardAuthenticationSuccessHandler`
+* <<mvc-authentication-principal,AuthenticationPrincipal>> supports expression attribute to support transforming the `Authentication.getPrincipal()` object (i.e. handling immutable custom `User` domain objects)
 
 === Authorization Improvements
 * <<el-access-web-path-variables,Path Variables in Web Security Expressions>>
@@ -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.

+ 21 - 2
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());

+ 80 - 1
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) {
+		}
+	}
+
+	static class CustomUserPrincipal {
+		public final String property = "property";
 	}
 
-	private static class CustomUserPrincipal {
+	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) {

+ 22 - 2
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());

+ 81 - 1
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) {
+		}
+	}
+
+	static class CustomUserPrincipal {
+		public final String property = "property";
 	}
 
-	private static class CustomUserPrincipal {
+	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) {