You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
682 lines
19 KiB
682 lines
19 KiB
[[mvc]] |
|
= Spring MVC Integration |
|
|
|
Spring Security provides a number of optional integrations with Spring MVC. |
|
This section covers the integration in further detail. |
|
|
|
[[mvc-enablewebmvcsecurity]] |
|
[[mvc-enablewebsecurity]] |
|
== @EnableWebSecurity |
|
|
|
To enable Spring Security integration with Spring MVC, add the `@EnableWebSecurity` annotation to your configuration. |
|
|
|
[NOTE] |
|
==== |
|
Spring Security provides the configuration by using Spring MVC's https://docs.spring.io/spring/docs/5.0.0.RELEASE/spring-framework-reference/web.html#mvc-config-customize[`WebMvcConfigurer`]. |
|
This means that, if you use more advanced options, such as integrating with `WebMvcConfigurationSupport` directly, you need to manually provide the Spring Security configuration. |
|
==== |
|
|
|
[[mvc-requestmatcher]] |
|
== PathPatternRequestMatcher |
|
|
|
Spring Security provides deep integration with how Spring MVC matches on URLs with `PathPatternRequestMatcher`. |
|
This is helpful to ensure that your Security rules match the logic used to handle your requests. |
|
|
|
`PathPatternRequestMatcher` must use the same `PathPatternParser` as Spring MVC. |
|
If you are not customizing the `PathPatternParser`, then you can do: |
|
|
|
[tabs] |
|
====== |
|
Java:: |
|
+ |
|
[source,java,role="primary"] |
|
---- |
|
@Bean |
|
PathPatternRequestMatcherBuilderFactoryBean usePathPattern() { |
|
return new PathPatternRequestMatcherBuilderFactoryBean(); |
|
} |
|
---- |
|
|
|
Kotlin:: |
|
+ |
|
[source,kotlin,role="secondary"] |
|
---- |
|
@Bean |
|
fun usePathPattern(): PathPatternRequestMatcherBuilderFactoryBean { |
|
return PathPatternRequestMatcherBuilderFactoryBean() |
|
} |
|
---- |
|
|
|
Xml:: |
|
+ |
|
[source,xml,role="secondary"] |
|
---- |
|
<b:bean class="org.springframework.security.config.web.PathPatternRequestMatcherBuilderFactoryBean"/> |
|
---- |
|
====== |
|
|
|
and Spring Security will find the appropriate Spring MVC configuration for you. |
|
|
|
If you *are* customizing Spring MVC's `PathPatternParser` instance, you will need to <<security-mvc-same-application-context, configure Spring Security and Spring MVC in the same `ApplicationContext`>>. |
|
|
|
[NOTE] |
|
==== |
|
We always recommend that you provide authorization rules by matching on the `HttpServletRequest` and method security. |
|
|
|
Providing authorization rules by matching on `HttpServletRequest` is good, because it happens very early in the code path and helps reduce the https://en.wikipedia.org/wiki/Attack_surface[attack surface]. |
|
Method security ensures that, if someone has bypassed the web authorization rules, your application is still secured. |
|
This is known as https://en.wikipedia.org/wiki/Defense_in_depth_(computing)[Defense in Depth] |
|
==== |
|
|
|
Now that Spring MVC is integrated with Spring Security, you are ready to write some xref:servlet/authorization/authorize-http-requests.adoc[authorization rules] that will use `PathPatternRequestMatcher`. |
|
|
|
[[mvc-authentication-principal]] |
|
== @AuthenticationPrincipal |
|
|
|
Spring Security provides `AuthenticationPrincipalArgumentResolver`, which can automatically resolve the current `Authentication.getPrincipal()` for Spring MVC arguments. |
|
By using `@EnableWebSecurity`, you automatically have this added to your Spring MVC configuration. |
|
If you use XML-based configuration, you must add this yourself: |
|
|
|
[source,xml] |
|
---- |
|
<mvc:annotation-driven> |
|
<mvc:argument-resolvers> |
|
<bean class="org.springframework.security.web.method.annotation.AuthenticationPrincipalArgumentResolver" /> |
|
</mvc:argument-resolvers> |
|
</mvc:annotation-driven> |
|
---- |
|
|
|
Once you have properly configured `AuthenticationPrincipalArgumentResolver`, you can entirely decouple from Spring Security in your Spring MVC layer. |
|
|
|
Consider a situation where a custom `UserDetailsService` returns an `Object` that implements `UserDetails` and your own `CustomUser` `Object`. The `CustomUser` of the currently authenticated user could be accessed by using the following code: |
|
|
|
[tabs] |
|
====== |
|
Java:: |
|
+ |
|
[source,java,role="primary"] |
|
---- |
|
@RequestMapping("/messages/inbox") |
|
public ModelAndView findMessagesForUser() { |
|
Authentication authentication = |
|
SecurityContextHolder.getContext().getAuthentication(); |
|
CustomUser custom = (CustomUser) authentication == null ? null : authentication.getPrincipal(); |
|
|
|
// .. find messages for this user and return them ... |
|
} |
|
---- |
|
|
|
Kotlin:: |
|
+ |
|
[source,kotlin,role="secondary"] |
|
---- |
|
@RequestMapping("/messages/inbox") |
|
open fun findMessagesForUser(): ModelAndView { |
|
val authentication: Authentication = SecurityContextHolder.getContext().authentication |
|
val custom: CustomUser? = if (authentication as CustomUser == null) null else authentication.principal |
|
|
|
// .. find messages for this user and return them ... |
|
} |
|
---- |
|
====== |
|
|
|
As of Spring Security 3.2, we can resolve the argument more directly by adding an annotation: |
|
|
|
[tabs] |
|
====== |
|
Java:: |
|
+ |
|
[source,java,role="primary"] |
|
---- |
|
import org.springframework.security.core.annotation.AuthenticationPrincipal; |
|
|
|
// ... |
|
|
|
@RequestMapping("/messages/inbox") |
|
public ModelAndView findMessagesForUser(@AuthenticationPrincipal CustomUser customUser) { |
|
|
|
// .. find messages for this user and return them ... |
|
} |
|
---- |
|
|
|
Kotlin:: |
|
+ |
|
[source,kotlin,role="secondary"] |
|
---- |
|
@RequestMapping("/messages/inbox") |
|
open fun findMessagesForUser(@AuthenticationPrincipal customUser: CustomUser?): ModelAndView { |
|
|
|
// .. find messages for this user and return them ... |
|
} |
|
---- |
|
====== |
|
|
|
Sometimes, you may need 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 return an `Object` that implements `UserDetails` and provides a method named `getCustomUser` to access `CustomUser`: |
|
|
|
[tabs] |
|
====== |
|
Java:: |
|
+ |
|
[source,java,role="primary"] |
|
---- |
|
public class CustomUserUserDetails extends User { |
|
// ... |
|
public CustomUser getCustomUser() { |
|
return customUser; |
|
} |
|
} |
|
---- |
|
|
|
Kotlin:: |
|
+ |
|
[source,kotlin,role="secondary"] |
|
---- |
|
class CustomUserUserDetails( |
|
username: String?, |
|
password: String?, |
|
authorities: MutableCollection<out GrantedAuthority>? |
|
) : User(username, password, authorities) { |
|
// ... |
|
val customUser: CustomUser? = null |
|
} |
|
---- |
|
====== |
|
|
|
We could then access the `CustomUser` by 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: |
|
|
|
[tabs] |
|
====== |
|
Java:: |
|
+ |
|
[source,java,role="primary"] |
|
---- |
|
import org.springframework.security.core.annotation.AuthenticationPrincipal; |
|
|
|
// ... |
|
|
|
@RequestMapping("/messages/inbox") |
|
public ModelAndView findMessagesForUser(@AuthenticationPrincipal(expression = "customUser") CustomUser customUser) { |
|
|
|
// .. find messages for this user and return them ... |
|
} |
|
---- |
|
|
|
Kotlin:: |
|
+ |
|
[source,kotlin,role="secondary"] |
|
---- |
|
import org.springframework.security.core.annotation.AuthenticationPrincipal |
|
|
|
// ... |
|
|
|
@RequestMapping("/messages/inbox") |
|
open fun findMessagesForUser(@AuthenticationPrincipal(expression = "customUser") customUser: CustomUser?): ModelAndView { |
|
|
|
// .. find messages for this user and return them ... |
|
} |
|
---- |
|
====== |
|
|
|
We can also refer to beans in our SpEL expressions. |
|
For example, we could use the following if we were using JPA to manage our users and if we wanted to modify and save a property on the current user: |
|
|
|
[tabs] |
|
====== |
|
Java:: |
|
+ |
|
[source,java,role="primary"] |
|
---- |
|
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 attached instance which will be persisted to the database |
|
attachedCustomUser.setFirstName(firstName); |
|
|
|
// ... |
|
} |
|
---- |
|
|
|
Kotlin:: |
|
+ |
|
[source,kotlin,role="secondary"] |
|
---- |
|
import org.springframework.security.core.annotation.AuthenticationPrincipal |
|
|
|
// ... |
|
|
|
@PutMapping("/users/self") |
|
open fun updateName( |
|
@AuthenticationPrincipal(expression = "@jpaEntityManager.merge(#this)") attachedCustomUser: CustomUser, |
|
@RequestParam firstName: String? |
|
): ModelAndView { |
|
|
|
// change the firstName on an attached 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. |
|
The next example demonstrates how we could do so on an annotation named `@CurrentUser`. |
|
|
|
[NOTE] |
|
==== |
|
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. |
|
==== |
|
|
|
[tabs] |
|
====== |
|
Java:: |
|
+ |
|
[source,java,role="primary"] |
|
---- |
|
@Target({ElementType.PARAMETER, ElementType.TYPE}) |
|
@Retention(RetentionPolicy.RUNTIME) |
|
@Documented |
|
@AuthenticationPrincipal |
|
public @interface CurrentUser {} |
|
---- |
|
|
|
Kotlin:: |
|
+ |
|
[source,kotlin,role="secondary"] |
|
---- |
|
@Target(AnnotationTarget.VALUE_PARAMETER, AnnotationTarget.TYPE) |
|
@Retention(AnnotationRetention.RUNTIME) |
|
@MustBeDocumented |
|
@AuthenticationPrincipal |
|
annotation class CurrentUser |
|
---- |
|
====== |
|
|
|
We have isolated our dependency on Spring Security to a single file. |
|
Now that `@CurrentUser` has been specified, we can use it to signal to resolve our `CustomUser` of the currently authenticated user: |
|
|
|
[tabs] |
|
====== |
|
Java:: |
|
+ |
|
[source,java,role="primary"] |
|
---- |
|
@RequestMapping("/messages/inbox") |
|
public ModelAndView findMessagesForUser(@CurrentUser CustomUser customUser) { |
|
|
|
// .. find messages for this user and return them ... |
|
} |
|
---- |
|
|
|
Kotlin:: |
|
+ |
|
[source,kotlin,role="secondary"] |
|
---- |
|
@RequestMapping("/messages/inbox") |
|
open fun findMessagesForUser(@CurrentUser customUser: CustomUser?): ModelAndView { |
|
|
|
// .. find messages for this user and return them ... |
|
} |
|
---- |
|
====== |
|
|
|
Once it is a meta-annotation, parameterization is also available to you. |
|
|
|
For example, consider when you have a JWT as your principal and you want to say which claim to retrieve. |
|
As a meta-annotation, you might do: |
|
|
|
[tabs] |
|
====== |
|
Java:: |
|
+ |
|
[source,java,role="primary"] |
|
---- |
|
@Target({ElementType.PARAMETER, ElementType.TYPE}) |
|
@Retention(RetentionPolicy.RUNTIME) |
|
@Documented |
|
@AuthenticationPrincipal(expression = "claims['sub']") |
|
public @interface CurrentUser {} |
|
---- |
|
|
|
Kotlin:: |
|
+ |
|
[source,kotlin,role="secondary"] |
|
---- |
|
@Target(AnnotationTarget.VALUE_PARAMETER, AnnotationTarget.TYPE) |
|
@Retention(AnnotationRetention.RUNTIME) |
|
@MustBeDocumented |
|
@AuthenticationPrincipal(expression = "claims['sub']") |
|
annotation class CurrentUser |
|
---- |
|
====== |
|
|
|
which is already quite powerful. |
|
But, it is also limited to retrieving the `sub` claim. |
|
|
|
To make this more flexible, first publish the `AnnotationTemplateExpressionDefaults` bean like so: |
|
|
|
[tabs] |
|
====== |
|
Java:: |
|
+ |
|
[source,java,role="primary"] |
|
---- |
|
@Bean |
|
public AnnotationTemplateExpressionDefaults templateDefaults() { |
|
return new AnnotationTemplateExpressionDefaults(); |
|
} |
|
---- |
|
|
|
Kotlin:: |
|
+ |
|
[source,kotlin,role="secondary"] |
|
---- |
|
@Bean |
|
fun templateDefaults(): AnnotationTemplateExpressionDefaults { |
|
return AnnotationTemplateExpressionDefaults() |
|
} |
|
---- |
|
|
|
Xml:: |
|
+ |
|
[source,xml,role="secondary"] |
|
---- |
|
<b:bean name="annotationExpressionTemplateDefaults" class="org.springframework.security.core.annotation.AnnotationTemplateExpressionDefaults"/> |
|
---- |
|
====== |
|
|
|
and then you can supply a parameter to `@CurrentUser` like so: |
|
|
|
[tabs] |
|
====== |
|
Java:: |
|
+ |
|
[source,java,role="primary"] |
|
---- |
|
@Target({ElementType.PARAMETER, ElementType.TYPE}) |
|
@Retention(RetentionPolicy.RUNTIME) |
|
@Documented |
|
@AuthenticationPrincipal(expression = "claims['{claim}']") |
|
public @interface CurrentUser { |
|
String claim() default 'sub'; |
|
} |
|
---- |
|
|
|
Kotlin:: |
|
+ |
|
[source,kotlin,role="secondary"] |
|
---- |
|
@Target(AnnotationTarget.VALUE_PARAMETER, AnnotationTarget.TYPE) |
|
@Retention(AnnotationRetention.RUNTIME) |
|
@MustBeDocumented |
|
@AuthenticationPrincipal(expression = "claims['{claim}']") |
|
annotation class CurrentUser(val claim: String = "sub") |
|
---- |
|
====== |
|
|
|
This will allow you more flexibility across your set of applications in the following way: |
|
|
|
[tabs] |
|
====== |
|
Java:: |
|
+ |
|
[source,java,role="primary"] |
|
---- |
|
@RequestMapping("/messages/inbox") |
|
public ModelAndView findMessagesForUser(@CurrentUser("user_id") String userId) { |
|
|
|
// .. find messages for this user and return them ... |
|
} |
|
---- |
|
|
|
Kotlin:: |
|
+ |
|
[source,kotlin,role="secondary"] |
|
---- |
|
@RequestMapping("/messages/inbox") |
|
open fun findMessagesForUser(@CurrentUser("user_id") userId: String?): ModelAndView { |
|
|
|
// .. find messages for this user and return them ... |
|
} |
|
---- |
|
====== |
|
|
|
[[mvc-async]] |
|
== Spring MVC Async Integration |
|
|
|
Spring Web MVC 3.2+ has excellent support for https://docs.spring.io/spring/docs/3.2.x/spring-framework-reference/html/mvc.html#mvc-ann-async[Asynchronous Request Processing]. |
|
With no additional configuration, Spring Security automatically sets up the `SecurityContext` to the `Thread` that invokes a `Callable` returned by your controllers. |
|
For example, the following method automatically has its `Callable` invoked with the `SecurityContext` that was available when the `Callable` was created: |
|
|
|
[tabs] |
|
====== |
|
Java:: |
|
+ |
|
[source,java,role="primary"] |
|
---- |
|
@RequestMapping(method=RequestMethod.POST) |
|
public Callable<String> processUpload(final MultipartFile file) { |
|
|
|
return new Callable<String>() { |
|
public Object call() throws Exception { |
|
// ... |
|
return "someView"; |
|
} |
|
}; |
|
} |
|
---- |
|
|
|
Kotlin:: |
|
+ |
|
[source,kotlin,role="secondary"] |
|
---- |
|
@RequestMapping(method = [RequestMethod.POST]) |
|
open fun processUpload(file: MultipartFile?): Callable<String> { |
|
return Callable { |
|
// ... |
|
"someView" |
|
} |
|
} |
|
---- |
|
====== |
|
|
|
|
|
.Associating SecurityContext to Callable's |
|
[NOTE] |
|
==== |
|
More technically speaking, Spring Security integrates with `WebAsyncManager`. |
|
The `SecurityContext` that is used to process the `Callable` is the `SecurityContext` that exists on the `SecurityContextHolder` when `startCallableProcessing` is invoked. |
|
==== |
|
|
|
There is no automatic integration with a `DeferredResult` that is returned by controllers. |
|
This is because `DeferredResult` is processed by the users and, thus, there is no way of automatically integrating with it. |
|
However, you can still use xref:features/integrations/concurrency.adoc#concurrency[Concurrency Support] to provide transparent integration with Spring Security. |
|
|
|
[[mvc-csrf]] |
|
== Spring MVC and CSRF Integration |
|
|
|
Spring Security integrates with Spring MVC to add CSRF protection. |
|
|
|
=== Automatic Token Inclusion |
|
|
|
Spring Security automatically xref:servlet/exploits/csrf.adoc#csrf-integration-form[include the CSRF Token] within forms that use the https://docs.spring.io/spring/docs/3.2.x/spring-framework-reference/html/view.html#view-jsp-formtaglib-formtag[Spring MVC form tag]. |
|
Consider the following JSP: |
|
|
|
[source,xml] |
|
---- |
|
<jsp:root xmlns:jsp="http://java.sun.com/JSP/Page" |
|
xmlns:c="http://java.sun.com/jsp/jstl/core" |
|
xmlns:form="http://www.springframework.org/tags/form" version="2.0"> |
|
<jsp:directive.page language="java" contentType="text/html" /> |
|
<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en"> |
|
<!-- ... --> |
|
|
|
<c:url var="logoutUrl" value="/logout"/> |
|
<form:form action="${logoutUrl}" |
|
method="post"> |
|
<input type="submit" |
|
value="Log out" /> |
|
<input type="hidden" |
|
name="${_csrf.parameterName}" |
|
value="${_csrf.token}"/> |
|
</form:form> |
|
|
|
<!-- ... --> |
|
</html> |
|
</jsp:root> |
|
---- |
|
|
|
The preceding example output HTMLs that is similar to the following: |
|
|
|
[source,xml] |
|
---- |
|
<!-- ... --> |
|
|
|
<form action="/context/logout" method="post"> |
|
<input type="submit" value="Log out"/> |
|
<input type="hidden" name="_csrf" value="f81d4fae-7dec-11d0-a765-00a0c91e6bf6"/> |
|
</form> |
|
|
|
<!-- ... --> |
|
---- |
|
|
|
[[mvc-csrf-resolver]] |
|
=== Resolving the CsrfToken |
|
|
|
Spring Security provides `CsrfTokenArgumentResolver`, which can automatically resolve the current `CsrfToken` for Spring MVC arguments. |
|
By using xref:servlet/configuration/java.adoc#jc-hello-wsca[@EnableWebSecurity], you automatically have this added to your Spring MVC configuration. |
|
If you use XML-based configuration, you must add this yourself. |
|
|
|
Once `CsrfTokenArgumentResolver` is properly configured, you can expose the `CsrfToken` to your static HTML based application: |
|
|
|
[tabs] |
|
====== |
|
Java:: |
|
+ |
|
[source,java,role="primary"] |
|
---- |
|
@RestController |
|
public class CsrfController { |
|
|
|
@RequestMapping("/csrf") |
|
public CsrfToken csrf(CsrfToken token) { |
|
return token; |
|
} |
|
} |
|
---- |
|
|
|
Kotlin:: |
|
+ |
|
[source,kotlin,role="secondary"] |
|
---- |
|
@RestController |
|
class CsrfController { |
|
@RequestMapping("/csrf") |
|
fun csrf(token: CsrfToken): CsrfToken { |
|
return token |
|
} |
|
} |
|
---- |
|
====== |
|
|
|
It is important to keep the `CsrfToken` a secret from other domains. |
|
This means that, if you use 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. |
|
|
|
[[security-mvc-same-application-context]] |
|
== Configuring Spring MVC and Spring Security in the Same Application Context |
|
|
|
If you are using Boot, Spring MVC and Spring Security are in the same application context by default. |
|
|
|
Otherwise, for Java Config, including both `@EnableWebMvc` and `@EnableWebSecurity` will construct Spring Security and Spring MVC components in the same context. |
|
|
|
Of, if you are using ``ServletListener``s you can do: |
|
|
|
[tabs] |
|
====== |
|
Java:: |
|
+ |
|
[source,java,role="primary"] |
|
---- |
|
public class SecurityInitializer extends |
|
AbstractAnnotationConfigDispatcherServletInitializer { |
|
|
|
@Override |
|
protected Class<?>[] getRootConfigClasses() { |
|
return null; |
|
} |
|
|
|
@Override |
|
protected Class<?>[] getServletConfigClasses() { |
|
return new Class[] { RootConfiguration.class, |
|
WebMvcConfiguration.class }; |
|
} |
|
|
|
@Override |
|
protected String[] getServletMappings() { |
|
return new String[] { "/" }; |
|
} |
|
} |
|
---- |
|
|
|
Kotlin:: |
|
+ |
|
[source,kotlin,role="secondary"] |
|
---- |
|
class SecurityInitializer : AbstractAnnotationConfigDispatcherServletInitializer() { |
|
override fun getRootConfigClasses(): Array<Class<*>>? { |
|
return null |
|
} |
|
|
|
override fun getServletConfigClasses(): Array<Class<*>> { |
|
return arrayOf( |
|
RootConfiguration::class.java, |
|
WebMvcConfiguration::class.java |
|
) |
|
} |
|
|
|
override fun getServletMappings(): Array<String> { |
|
return arrayOf("/") |
|
} |
|
} |
|
---- |
|
====== |
|
|
|
And finally for a `web.xml` file, you configure the `DispatcherServlet` like so: |
|
|
|
[source,xml] |
|
---- |
|
<listener> |
|
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> |
|
</listener> |
|
|
|
<!-- All Spring Configuration (both MVC and Security) are in /WEB-INF/spring/ --> |
|
<context-param> |
|
<param-name>contextConfigLocation</param-name> |
|
<param-value>/WEB-INF/spring/*.xml</param-value> |
|
</context-param> |
|
|
|
<servlet> |
|
<servlet-name>spring</servlet-name> |
|
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> |
|
<!-- Load from the ContextLoaderListener --> |
|
<init-param> |
|
<param-name>contextConfigLocation</param-name> |
|
<param-value></param-value> |
|
</init-param> |
|
</servlet> |
|
|
|
<servlet-mapping> |
|
<servlet-name>spring</servlet-name> |
|
<url-pattern>/</url-pattern> |
|
</servlet-mapping> |
|
---- |
|
|
|
The following `WebSecurityConfiguration` in placed in the `ApplicationContext` of the `DispatcherServlet`. |
|
|
|
|