581 lines
17 KiB
Plaintext
581 lines
17 KiB
Plaintext
[[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]]
|
|
== @EnableWebMvcSecurity
|
|
|
|
NOTE: 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.
|
|
|
|
NOTE: Spring Security provides the configuration 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 are using more advanced options, like integrating with `WebMvcConfigurationSupport` directly, then you will need to manually provide the Spring Security configuration.
|
|
|
|
[[mvc-requestmatcher]]
|
|
== MvcRequestMatcher
|
|
|
|
Spring Security provides deep integration with how Spring MVC matches on URLs with `MvcRequestMatcher`.
|
|
This is helpful to ensure your Security rules match the logic used to handle your requests.
|
|
|
|
In order to use `MvcRequestMatcher` you must place the Spring Security Configuration in the same `ApplicationContext` as your `DispatcherServlet`.
|
|
This is necessary because Spring Security's `MvcRequestMatcher` expects a `HandlerMappingIntrospector` bean with the name of `mvcHandlerMappingIntrospector` to be registered by your Spring MVC configuration that is used to perform the matching.
|
|
|
|
For a `web.xml` this means that you should place your configuration in the `DispatcherServlet.xml`.
|
|
|
|
[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>
|
|
----
|
|
|
|
Below `WebSecurityConfiguration` in placed in the ``DispatcherServlet``s `ApplicationContext`.
|
|
|
|
====
|
|
.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("/")
|
|
}
|
|
}
|
|
----
|
|
====
|
|
|
|
[NOTE]
|
|
====
|
|
It is always recommended to 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, that your application is still secured.
|
|
This is what is known as https://en.wikipedia.org/wiki/Defense_in_depth_(computing)[Defence in Depth]
|
|
====
|
|
|
|
Consider a controller that is mapped as follows:
|
|
|
|
====
|
|
.Java
|
|
[source,java,role="primary"]
|
|
----
|
|
@RequestMapping("/admin")
|
|
public String admin() {
|
|
----
|
|
|
|
.Kotlin
|
|
[source,kotlin,role="secondary"]
|
|
----
|
|
@RequestMapping("/admin")
|
|
fun admin(): String {
|
|
----
|
|
====
|
|
|
|
If we wanted to restrict access to this controller method to admin users, a developer can provide authorization rules by matching on the `HttpServletRequest` with the following:
|
|
|
|
====
|
|
.Java
|
|
[source,java,role="primary"]
|
|
----
|
|
protected configure(HttpSecurity http) throws Exception {
|
|
http
|
|
.authorizeHttpRequests(authorize -> authorize
|
|
.antMatchers("/admin").hasRole("ADMIN")
|
|
);
|
|
}
|
|
----
|
|
|
|
.Kotlin
|
|
[source,kotlin,role="secondary"]
|
|
----
|
|
override fun configure(http: HttpSecurity) {
|
|
http {
|
|
authorizeRequests {
|
|
authorize(AntPathRequestMatcher("/admin"), hasRole("ADMIN"))
|
|
}
|
|
}
|
|
}
|
|
----
|
|
====
|
|
|
|
or in XML
|
|
|
|
[source,xml]
|
|
----
|
|
<http>
|
|
<intercept-url pattern="/admin" access="hasRole('ADMIN')"/>
|
|
</http>
|
|
----
|
|
|
|
With either configuration, the URL `/admin` will require the authenticated user to be an admin user.
|
|
However, depending on our Spring MVC configuration, the URL `/admin.html` will also map to our `admin()` method.
|
|
Additionally, depending on our Spring MVC configuration, the URL `/admin/` will also map to our `admin()` method.
|
|
|
|
The problem is that our security rule is only protecting `/admin`.
|
|
We could add additional rules for all the permutations of Spring MVC, but this would be quite verbose and tedious.
|
|
|
|
Instead, we can leverage Spring Security's `MvcRequestMatcher`.
|
|
The following configuration will protect the same URLs that Spring MVC will match on by using Spring MVC to match on the URL.
|
|
|
|
|
|
====
|
|
.Java
|
|
[source,java,role="primary"]
|
|
----
|
|
protected configure(HttpSecurity http) throws Exception {
|
|
http
|
|
.authorizeHttpRequests(authorize -> authorize
|
|
.mvcMatchers("/admin").hasRole("ADMIN")
|
|
);
|
|
}
|
|
----
|
|
|
|
.Kotlin
|
|
[source,kotlin,role="secondary"]
|
|
----
|
|
override fun configure(http: HttpSecurity) {
|
|
http {
|
|
authorizeRequests {
|
|
authorize("/admin", hasRole("ADMIN"))
|
|
}
|
|
}
|
|
}
|
|
----
|
|
====
|
|
|
|
or in XML
|
|
|
|
[source,xml]
|
|
----
|
|
<http request-matcher="mvc">
|
|
<intercept-url pattern="/admin" access="hasRole('ADMIN')"/>
|
|
</http>
|
|
----
|
|
|
|
[[mvc-authentication-principal]]
|
|
== @AuthenticationPrincipal
|
|
|
|
Spring Security provides `AuthenticationPrincipalArgumentResolver` which can automatically resolve the current `Authentication.getPrincipal()` for Spring MVC arguments.
|
|
By using `@EnableWebSecurity` you will automatically have this added to your Spring MVC configuration.
|
|
If you use XML based configuration, you must add this yourself.
|
|
For example:
|
|
|
|
[source,xml]
|
|
----
|
|
<mvc:annotation-driven>
|
|
<mvc:argument-resolvers>
|
|
<bean class="org.springframework.security.web.method.annotation.AuthenticationPrincipalArgumentResolver" />
|
|
</mvc:argument-resolvers>
|
|
</mvc:annotation-driven>
|
|
----
|
|
|
|
Once `AuthenticationPrincipalArgumentResolver` is properly configured, you can be entirely decoupled from Spring Security in your Spring MVC layer.
|
|
|
|
Consider a situation where a custom `UserDetailsService` that returns an `Object` that implements `UserDetails` and your own `CustomUser` `Object`. The `CustomUser` of the currently authenticated user could be accessed using the following code:
|
|
|
|
====
|
|
.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. For example:
|
|
|
|
====
|
|
.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 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:
|
|
|
|
====
|
|
.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` 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:
|
|
|
|
====
|
|
.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, the following could be used if we were using JPA to manage our Users and we wanted to modify and save a property on the current user.
|
|
|
|
====
|
|
.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.
|
|
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.
|
|
|
|
====
|
|
.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
|
|
----
|
|
====
|
|
|
|
Now that `@CurrentUser` has been specified, we can use it to signal to resolve our `CustomUser` of the currently authenticated user.
|
|
We have also isolated our dependency on Spring Security to a single file.
|
|
|
|
====
|
|
.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 ...
|
|
}
|
|
----
|
|
====
|
|
|
|
|
|
[[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 will automatically setup the `SecurityContext` to the `Thread` that invokes a `Callable` returned by your controllers.
|
|
For example, the following method will automatically have its `Callable` invoked with the `SecurityContext` that was available when the `Callable` was created:
|
|
|
|
====
|
|
.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"
|
|
}
|
|
}
|
|
----
|
|
====
|
|
|
|
[NOTE]
|
|
.Associating SecurityContext to Callable's
|
|
====
|
|
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` at the time `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
|
|
|
|
=== Automatic Token Inclusion
|
|
|
|
Spring Security will automatically xref:servlet/exploits/csrf.adoc#servlet-csrf-include[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].
|
|
For example, 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>
|
|
----
|
|
|
|
Will output HTML 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 will 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.
|
|
|
|
====
|
|
.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 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.
|