浏览代码

SEC-2859: Add CsrfTokenArgumentResolver

Rob Winch 10 年之前
父节点
当前提交
a27c33754c

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

@@ -17,6 +17,7 @@ package org.springframework.security.config.annotation.web.configuration;
 
 import org.springframework.context.annotation.Bean;
 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;
 import org.springframework.web.method.support.HandlerMethodArgumentResolver;
 import org.springframework.web.servlet.config.annotation.EnableWebMvc;
@@ -44,6 +45,7 @@ class WebMvcSecurityConfiguration extends WebMvcConfigurerAdapter {
             List<HandlerMethodArgumentResolver> argumentResolvers) {
         argumentResolvers.add(new AuthenticationPrincipalArgumentResolver());
         argumentResolvers.add(new org.springframework.security.web.bind.support.AuthenticationPrincipalArgumentResolver());
+        argumentResolvers.add(new CsrfTokenArgumentResolver());
     }
 
     @ConditionalOnMissingBean(RequestDataValueProcessor.class)

+ 138 - 0
config/src/test/java/org/springframework/security/config/annotation/web/configuration/WebMvcSecurityConfigurationTests.java

@@ -0,0 +1,138 @@
+/*
+ * Copyright 2002-2015 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.*;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
+
+import org.junit.After;
+import org.junit.Before;
+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.context.annotation.Configuration;
+import org.springframework.security.authentication.TestingAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.annotation.AuthenticationPrincipal;
+import org.springframework.security.core.authority.AuthorityUtils;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.web.csrf.CsrfToken;
+import org.springframework.security.web.csrf.DefaultCsrfToken;
+import org.springframework.stereotype.Controller;
+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.ResultMatcher;
+import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
+import org.springframework.test.web.servlet.setup.DefaultMockMvcBuilder;
+import org.springframework.test.web.servlet.setup.MockMvcBuilders;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.context.WebApplicationContext;
+import org.springframework.web.servlet.ModelAndView;
+import org.springframework.web.servlet.config.annotation.EnableWebMvc;
+
+/**
+ * @author Rob Winch
+ */
+@RunWith(SpringJUnit4ClassRunner.class)
+@ContextConfiguration
+@WebAppConfiguration
+public class WebMvcSecurityConfigurationTests {
+
+    @Autowired
+    WebApplicationContext context;
+
+    MockMvc mockMvc;
+
+    Authentication authentication;
+
+    @Before
+    public void setup() {
+        mockMvc = MockMvcBuilders.webAppContextSetup(context).build();
+        authentication = new TestingAuthenticationToken("user","password", AuthorityUtils.createAuthorityList("ROLE_USER"));
+        SecurityContextHolder.getContext().setAuthentication(authentication);
+    }
+
+    @After
+    public void cleanup() {
+        SecurityContextHolder.clearContext();
+    }
+
+    @Test
+    public void authenticationPrincipalResolved() throws Exception {
+        mockMvc
+                .perform(get("/authentication-principal"))
+                .andExpect(assertResult(authentication.getPrincipal()))
+                .andExpect(view().name("authentication-principal-view"));
+    }
+
+    @Test
+    public void deprecatedAuthenticationPrincipalResolved() throws Exception {
+        mockMvc
+                .perform(get("/deprecated-authentication-principal"))
+                .andExpect(assertResult(authentication.getPrincipal()))
+                .andExpect(view().name("deprecated-authentication-principal-view"));
+    }
+
+    @Test
+    public void csrfToken() throws Exception {
+        CsrfToken csrfToken = new DefaultCsrfToken("headerName", "paramName", "token");
+        MockHttpServletRequestBuilder request =
+                get("/csrf")
+                .requestAttr(CsrfToken.class.getName(), csrfToken);
+
+        mockMvc
+                .perform(request)
+                .andExpect(assertResult(csrfToken));
+    }
+
+    private ResultMatcher assertResult(Object expected) {
+        return model().attribute("result", expected);
+    }
+
+    @Controller
+    static class TestController {
+
+        @RequestMapping("/authentication-principal")
+        public ModelAndView authenticationPrincipal(@AuthenticationPrincipal String principal) {
+            return new ModelAndView("authentication-principal-view", "result", principal);
+        }
+
+        @RequestMapping("/deprecated-authentication-principal")
+        public ModelAndView deprecatedAuthenticationPrincipal(@org.springframework.security.web.bind.annotation.AuthenticationPrincipal String principal) {
+            return new ModelAndView("deprecated-authentication-principal-view", "result", principal);
+        }
+
+        @RequestMapping("/csrf")
+        public ModelAndView csrf(CsrfToken token) {
+            return new ModelAndView("view", "result", token);
+        }
+    }
+
+    @Configuration
+    @EnableWebMvc
+    @EnableWebSecurity
+    static class Config {
+        @Bean
+        public TestController testController() {
+            return new TestController();
+        }
+    }
+
+}

