Bladeren bron

SEC-2298: Add AuthenticationPrincipalArgumentResolver

Rob Winch 12 jaren geleden
bovenliggende
commit
6e9fb7930b

+ 2 - 2
config/src/main/java/org/springframework/security/config/annotation/web/configuration/SpringWebMvcImportSelector.java

@@ -21,7 +21,7 @@ import org.springframework.util.ClassUtils;
 
 /**
  * Used by {@link EnableWebSecurity} to conditionaly import
- * {@link CsrfWebMvcConfiguration} when the DispatcherServlet is present on the
+ * {@link WebMvcSecurityConfiguration} when the DispatcherServlet is present on the
  * classpath.
  *
  * @author Rob Winch
@@ -34,6 +34,6 @@ class SpringWebMvcImportSelector implements ImportSelector {
      */
     public String[] selectImports(AnnotationMetadata importingClassMetadata) {
         boolean webmvcPresent = ClassUtils.isPresent("org.springframework.web.servlet.DispatcherServlet", getClass().getClassLoader());
-        return webmvcPresent ? new String[] {"org.springframework.security.config.annotation.web.configuration.CsrfWebMvcConfiguration"} : new String[] {};
+        return webmvcPresent ? new String[] {"org.springframework.security.config.annotation.web.configuration.WebMvcSecurityConfiguration"} : new String[] {};
     }
 }

+ 15 - 2
config/src/main/java/org/springframework/security/config/annotation/web/configuration/CsrfWebMvcConfiguration.java → config/src/main/java/org/springframework/security/config/annotation/web/configuration/WebMvcSecurityConfiguration.java

@@ -15,23 +15,36 @@
  */
 package org.springframework.security.config.annotation.web.configuration;
 
+import java.util.List;
+
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
+import org.springframework.security.web.bind.support.AuthenticationPrincipalArgumentResolver;
 import org.springframework.security.web.servlet.support.csrf.CsrfRequestDataValueProcessor;
+import org.springframework.web.method.support.HandlerMethodArgumentResolver;
 import org.springframework.web.servlet.config.annotation.EnableWebMvc;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
 import org.springframework.web.servlet.support.RequestDataValueProcessor;
 
 /**
  * Used to add a {@link RequestDataValueProcessor} for Spring MVC and Spring
  * Security CSRF integration. This configuration is added whenever
  * {@link EnableWebMvc} is added by {@link SpringWebMvcImportSelector} and the
- * DispatcherServlet is present on the classpath.
+ * DispatcherServlet is present on the classpath. It also adds the
+ * {@link AuthenticationPrincipalArgumentResolver} as a
+ * {@link HandlerMethodArgumentResolver}.
  *
  * @author Rob Winch
  * @since 3.2
  */
 @Configuration
