Browse Source

Add BeanResolver to AuthenticationPrincipalArgumentResolver

Previously @AuthenticationPrincipal's expression attribute didn't support
bean references because the BeanResolver was not set on the SpEL context.

This commit adds a BeanResolver and ensures that the configuration
sets a BeanResolver.

Fixes gh-3949
Rob Winch 8 years ago
parent
commit
aaa9708b95

+ 17 - 4
config/src/main/java/org/springframework/security/config/annotation/web/configuration/WebMvcSecurityConfiguration.java

@@ -15,7 +15,14 @@
  */
 package org.springframework.security.config.annotation.web.configuration;
 
+import java.util.List;
+
+import org.springframework.beans.BeansException;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.ApplicationContextAware;
 import org.springframework.context.annotation.Bean;
+import org.springframework.context.expression.BeanFactoryResolver;
+import org.springframework.expression.BeanResolver;
 import org.springframework.security.web.method.annotation.AuthenticationPrincipalArgumentResolver;
 import org.springframework.security.web.method.annotation.CsrfTokenArgumentResolver;
 import org.springframework.security.web.servlet.support.csrf.CsrfRequestDataValueProcessor;
@@ -24,8 +31,6 @@ import org.springframework.web.servlet.config.annotation.EnableWebMvc;
 import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
 import org.springframework.web.servlet.support.RequestDataValueProcessor;
 
-import java.util.List;
-
 /**
  * Used to add a {@link RequestDataValueProcessor} for Spring MVC and Spring Security CSRF
  * integration. This configuration is added whenever {@link EnableWebMvc} is added by
@@ -36,12 +41,15 @@ import java.util.List;
  * @author Rob Winch
  * @since 3.2
  */
-class WebMvcSecurityConfiguration extends WebMvcConfigurerAdapter {
+class WebMvcSecurityConfiguration extends WebMvcConfigurerAdapter implements ApplicationContextAware {
+	private BeanResolver beanResolver;
 
 	@Override
 	@SuppressWarnings("deprecation")
 	public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
-		argumentResolvers.add(new AuthenticationPrincipalArgumentResolver());
+		AuthenticationPrincipalArgumentResolver authenticationPrincipalResolver = new AuthenticationPrincipalArgumentResolver();
+		authenticationPrincipalResolver.setBeanResolver(beanResolver);
+		argumentResolvers.add(authenticationPrincipalResolver);
 		argumentResolvers
 				.add(new org.springframework.security.web.bind.support.AuthenticationPrincipalArgumentResolver());
 		argumentResolvers.add(new CsrfTokenArgumentResolver());
@@ -51,4 +59,9 @@ class WebMvcSecurityConfiguration extends WebMvcConfigurerAdapter {
 	public RequestDataValueProcessor requestDataValueProcessor() {
 		return new CsrfRequestDataValueProcessor();
 	}
+
+	@Override
+	public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
+		this.beanResolver = new BeanFactoryResolver(applicationContext.getAutowireCapableBeanFactory());
+	}
 }

+ 105 - 0
config/src/test/java/org/springframework/security/config/annotation/web/configuration/AuthenticationPrincipalArgumentResolverTests.java

@@ -0,0 +1,105 @@
+/*
+ * Copyright 2002-2016 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.security.config.annotation.web.configuration;
+
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+import org.junit.After;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
+import org.springframework.security.core.annotation.AuthenticationPrincipal;
+import org.springframework.security.core.authority.AuthorityUtils;
+import org.springframework.security.core.context.SecurityContext;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.core.userdetails.User;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
+import org.springframework.test.context.web.WebAppConfiguration;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.setup.MockMvcBuilders;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.context.WebApplicationContext;
+import org.springframework.web.servlet.config.annotation.EnableWebMvc;
+
+/**
+ *
+ * @author Rob Winch
+ *
+ */
+@RunWith(SpringJUnit4ClassRunner.class)
+@ContextConfiguration
+@WebAppConfiguration
+public class AuthenticationPrincipalArgumentResolverTests {
+	@Autowired
+	WebApplicationContext wac;
+
+	@After
+	public void cleanup() {
+		SecurityContextHolder.clearContext();
+	}
+
+	@Test
+	public void authenticationPrincipalExpressionWhenBeanExpressionSuppliedThenBeanUsed() throws Exception {
+		User user = new User("user", "password", AuthorityUtils.createAuthorityList("ROLE_USER"));
+		SecurityContext context = SecurityContextHolder.createEmptyContext();
+		context.setAuthentication(new UsernamePasswordAuthenticationToken(user, user.getPassword(), user.getAuthorities()));
+		SecurityContextHolder.setContext(context);
+
+		MockMvc mockMvc = MockMvcBuilders
+			.webAppContextSetup(wac)
+			.build();
+
+		mockMvc.perform(get("/users/self"))
+			.andExpect(status().isOk())
+			.andExpect(content().string("extracted-user"));
+	}
+
+	@EnableWebSecurity
+	@EnableWebMvc
+	static class Config {
+		@Autowired
+		public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
+			auth
+				.inMemoryAuthentication();
+		}
+
+		@Bean
+		public UsernameExtractor usernameExtractor() {
+			return new UsernameExtractor();
+		}
+
+		@RestController
+		static class UserController {
+			@GetMapping("/users/self")
+			public String usersSelf(@AuthenticationPrincipal(expression = "@usernameExtractor.extract(#this)") String userName) {
+				return userName;
+			}
+		}
+	}
+
+	static class UsernameExtractor {
+		public String extract(User u) {
+			return "extracted-" + u.getUsername();
+		}
+	}
+}

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

