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:
parent
78bf6e2bd5
commit
9745de9510
|
@ -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>
|
||||
* @AuthenticationPrincipal(expression = "customUser")
|
||||
* </pre>
|
||||
*
|
||||
* @return the expression to use.
|
||||
*/
|
||||
String expression() default "";
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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) {
|
||||
|
|
Loading…
Reference in New Issue