-class CsrfWebMvcConfiguration {
+class WebMvcSecurityConfiguration extends WebMvcConfigurerAdapter {
+
+    @Override
+    public void addArgumentResolvers(
+            List<HandlerMethodArgumentResolver> argumentResolvers) {
+        argumentResolvers.add(new AuthenticationPrincipalArgumentResolver());
+    }
 
     @Bean
     public RequestDataValueProcessor requestDataValueProcessor() {

+ 40 - 0
web/src/main/java/org/springframework/security/web/bind/annotation/AuthenticationPrincipal.java

@@ -0,0 +1,40 @@
+/*
+ * Copyright 2002-2013 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.web.bind.annotation;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+import org.springframework.security.core.Authentication;
+
+/**
+ * Annotation that binds a method parameter or method return value to the
+ * {@link Authentication#getPrincipal()}. This is necessary to signal that the
+ * argument should be resolved to the current user rather than a user that might
+ * be edited on a form.
+ *
+ * @author Rob Winch
+ * @since 3.2
+ */
+@Target({ ElementType.PARAMETER, ElementType.ANNOTATION_TYPE })
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface AuthenticationPrincipal {
+
+}

+ 114 - 0
web/src/main/java/org/springframework/security/web/bind/support/AuthenticationPrincipalArgumentResolver.java

@@ -0,0 +1,114 @@
+/*
+ * Copyright 2002-2013 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.web.bind.support;
+
+import java.lang.annotation.Annotation;
+
+import org.springframework.core.MethodParameter;
+import org.springframework.core.annotation.AnnotationUtils;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.web.bind.annotation.AuthenticationPrincipal;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.support.WebDataBinderFactory;
+import org.springframework.web.context.request.NativeWebRequest;
+import org.springframework.web.method.support.HandlerMethodArgumentResolver;
+import org.springframework.web.method.support.ModelAndViewContainer;
+
+
+/**
+ * Allows resolving the {@link Authentication#getPrincipal()} using the
+ * {@link AuthenticationPrincipal} annotation. For example, the following
+ * {@link Controller}:
+ *
+ * <pre>
+ * @Controller
+ * public class MyController {
+ *     @RequestMapping("/user/current/show")
+ *     public String show(@AuthenticationPrincipal CustomUser customUser) {
+ *         // do something with CustomUser
+ *         return "view";
+ *     }
+ * </pre>
+ *
+ * <p>Will resolve the CustomUser argument using
+ * {@link Authentication#getPrincipal()} from the {@link SecurityContextHolder}.
+ * If the {@link Authentication} or {@link Authentication#getPrincipal()} is
+ * null, it will return null. If the types do not match, a
+ * {@link ClassCastException} will be thrown.</p>
+ *
+ * <p>Alternatively, users can create a custom meta annotation as shown below:</p>
+ * <pre>
+ *   @Target({ ElementType.PARAMETER})
+ *   @Retention(RetentionPolicy.RUNTIME)
+ *   @AuthenticationPrincipal
+ *   public @interface CurrentUser { }
+ * </pre>
+ *
+ * <p>The custom annotation can then be used instead. For example:</p>
+ *
+ * <pre>
+ * @Controller
+ * public class MyController {
+ *     @RequestMapping("/user/current/show")
+ *     public String show(@CurrentUser CustomUser customUser) {
+ *         // do something with CustomUser
+ *         return "view";
+ *     }
+ * </pre>
+ *
+ * @author Rob Winch
+ * @since 3.2
+ */
+public final class AuthenticationPrincipalArgumentResolver implements
+    HandlerMethodArgumentResolver {
+
+    /* (non-Javadoc)
+     * @see org.springframework.web.method.support.HandlerMethodArgumentResolver#supportsParameter(org.springframework.core.MethodParameter)
+     */
+    public boolean supportsParameter(MethodParameter parameter) {
+        if(parameter.getParameterAnnotation(AuthenticationPrincipal.class) != null) {
+            return true;
+        }
+        Annotation[] annotationsToSearch = parameter.getParameterAnnotations();
+        for(Annotation toSearch : annotationsToSearch) {
+            if(AnnotationUtils.findAnnotation(toSearch.annotationType(), AuthenticationPrincipal.class) != null) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /* (non-Javadoc)
+     * @see org.springframework.web.method.support.HandlerMethodArgumentResolver#resolveArgument(org.springframework.core.MethodParameter, org.springframework.web.method.support.ModelAndViewContainer, org.springframework.web.context.request.NativeWebRequest, org.springframework.web.bind.support.WebDataBinderFactory)
+     */
+    public Object resolveArgument(MethodParameter parameter,
+            ModelAndViewContainer mavContainer, NativeWebRequest webRequest,
+            WebDataBinderFactory binderFactory) throws Exception {
+        if(!supportsParameter(parameter)) {
+            return null;
+        }
+        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
+        if(authentication == null) {
+            return null;
+        }
+        Object principal = authentication.getPrincipal();
+        if(principal != null && !parameter.getParameterType().isAssignableFrom(principal.getClass())) {
+            throw new ClassCastException(principal + " is not assiable to " + parameter.getParameterType());
+        }
+        return principal;
+    }
+}

+ 165 - 0
web/src/test/java/org/springframework/security/web/bind/support/AuthenticationPrincipalArgumentResolverTests.java

@@ -0,0 +1,165 @@
+/*
+ * Copyright 2002-2013 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.web.bind.support;
+
+
+import static org.fest.assertions.Assertions.assertThat;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.lang.reflect.Method;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.springframework.core.MethodParameter;
+import org.springframework.security.authentication.TestingAuthenticationToken;
+import org.springframework.security.core.authority.AuthorityUtils;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.core.userdetails.User;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.web.bind.annotation.AuthenticationPrincipal;
+import org.springframework.util.ReflectionUtils;
+
+/**
+ * @author Rob Winch
+ *
+ */
+public class AuthenticationPrincipalArgumentResolverTests {
+    private Object expectedPrincipal;
+    private AuthenticationPrincipalArgumentResolver resolver;
+
+    @Before
+    public void setup() {
+        resolver = new AuthenticationPrincipalArgumentResolver();
+    }
+
+    @After
+    public void cleanup() {
+        SecurityContextHolder.clearContext();
+    }
+
+    @Test
+    public void resolveArgumentNullAuthentication() throws Exception {
+        assertThat(resolver.resolveArgument(showUserAnnotationString(), null, null, null)).isNull();
+    }
+
+    @Test
+    public void resolveArgumentNullPrincipal() throws Exception {
+        setAuthenticationPrincipal(null);
+        assertThat(resolver.resolveArgument(showUserAnnotationString(), null, null, null)).isNull();
+    }
+
+    @Test
+    public void resolveArgumentNoAnnotation() throws Exception {
+        setAuthenticationPrincipal("john");
+        assertThat(resolver.resolveArgument(showUserNoAnnotation(), null, null, null)).isNull();
+    }
+
+    @Test
+    public void resolveArgumentString() throws Exception {
+        setAuthenticationPrincipal("john");
+        assertThat(resolver.resolveArgument(showUserAnnotationString(), null, null, null)).isEqualTo(expectedPrincipal);
+    }
+
+    @Test
+    public void resolveArgumentPrincipalStringOnObject() throws Exception {
+        setAuthenticationPrincipal("john");
+        assertThat(resolver.resolveArgument(showUserAnnotationObject(), null, null, null)).isEqualTo(expectedPrincipal);
+    }
+
+    @Test
+    public void resolveArgumentUserDetails() throws Exception {
+        setAuthenticationPrincipal(new User("user", "password", AuthorityUtils.createAuthorityList("ROLE_USER")));
+        assertThat(resolver.resolveArgument(showUserAnnotationUserDetails(), null, null, null)).isEqualTo(expectedPrincipal);
+    }
+
+    @Test
+    public void resolveArgumentCustomUserPrincipal() throws Exception {
+        setAuthenticationPrincipal(new CustomUserPrincipal());
+        assertThat(resolver.resolveArgument(showUserAnnotationCustomUserPrincipal(), null, null, null)).isEqualTo(expectedPrincipal);
+    }
+
+    @Test
+    public void resolveArgumentCustomAnnotation() throws Exception {
+        setAuthenticationPrincipal(new CustomUserPrincipal());
+        assertThat(resolver.resolveArgument(showUserCustomAnnotation(), null, null, null)).isEqualTo(expectedPrincipal);
+    }
+
+    @Test(expected = ClassCastException.class)
+    public void resolveArgumentClassCastException() throws Exception {
+        setAuthenticationPrincipal(new CustomUserPrincipal());
+        resolver.resolveArgument(showUserAnnotationString(), null, null, null);
+    }
+
+    @Test
+    public void resolveArgumentObject() throws Exception {
+        setAuthenticationPrincipal(new Object());
+        assertThat(resolver.resolveArgument(showUserAnnotationObject(), null, null, null)).isEqualTo(expectedPrincipal);
+    }
+
+    private MethodParameter showUserNoAnnotation() {
+        return getMethodParameter("showUserNoAnnotation", String.class);
+    }
+
+    private MethodParameter showUserAnnotationString() {
+        return getMethodParameter("showUserAnnotation", String.class);
+    }
+
+    private MethodParameter showUserAnnotationUserDetails() {
+        return getMethodParameter("showUserAnnotation", UserDetails.class);
+    }
+
+    private MethodParameter showUserAnnotationCustomUserPrincipal() {
+        return getMethodParameter("showUserAnnotation", CustomUserPrincipal.class);
+    }
+
+    private MethodParameter showUserCustomAnnotation() {
+        return getMethodParameter("showUserCustomAnnotation", CustomUserPrincipal.class);
+    }
+
+    private MethodParameter showUserAnnotationObject() {
+        return getMethodParameter("showUserAnnotation", Object.class);
+    }
+
+    private MethodParameter getMethodParameter(String methodName, Class<?>... paramTypes) {
+        Method method = ReflectionUtils.findMethod(TestController.class, methodName,paramTypes);
+        return new MethodParameter(method,0);
+    }
+
+    @Target({ ElementType.PARAMETER})
+    @Retention(RetentionPolicy.RUNTIME)
+    @AuthenticationPrincipal
+    static @interface CurrentUser { }
+
+    public static class TestController {
+        public void showUserNoAnnotation(String user) {}
+        public void showUserAnnotation(@AuthenticationPrincipal String user) {}
+        public void showUserAnnotation(@AuthenticationPrincipal UserDetails user) {}
+        public void showUserAnnotation(@AuthenticationPrincipal CustomUserPrincipal user) {}
+        public void showUserCustomAnnotation(@CurrentUser CustomUserPrincipal user) {}
+        public void showUserAnnotation(@AuthenticationPrincipal Object user) {}
+    }
+
+    private static class CustomUserPrincipal {}
+
+    private void setAuthenticationPrincipal(Object principal) {
+        this.expectedPrincipal = principal;
+        SecurityContextHolder.getContext().setAuthentication(new TestingAuthenticationToken(expectedPrincipal, "password", "ROLE_USER"));
+    }
+}

+ 1 - 0
web/template.mf

@@ -35,6 +35,7 @@ Import-Template:
  org.springframework.jdbc.*;version="${springRange}";resolution:=optional,
  org.springframework.mock.web;version="${springRange}";resolution:=optional,
  org.springframework.web.*;version="${springRange}";resolution:=optional,
+ org.springframework.core.annotation.*;version="${springRange}";resolution:=optional,,
  org.springframework.web.context.*;version="${springRange}";resolution:=optional,
  org.springframework.web.filter.*;version="${springRange}",
  org.springframework.web.*;version="${springRange}",