+ 27 - 1
docs/manual/src/docs/asciidoc/index.adoc

@@ -6040,7 +6040,7 @@ Spring Security provides a number of optional integrations with Spring MVC. This
 
 WARN: As of Spring Security 4.0, `@EnableWebMvcSecurity` is deprecated. The replacement is `@EnableWebSecurity` which will determine adding the Spring MVC features based upon the classpath.
 
-To enable Spring Security integration with Spring MVC add the `@EnableWebSecurity` annotation to your configuration. A typical example will look something like this:
+To enable Spring Security integration with Spring MVC add the `@EnableWebSecurity` annotation to your configuration.
 
 
 [[mvc-authentication-principal]]
@@ -6134,6 +6134,8 @@ There is no automatic integration with a `DeferredResult` that is returned by co
 [[mvc-csrf]]
 === Spring MVC and CSRF Integration
 
+==== Automatic Token Inclusion
+
 Spring Security will automatically <<csrf-include-csrf-token,include the CSRF Token>> within forms that use the http://docs.spring.io/spring/docs/3.2.x/spring-framework-reference/html/view.html#view-jsp-formtaglib-formtag[Spring MVC form tag]. For example, the following JSP:
 
 [source,xml]
@@ -6174,6 +6176,30 @@ Will output HTML that is similar to the following:
 <!-- ... -->
 ----
 
+[[mvc-csrf-resolver]]
+==== Resolving the CsrfToken
+
+Spring Security provides `CsrfTokenResolver` which can automatically resolve the current `CsrfToken` for Spring MVC arguments.
+By using <<mvc-enablewebsecurity>> you will automatically have this added to your Spring MVC configuration.
+If you use XML based configuraiton, you must add this yourself.
+
+Once `CsrfTokenResolver` is properly configured, you can expose the `CsrfToken` to your static HTML based application.
+
+[source,java]
+----
+@RestController
+public class CsrfController {
+
+    @RequestMapping("/csrf")
+    public CsrfToken csrf(CsrfToken token) {
+        return token;
+    }
+}
+----
+
+It is important to keep the `CsrfToken` a secret from other domains.
+This means if you are using https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS[Cross Origin Sharing (CORS)], you should **NOT** expose the `CsrfToken` to any external domains.
+
 = Appendix
 
 [[appendix-schema]]

+ 64 - 0
web/src/main/java/org/springframework/security/web/method/annotation/CsrfTokenArgumentResolver.java

