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
This commit is contained in:
Rob Winch 2016-05-03 15:42:04 -05:00 committed by Joe Grandja
parent 78bf6e2bd5
commit 9745de9510
6 changed files with 267 additions and 9 deletions

View File

@ -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 "";
}

View File

@ -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.

View File

@ -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());

View File

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

View File

@ -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());

View File

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