@@ -6864,6 +6864,26 @@ public ModelAndView findMessagesForUser(@AuthenticationPrincipal(expression = "c
 }
 ----
 
+We can also refer to Beans in our SpEL expressions.
+For example, the following could be used if we were using JPA to manage our Users and we wanted to modify and save a propoerty on the current user.
+
+[source,java]
+----
+import org.springframework.security.core.annotation.AuthenticationPrincipal;
+
+// ...
+
+@PutMapping("/users/self")
+public ModelAndView updateName(@AuthenticationPrincipal(expression = "@jpaEntityManager.merge(#this)") CustomUser attachedCustomUser
+		@RequestParam String firstName) {
+
+	// change the firstName on an atached instance which will be persisted to the database
+	attachedCustomUser.setFirstName(firstName);
+
+	// ...
+}
+----
+
 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.

+ 12 - 0
web/src/main/java/org/springframework/security/web/method/annotation/AuthenticationPrincipalArgumentResolver.java

@@ -19,6 +19,7 @@ import java.lang.annotation.Annotation;
 
 import org.springframework.core.MethodParameter;
 import org.springframework.core.annotation.AnnotationUtils;
+import org.springframework.expression.BeanResolver;
 import org.springframework.expression.Expression;
 import org.springframework.expression.ExpressionParser;
 import org.springframework.expression.spel.standard.SpelExpressionParser;
@@ -88,6 +89,8 @@ public final class AuthenticationPrincipalArgumentResolver
 
 	private ExpressionParser parser = new SpelExpressionParser();
 
+	private BeanResolver beanResolver;
+
 	/*
 	 * (non-Javadoc)
 	 *
@@ -125,6 +128,7 @@ public final class AuthenticationPrincipalArgumentResolver
 			StandardEvaluationContext context = new StandardEvaluationContext();
 			context.setRootObject(principal);
 			context.setVariable("this", principal);
+			context.setBeanResolver(beanResolver);
 
 			Expression expression = this.parser.parseExpression(expressionToParse);
 			principal = expression.getValue(context);
@@ -144,6 +148,14 @@ public final class AuthenticationPrincipalArgumentResolver
 		return principal;
 	}
 
+	/**
+	 * Sets the {@link BeanResolver} to be used on the expressions
+	 * @param beanResolver the {@link BeanResolver} to use
+	 */
+	public void setBeanResolver(BeanResolver beanResolver) {
+		this.beanResolver = beanResolver;
+	}
+
 	/**
 	 * Obtains the specified {@link Annotation} on the specified {@link MethodParameter}.
 	 *