@@ -0,0 +1,64 @@
+/*
+ * 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.method.annotation;
+
+import org.springframework.core.MethodParameter;
+import org.springframework.security.web.csrf.CsrfToken;
+import org.springframework.web.bind.annotation.RestController;
+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 current {@link CsrfToken}. For example, the following
+ * {@link RestController} will resolve the current {@link CsrfToken}:
+ *
+ * <pre>
+ * @RestController
+ * public class MyController {
+ *     @MessageMapping("/im")
+ *     public CsrfToken csrf(CsrfToken token) {
+ *         return token;
+ *     }
+ * </pre>
+ *
+ *
+ * @author Rob Winch
+ * @since 4.0
+ */
+public final class CsrfTokenArgumentResolver implements
+    HandlerMethodArgumentResolver {
+
+
+    /* (non-Javadoc)
+     * @see org.springframework.web.method.support.HandlerMethodArgumentResolver#supportsParameter(org.springframework.core.MethodParameter)
+     */
+    public boolean supportsParameter(MethodParameter parameter) {
+        return CsrfToken.class.equals(parameter.getParameterType());
+    }
+
+    /* (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 {
+        CsrfToken token = (CsrfToken) webRequest.getAttribute(CsrfToken.class.getName(), NativeWebRequest.SCOPE_REQUEST);
+        return token;
+    }
+}

+ 104 - 0
web/src/test/java/org/springframework/security/web/method/annotation/CsrfTokenArgumentResolverTests.java

@@ -0,0 +1,104 @@
+/*
+ * Copyright 2002-2015 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.method.annotation;
+
+import static org.fest.assertions.Assertions.assertThat;
+
+import java.lang.reflect.Method;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.runners.MockitoJUnitRunner;
+import org.springframework.core.MethodParameter;
+import org.springframework.mock.web.MockHttpServletRequest;
+import org.springframework.security.web.csrf.CsrfToken;
+import org.springframework.security.web.csrf.DefaultCsrfToken;
+import org.springframework.util.ReflectionUtils;
+import org.springframework.web.bind.support.WebDataBinderFactory;
+import org.springframework.web.context.request.NativeWebRequest;
+import org.springframework.web.context.request.ServletWebRequest;
+import org.springframework.web.method.support.ModelAndViewContainer;
+
+/**
+ *
+ * @author Rob Winch
+ *
+ */
+@RunWith(MockitoJUnitRunner.class)
+public class CsrfTokenArgumentResolverTests {
+    @Mock
+    private ModelAndViewContainer mavContainer;
+    @Mock
+    private WebDataBinderFactory binderFactory;
+
+    private MockHttpServletRequest request;
+    private NativeWebRequest webRequest;
+
+    private CsrfToken token;
+
+    private CsrfTokenArgumentResolver resolver;
+
+    @Before
+    public void setup() {
+        token = new DefaultCsrfToken("X-CSRF-TOKEN", "_csrf", "secret");
+        resolver = new CsrfTokenArgumentResolver();
+        request = new MockHttpServletRequest();
+        webRequest = new ServletWebRequest(request);
+    }
+
+    @Test
+    public void supportsParameterFalse() {
+        assertThat(resolver.supportsParameter(noToken())).isFalse();
+    }
+
+    @Test
+    public void supportsParameterTrue() {
+        assertThat(resolver.supportsParameter(token())).isTrue();
+    }
+
+    @Test
+    public void resolveArgumentNotFound() throws Exception {
+        assertThat(resolver.resolveArgument(token(), mavContainer, webRequest, binderFactory)).isNull();
+    }
+
+    @Test
+    public void resolveArgumentFound() throws Exception {
+        request.setAttribute(CsrfToken.class.getName(), token);
+
+        assertThat(resolver.resolveArgument(token(), mavContainer, webRequest, binderFactory)).isSameAs(token);
+    }
+
+    private MethodParameter noToken() {
+        return getMethodParameter("noToken", String.class);
+    }
+
+    private MethodParameter token() {
+        return getMethodParameter("token", CsrfToken.class);
+    }
+
+    private MethodParameter getMethodParameter(String methodName, Class<?>... paramTypes) {
+        Method method = ReflectionUtils.findMethod(TestController.class, methodName,paramTypes);
+        return new MethodParameter(method,0);
+    }
+
+    public static class TestController {
+        public void noToken(String user) {}
+        public void token(CsrfToken token) {}
+    }
+
+}