1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156 |
- [[servlet-authorization-authorizationfilter]]
- = Authorize HttpServletRequests
- :figures: servlet/authorization
- Spring Security allows you to xref:servlet/authorization/index.adoc[model your authorization] at the request level.
- For example, with Spring Security you can say that all pages under `/admin` require one authority while all other pages simply require authentication.
- By default, Spring Security requires that every request be authenticated.
- That said, any time you use xref:servlet/configuration/java.adoc#jc-httpsecurity[an `HttpSecurity` instance], it's necessary to declare your authorization rules.
- [[activate-request-security]]
- Whenever you have an `HttpSecurity` instance, you should at least do:
- .Use authorizeHttpRequests
- [tabs]
- ======
- Java::
- +
- [source,java,role="primary"]
- ----
- http
- .authorizeHttpRequests((authorize) -> authorize
- .anyRequest().authenticated()
- )
- ----
- Kotlin::
- +
- [source,kotlin,role="secondary"]
- ----
- http {
- authorizeHttpRequests {
- authorize(anyRequest, authenticated)
- }
- }
- ----
- Xml::
- +
- [source,xml,role="secondary"]
- ----
- <http>
- <intercept-url pattern="/**" access="authenticated"/>
- </http>
- ----
- ======
- This tells Spring Security that any endpoint in your application requires that the security context at a minimum be authenticated in order to allow it.
- In many cases, your authorization rules will be more sophisticated than that, so please consider the following use cases:
- * I have an app that uses `authorizeRequests` and I want to <<migrate-authorize-requests,migrate it to `authorizeHttpRequests`>>
- * I want to <<request-authorization-architecture,understand how the `AuthorizationFilter` components work>>
- * I want to <<match-requests, match requests>> based on a pattern; specifically <<match-by-regex,regex>>
- * I want to match request, and I map Spring MVC to <<mvc-not-default-servlet, something other than the default servlet>>
- * I want to <<authorize-requests, authorize requests>>
- * I want to <<match-by-custom, match a request programmatically>>
- * I want to <<authorize-requests, authorize a request programmatically>>
- * I want to <<remote-authorization-manager, delegate request authorization>> to a policy agent
- [[request-authorization-architecture]]
- == Understanding How Request Authorization Components Work
- [NOTE]
- This section builds on xref:servlet/architecture.adoc#servlet-architecture[Servlet Architecture and Implementation] by digging deeper into how xref:servlet/authorization/index.adoc#servlet-authorization[authorization] works at the request level in Servlet-based applications.
- .Authorize HttpServletRequest
- image::{figures}/authorizationfilter.png[]
- * image:{icondir}/number_1.png[] First, the `AuthorizationFilter` constructs a `Supplier` that retrieves an xref:servlet/authentication/architecture.adoc#servlet-authentication-authentication[Authentication] from the xref:servlet/authentication/architecture.adoc#servlet-authentication-securitycontextholder[SecurityContextHolder].
- * image:{icondir}/number_2.png[] Second, it passes the `Supplier<Authentication>` and the `HttpServletRequest` to the xref:servlet/architecture.adoc#authz-authorization-manager[`AuthorizationManager`].
- The `AuthorizationManager` matches the request to the patterns in `authorizeHttpRequests`, and runs the corresponding rule.
- ** image:{icondir}/number_3.png[] If authorization is denied, xref:servlet/authorization/events.adoc[an `AuthorizationDeniedEvent` is published], and an `AccessDeniedException` is thrown.
- In this case the xref:servlet/architecture.adoc#servlet-exceptiontranslationfilter[`ExceptionTranslationFilter`] handles the `AccessDeniedException`.
- ** image:{icondir}/number_4.png[] If access is granted, xref:servlet/authorization/events.adoc[an `AuthorizationGrantedEvent` is published] and `AuthorizationFilter` continues with the xref:servlet/architecture.adoc#servlet-filters-review[FilterChain] which allows the application to process normally.
- === `AuthorizationFilter` Is Last By Default
- The `AuthorizationFilter` is last in xref:servlet/architecture.adoc#servlet-filterchain-figure[the Spring Security filter chain] by default.
- This means that Spring Security's xref:servlet/authentication/index.adoc[authentication filters], xref:servlet/exploits/index.adoc[exploit protections], and other filter integrations do not require authorization.
- If you add filters of your own before the `AuthorizationFilter`, they will also not require authorization; otherwise, they will.
- A place where this typically becomes important is when you are adding {spring-framework-reference-url}web.html#spring-web[Spring MVC] endpoints.
- Because they are executed by the {spring-framework-reference-url}web.html#mvc-servlet[`DispatcherServlet`] and this comes after the `AuthorizationFilter`, your endpoints need to be <<authorizing-endpoints,included in `authorizeHttpRequests` to be permitted>>.
- === All Dispatches Are Authorized
- The `AuthorizationFilter` runs not just on every request, but on every dispatch.
- This means that the `REQUEST` dispatch needs authorization, but also ``FORWARD``s, ``ERROR``s, and ``INCLUDE``s.
- For example, {spring-framework-reference-url}web.html#spring-web[Spring MVC] can `FORWARD` the request to a view resolver that renders a Thymeleaf template, like so:
- .Sample Forwarding Spring MVC Controller
- [tabs]
- ======
- Java::
- +
- [source,java,role="primary"]
- ----
- @Controller
- public class MyController {
- @GetMapping("/endpoint")
- public String endpoint() {
- return "endpoint";
- }
- }
- ----
- Kotlin::
- +
- [source,kotlin,role="secondary"]
- ----
- @Controller
- class MyController {
- @GetMapping("/endpoint")
- fun endpoint(): String {
- return "endpoint"
- }
- }
- ----
- ======
- In this case, authorization happens twice; once for authorizing `/endpoint` and once for forwarding to Thymeleaf to render the "endpoint" template.
- For that reason, you may want to <<match-by-dispatcher-type, permit all `FORWARD` dispatches>>.
- Another example of this principle is {spring-boot-reference-url}web.html#web.servlet.spring-mvc.error-handling[how Spring Boot handles errors].
- If the container catches an exception, say like the following:
- .Sample Erroring Spring MVC Controller
- [tabs]
- ======
- Java::
- +
- [source,java,role="primary"]
- ----
- @Controller
- public class MyController {
- @GetMapping("/endpoint")
- public String endpoint() {
- throw new UnsupportedOperationException("unsupported");
- }
- }
- ----
- Kotlin::
- +
- [source,kotlin,role="secondary"]
- ----
- @Controller
- class MyController {
- @GetMapping("/endpoint")
- fun endpoint(): String {
- throw UnsupportedOperationException("unsupported")
- }
- }
- ----
- ======
- then Boot will dispatch it to the `ERROR` dispatch.
- In that case, authorization also happens twice; once for authorizing `/endpoint` and once for dispatching the error.
- For that reason, you may want to <<match-by-dispatcher-type, permit all `ERROR` dispatches>>.
- === `Authentication` Lookup is Deferred
- Remember that xref:servlet/authorization/architecture.adoc#_the_authorizationmanager[the `AuthorizationManager` API uses a `Supplier<Authentication>`].
- This matters with `authorizeHttpRequests` when requests are <<authorize-requests,always permitted or always denied>>.
- In those cases, xref:servlet/authentication/architecture.adoc#servlet-authentication-authentication[the `Authentication`] is not queried, making for a faster request.
- [[authorizing-endpoints]]
- == Authorizing an Endpoint
- You can configure Spring Security to have different rules by adding more rules in order of precedence.
- If you want to require that `/endpoint` only be accessible by end users with the `USER` authority, then you can do:
- .Authorize an Endpoint
- [tabs]
- ======
- Java::
- +
- [source,java,role="primary"]
- ----
- @Bean
- SecurityFilterChain web(HttpSecurity http) throws Exception {
- http
- .authorizeHttpRequests((authorize) -> authorize
- .requestMatchers("/endpoint").hasAuthority("USER")
- .anyRequest().authenticated()
- )
- // ...
- return http.build();
- }
- ----
- Kotlin::
- +
- [source,kotlin,role="secondary"]
- ----
- @Bean
- SecurityFilterChain web(HttpSecurity http) throws Exception {
- http {
- authorizeHttpRequests {
- authorize("/endpoint", hasAuthority("USER"))
- authorize(anyRequest, authenticated)
- }
- }
- return http.build();
- }
- ----
- Xml::
- +
- [source,xml,role="secondary"]
- ----
- <http>
- <intercept-url pattern="/endpoint" access="hasAuthority('USER')"/>
- <intercept-url pattern="/**" access="authenticated"/>
- </http>
- ----
- ======
- As you can see, the declaration can be broken up in to pattern/rule pairs.
- `AuthorizationFilter` processes these pairs in the order listed, applying only the first match to the request.
- This means that even though `/**` would also match for `/endpoint` the above rules are not a problem.
- The way to read the above rules is "if the request is `/endpoint`, then require the `USER` authority; else, only require authentication".
- Spring Security supports several patterns and several rules; you can also programmatically create your own of each.
- Once authorized, you can test it using xref:servlet/test/method.adoc#test-method-withmockuser[Security's test support] in the following way:
- .Test Endpoint Authorization
- [tabs]
- ======
- Java::
- +
- [source,java,role="primary"]
- ----
- @WithMockUser(authorities="USER")
- @Test
- void endpointWhenUserAuthorityThenAuthorized() {
- this.mvc.perform(get("/endpoint"))
- .andExpect(status().isOk());
- }
- @WithMockUser
- @Test
- void endpointWhenNotUserAuthorityThenForbidden() {
- this.mvc.perform(get("/endpoint"))
- .andExpect(status().isForbidden());
- }
- @Test
- void anyWhenUnauthenticatedThenUnauthorized() {
- this.mvc.perform(get("/any"))
- .andExpect(status().isUnauthorized())
- }
- ----
- ======
- [[match-requests]]
- == Matching Requests
- Above you've already seen <<authorizing-endpoints, two ways to match requests>>.
- The first you saw was the simplest, which is to match any request.
- The second is to match by a URI pattern.
- Spring Security supports two languages for URI pattern-matching: <<match-by-ant,Ant>> (as seen above) and <<match-by-regex,Regular Expressions>>.
- [[match-by-ant]]
- === Matching Using Ant
- Ant is the default language that Spring Security uses to match requests.
- You can use it to match a single endpoint or a directory, and you can even capture placeholders for later use.
- You can also refine it to match a specific set of HTTP methods.
- Let's say that you instead of wanting to match the `/endpoint` endpoint, you want to match all endpoints under the `/resource` directory.
- In that case, you can do something like the following:
- .Match with Ant
- [tabs]
- ======
- Java::
- +
- [source,java,role="primary"]
- ----
- http
- .authorizeHttpRequests((authorize) -> authorize
- .requestMatchers("/resource/**").hasAuthority("USER")
- .anyRequest().authenticated()
- )
- ----
- Kotlin::
- +
- [source,kotlin,role="secondary"]
- ----
- http {
- authorizeHttpRequests {
- authorize("/resource/**", hasAuthority("USER"))
- authorize(anyRequest, authenticated)
- }
- }
- ----
- Xml::
- +
- [source,xml,role="secondary"]
- ----
- <http>
- <intercept-url pattern="/resource/**" access="hasAuthority('USER')"/>
- <intercept-url pattern="/**" access="authenticated"/>
- </http>
- ----
- ======
- The way to read this is "if the request is `/resource` or some subdirectory, require the `USER` authority; otherwise, only require authentication"
- You can also extract path values from the request, as seen below:
- .Authorize and Extract
- [tabs]
- ======
- Java::
- +
- [source,java,role="primary"]
- ----
- http
- .authorizeHttpRequests((authorize) -> authorize
- .requestMatchers("/resource/{name}").access(new WebExpressionAuthorizationManager("#name == authentication.name"))
- .anyRequest().authenticated()
- )
- ----
- Kotlin::
- +
- [source,kotlin,role="secondary"]
- ----
- http {
- authorizeHttpRequests {
- authorize("/resource/{name}", WebExpressionAuthorizationManager("#name == authentication.name"))
- authorize(anyRequest, authenticated)
- }
- }
- ----
- Xml::
- +
- [source,xml,role="secondary"]
- ----
- <http>
- <intercept-url pattern="/resource/{name}" access="#name == authentication.name"/>
- <intercept-url pattern="/**" access="authenticated"/>
- </http>
- ----
- ======
- Once authorized, you can test it using xref:servlet/test/method.adoc#test-method-withmockuser[Security's test support] in the following way:
- .Test Directory Authorization
- [tabs]
- ======
- Java::
- +
- [source,java,role="primary"]
- ----
- @WithMockUser(authorities="USER")
- @Test
- void endpointWhenUserAuthorityThenAuthorized() {
- this.mvc.perform(get("/endpoint/jon"))
- .andExpect(status().isOk());
- }
- @WithMockUser
- @Test
- void endpointWhenNotUserAuthorityThenForbidden() {
- this.mvc.perform(get("/endpoint/jon"))
- .andExpect(status().isForbidden());
- }
- @Test
- void anyWhenUnauthenticatedThenUnauthorized() {
- this.mvc.perform(get("/any"))
- .andExpect(status().isUnauthorized())
- }
- ----
- ======
- [NOTE]
- Spring Security only matches paths.
- If you want to match query parameters, you will need a custom request matcher.
- [[match-by-regex]]
- === Matching Using Regular Expressions
- Spring Security supports matching requests against a regular expression.
- This can come in handy if you want to apply more strict matching criteria than `**` on a subdirectory.
- For example, consider a path that contains the username and the rule that all usernames must be alphanumeric.
- You can use {security-api-url}org/springframework/security/web/util/matcher/RegexRequestMatcher.html[`RegexRequestMatcher`] to respect this rule, like so:
- .Match with Regex
- [tabs]
- ======
- Java::
- +
- [source,java,role="primary"]
- ----
- http
- .authorizeHttpRequests((authorize) -> authorize
- .requestMatchers(RegexRequestMatcher.regexMatcher("/resource/[A-Za-z0-9]+")).hasAuthority("USER")
- .anyRequest().denyAll()
- )
- ----
- Kotlin::
- +
- [source,kotlin,role="secondary"]
- ----
- http {
- authorizeHttpRequests {
- authorize(RegexRequestMatcher.regexMatcher("/resource/[A-Za-z0-9]+"), hasAuthority("USER"))
- authorize(anyRequest, denyAll)
- }
- }
- ----
- Xml::
- +
- [source,xml,role="secondary"]
- ----
- <http>
- <intercept-url request-matcher="regex" pattern="/resource/[A-Za-z0-9]+" access="hasAuthority('USER')"/>
- <intercept-url pattern="/**" access="denyAll"/>
- </http>
- ----
- ======
- [[match-by-httpmethod]]
- === Matching By Http Method
- You can also match rules by HTTP method.
- One place where this is handy is when authorizing by permissions granted, like being granted a `read` or `write` privilege.
- To require all ``GET``s to have the `read` permission and all ``POST``s to have the `write` permission, you can do something like this:
- .Match by HTTP Method
- [tabs]
- ======
- Java::
- +
- [source,java,role="primary"]
- ----
- http
- .authorizeHttpRequests((authorize) -> authorize
- .requestMatchers(HttpMethod.GET).hasAuthority("read")
- .requestMatchers(HttpMethod.POST).hasAuthority("write")
- .anyRequest().denyAll()
- )
- ----
- Kotlin::
- +
- [source,kotlin,role="secondary"]
- ----
- http {
- authorizeHttpRequests {
- authorize(HttpMethod.GET, hasAuthority("read"))
- authorize(HttpMethod.POST, hasAuthority("write"))
- authorize(anyRequest, denyAll)
- }
- }
- ----
- Xml::
- +
- [source,xml,role="secondary"]
- ----
- <http>
- <intercept-url http-method="GET" pattern="/**" access="hasAuthority('read')"/>
- <intercept-url http-method="POST" pattern="/**" access="hasAuthority('write')"/>
- <intercept-url pattern="/**" access="denyAll"/>
- </http>
- ----
- ======
- These authorization rules should read as: "if the request is a GET, then require `read` permission; else, if the request is a POST, then require `write` permission; else, deny the request"
- [TIP]
- Denying the request by default is a healthy security practice since it turns the set of rules into an allow list.
- Once authorized, you can test it using xref:servlet/test/method.adoc#test-method-withmockuser[Security's test support] in the following way:
- .Test Http Method Authorization
- [tabs]
- ======
- Java::
- +
- [source,java,role="primary"]
- ----
- @WithMockUser(authorities="read")
- @Test
- void getWhenReadAuthorityThenAuthorized() {
- this.mvc.perform(get("/any"))
- .andExpect(status().isOk());
- }
- @WithMockUser
- @Test
- void getWhenNoReadAuthorityThenForbidden() {
- this.mvc.perform(get("/any"))
- .andExpect(status().isForbidden());
- }
- @WithMockUser(authorities="write")
- @Test
- void postWhenWriteAuthorityThenAuthorized() {
- this.mvc.perform(post("/any").with(csrf()))
- .andExpect(status().isOk())
- }
- @WithMockUser(authorities="read")
- @Test
- void postWhenNoWriteAuthorityThenForbidden() {
- this.mvc.perform(get("/any").with(csrf()))
- .andExpect(status().isForbidden());
- }
- ----
- ======
- [[match-by-dispatcher-type]]
- === Matching By Dispatcher Type
- [NOTE]
- This feature is not currently supported in XML
- As stated earlier, Spring Security <<_all_dispatches_are_authorized, authorizes all dispatcher types by default>>.
- And even though xref:servlet/authentication/architecture.adoc#servlet-authentication-securitycontext[the security context] established on the `REQUEST` dispatch carries over to subsequent dispatches, subtle mismatches can sometimes cause an unexpected `AccessDeniedException`.
- To address that, you can configure Spring Security Java configuration to allow dispatcher types like `FORWARD` and `ERROR`, like so:
- .Match by Dispatcher Type
- ====
- .Java
- [source,java,role="secondary"]
- ----
- http
- .authorizeHttpRequests((authorize) -> authorize
- .dispatcherTypeMatchers(DispatcherType.FORWARD, DispatcherType.ERROR).permitAll()
- .requestMatchers("/endpoint").permitAll()
- .anyRequest().denyAll()
- )
- ----
- .Kotlin
- [source,kotlin,role="secondary"]
- ----
- http {
- authorizeHttpRequests {
- authorize(DispatcherTypeRequestMatcher(DispatcherType.FORWARD), permitAll)
- authorize(DispatcherTypeRequestMatcher(DispatcherType.ERROR), permitAll)
- authorize("/endpoint", permitAll)
- authorize(anyRequest, denyAll)
- }
- }
- ----
- ====
- [[match-by-mvc]]
- === Using an MvcRequestMatcher
- Generally speaking, you can use `requestMatchers(String)` as demonstrated above.
- However, if you map Spring MVC to a different servlet path, then you need to account for that in your security configuration.
- For example, if Spring MVC is mapped to `/spring-mvc` instead of `/` (the default), then you may have an endpoint like `/spring-mvc/my/controller` that you want to authorize.
- You need to use `MvcRequestMatcher` to split the servlet path and the controller path in your configuration like so:
- .Match by MvcRequestMatcher
- ====
- .Java
- [source,java,role="primary"]
- ----
- @Bean
- MvcRequestMatcher.Builder mvc(HandlerMappingIntrospector introspector) {
- return new MvcRequestMatcher.Builder(introspector).servletPath("/spring-mvc");
- }
- @Bean
- SecurityFilterChain appEndpoints(HttpSecurity http, MvcRequestMatcher.Builder mvc) {
- http
- .authorizeHttpRequests((authorize) -> authorize
- .requestMatchers(mvc.pattern("/my/controller/**")).hasAuthority("controller")
- .anyRequest().authenticated()
- );
- return http.build();
- }
- ----
- .Kotlin
- [source,kotlin,role="secondary"]
- ----
- @Bean
- fun mvc(introspector: HandlerMappingIntrospector): MvcRequestMatcher.Builder =
- MvcRequestMatcher.Builder(introspector).servletPath("/spring-mvc");
- @Bean
- fun appEndpoints(http: HttpSecurity, mvc: MvcRequestMatcher.Builder): SecurityFilterChain =
- http {
- authorizeHttpRequests {
- authorize(mvc.pattern("/my/controller/**"), hasAuthority("controller"))
- authorize(anyRequest, authenticated)
- }
- }
- ----
- .Xml
- [source,xml,role="secondary"]
- ----
- <http>
- <intercept-url servlet-path="/spring-mvc" pattern="/my/controller/**" access="hasAuthority('controller')"/>
- <intercept-url pattern="/**" access="authenticated"/>
- </http>
- ----
- ====
- This need can arise in at least two different ways:
- * If you use the `spring.mvc.servlet.path` Boot property to change the default path (`/`) to something else
- * If you register more than one Spring MVC `DispatcherServlet` (thus requiring that one of them not be the default path)
- [[match-by-custom]]
- === Using a Custom Matcher
- [NOTE]
- This feature is not currently supported in XML
- In Java configuration, you can create your own {security-api-url}org/springframework/security/web/util/matcher/RequestMatcher.html[`RequestMatcher`] and supply it to the DSL like so:
- .Authorize by Dispatcher Type
- ====
- .Java
- [source,java,role="secondary"]
- ----
- RequestMatcher printview = (request) -> request.getParameter("print") != null;
- http
- .authorizeHttpRequests((authorize) -> authorize
- .requestMatchers(printview).hasAuthority("print")
- .anyRequest().authenticated()
- )
- ----
- .Kotlin
- [source,kotlin,role="secondary"]
- ----
- val printview: RequestMatcher = { (request) -> request.getParameter("print") != null }
- http {
- authorizeHttpRequests {
- authorize(printview, hasAuthority("print"))
- authorize(anyRequest, authenticated)
- }
- }
- ----
- ====
- [TIP]
- Because {security-api-url}org/springframework/security/web/util/matcher/RequestMatcher.html[`RequestMatcher`] is a functional interface, you can supply it as a lambda in the DSL.
- However, if you want to extract values from the request, you will need to have a concrete class since that requires overriding a `default` method.
- Once authorized, you can test it using xref:servlet/test/method.adoc#test-method-withmockuser[Security's test support] in the following way:
- .Test Custom Authorization
- [tabs]
- ======
- Java::
- +
- [source,java,role="primary"]
- ----
- @WithMockUser(authorities="print")
- @Test
- void printWhenPrintAuthorityThenAuthorized() {
- this.mvc.perform(get("/any?print"))
- .andExpect(status().isOk());
- }
- @WithMockUser
- @Test
- void printWhenNoPrintAuthorityThenForbidden() {
- this.mvc.perform(get("/any?print"))
- .andExpect(status().isForbidden());
- }
- ----
- ======
- [[authorize-requests]]
- == Authorizing Requests
- Once a request is matched, you can authorize it in several ways <<match-requests, already seen>> like `permitAll`, `denyAll`, and `hasAuthority`.
- As a quick summary, here are the authorization rules built into the DSL:
- * `permitAll` - The request requires no authorization and is a public endpoint; note that in this case, xref:servlet/authentication/architecture.adoc#servlet-authentication-authentication[the `Authentication`] is never retrieved from the session
- * `denyAll` - The request is not allowed under any circumstances; note that in this case, the `Authentication` is never retrieved from the session
- * `hasAuthority` - The request requires that the `Authentication` have xref:servlet/authorization/architecture.adoc#authz-authorities[a `GrantedAuthority`] that matches the given value
- * `hasRole` - A shortcut for `hasAuthority` that prefixes `ROLE_` or whatever is configured as the default prefix
- * `hasAnyAuthority` - The request requires that the `Authentication` have a `GrantedAuthority` that matches any of the given values
- * `hasAnyRole` - A shortcut for `hasAnyAuthority` that prefixes `ROLE_` or whatever is configured as the default prefix
- * `access` - The request uses this custom `AuthorizationManager` to determine access
- Having now learned the patterns, rules, and how they can be paired together, you should be able to understand what is going on in this more complex example:
- .Authorize Requests
- [tabs]
- ======
- Java::
- +
- [source,java,role="primary"]
- ----
- import static jakarta.servlet.DispatcherType.*;
- import static org.springframework.security.authorization.AuthorizationManagers.allOf;
- import static org.springframework.security.authorization.AuthorityAuthorizationManager.hasAuthority;
- import static org.springframework.security.authorization.AuthorityAuthorizationManager.hasRole;
- @Bean
- SecurityFilterChain web(HttpSecurity http) throws Exception {
- http
- // ...
- .authorizeHttpRequests(authorize -> authorize // <1>
- .dispatcherTypeMatchers(FORWARD, ERROR).permitAll() // <2>
- .requestMatchers("/static/**", "/signup", "/about").permitAll() // <3>
- .requestMatchers("/admin/**").hasRole("ADMIN") // <4>
- .requestMatchers("/db/**").access(allOf(hasAuthority('db'), hasRole('ADMIN'))) // <5>
- .anyRequest().denyAll() // <6>
- );
- return http.build();
- }
- ----
- ======
- <1> There are multiple authorization rules specified.
- Each rule is considered in the order they were declared.
- <2> Dispatches `FORWARD` and `ERROR` are permitted to allow {spring-framework-reference-url}web.html#spring-web[Spring MVC] to render views and Spring Boot to render errors
- <3> We specified multiple URL patterns that any user can access.
- Specifically, any user can access a request if the URL starts with "/static/", equals "/signup", or equals "/about".
- <4> Any URL that starts with "/admin/" will be restricted to users who have the role "ROLE_ADMIN".
- You will notice that since we are invoking the `hasRole` method we do not need to specify the "ROLE_" prefix.
- <5> Any URL that starts with "/db/" requires the user to have both been granted the "db" permission as well as be a "ROLE_ADMIN".
- You will notice that since we are using the `hasRole` expression we do not need to specify the "ROLE_" prefix.
- <6> Any URL that has not already been matched on is denied access.
- This is a good strategy if you do not want to accidentally forget to update your authorization rules.
- [[authorization-expressions]]
- == Expressing Authorization with SpEL
- While using a concrete `AuthorizationManager` is recommended, there are some cases where an expression is necessary, like with `<intercept-url>` or with JSP Taglibs.
- For that reason, this section will focus on examples from those domains.
- Given that, let's cover Spring Security's Web Security Authorization SpEL API a bit more in depth.
- Spring Security encapsulates all of its authorization fields and methods in a set of root objects.
- The most generic root object is called `SecurityExpressionRoot` and it forms the basis for `WebSecurityExpressionRoot`.
- Spring Security supplies this root object to `StandardEvaluationContext` when preparing to evaluate an authorization expression.
- [[using-authorization-expression-fields-and-methods]]
- === Using Authorization Expression Fields and Methods
- The first thing this provides is an enhanced set of authorization fields and methods to your SpEL expressions.
- What follows is a quick overview of the most common methods:
- * `permitAll` - The request requires no authorization to be invoked; note that in this case, xref:servlet/authentication/architecture.adoc#servlet-authentication-authentication[the `Authentication`] is never retrieved from the session
- * `denyAll` - The request is not allowed under any circumstances; note that in this case, the `Authentication` is never retrieved from the session
- * `hasAuthority` - The request requires that the `Authentication` have xref:servlet/authorization/architecture.adoc#authz-authorities[a `GrantedAuthority`] that matches the given value
- * `hasRole` - A shortcut for `hasAuthority` that prefixes `ROLE_` or whatever is configured as the default prefix
- * `hasAnyAuthority` - The request requires that the `Authentication` have a `GrantedAuthority` that matches any of the given values
- * `hasAnyRole` - A shortcut for `hasAnyAuthority` that prefixes `ROLE_` or whatever is configured as the default prefix
- * `hasPermission` - A hook into your `PermissionEvaluator` instance for doing object-level authorization
- And here is a brief look at the most common fields:
- * `authentication` - The `Authentication` instance associated with this method invocation
- * `principal` - The `Authentication#getPrincipal` associated with this method invocation
- Having now learned the patterns, rules, and how they can be paired together, you should be able to understand what is going on in this more complex example:
- .Authorize Requests Using SpEL
- [tabs]
- ======
- Xml::
- +
- [source,java,role="primary"]
- ----
- <http>
- <intercept-url pattern="/static/**" access="permitAll"/> <1>
- <intercept-url pattern="/admin/**" access="hasRole('ADMIN')"/> <2>
- <intercept-url pattern="/db/**" access="hasAuthority('db') and hasRole('ADMIN')"/> <3>
- <intercept-url pattern="/**" access="denyAll"/> <4>
- </http>
- ----
- ======
- <1> We specified a URL patters that any user can access.
- Specifically, any user can access a request if the URL starts with "/static/".
- <2> Any URL that starts with "/admin/" will be restricted to users who have the role "ROLE_ADMIN".
- You will notice that since we are invoking the `hasRole` method we do not need to specify the "ROLE_" prefix.
- <3> Any URL that starts with "/db/" requires the user to have both been granted the "db" permission as well as be a "ROLE_ADMIN".
- You will notice that since we are using the `hasRole` expression we do not need to specify the "ROLE_" prefix.
- <4> Any URL that has not already been matched on is denied access.
- This is a good strategy if you do not want to accidentally forget to update your authorization rules.
- [[using_path_parameters]]
- === Using Path Parameters
- Additionally, Spring Security provides a mechanism for discovering path parameters so they can also be accessed in the SpEL expression as well.
- For example, you can access a path parameter in your SpEL expression in the following way:
- .Authorize Request using SpEL path variable
- [tabs]
- ======
- Xml::
- +
- [source,xml,role="primary"]
- ----
- <http>
- <intercept-url pattern="/resource/{name}" access="#name == authentication.name"/>
- <intercept-url pattern="/**" access="authenticated"/>
- </http>
- ----
- ======
- This expression refers to the path variable after `/resource/` and requires that it is equal to `Authentication#getName`.
- [[remote-authorization-manager]]
- === Use an Authorization Database, Policy Agent, or Other Service
- If you want to configure Spring Security to use a separate service for authorization, you can create your own `AuthorizationManager` and match it to `anyRequest`.
- First, your `AuthorizationManager` may look something like this:
- .Open Policy Agent Authorization Manager
- [tabs]
- ======
- Java::
- +
- [source,java,role="primary"]
- ----
- @Component
- public final class OpenPolicyAgentAuthorizationManager implements AuthorizationManager<RequestAuthorizationContext> {
- @Override
- public AuthorizationDecision check(Supplier<Authentication> authentication, RequestAuthorizationContext context) {
- // make request to Open Policy Agent
- }
- }
- ----
- ======
- Then, you can wire it into Spring Security in the following way:
- .Any Request Goes to Remote Service
- [tabs]
- ======
- Java::
- +
- [source,java,role="primary"]
- ----
- @Bean
- SecurityFilterChain web(HttpSecurity http, AuthorizationManager<RequestAuthorizationContext> authz) throws Exception {
- http
- // ...
- .authorizeHttpRequests((authorize) -> authorize
- .anyRequest().access(authz)
- );
- return http.build();
- }
- ----
- ======
- [[favor-permitall]]
- === Favor `permitAll` over `ignoring`
- When you have static resources it can be tempting to configure the filter chain to ignore these values.
- A more secure approach is to permit them using `permitAll` like so:
- .Permit Static Resources
- ====
- .Java
- [source,java,role="secondary"]
- ----
- http
- .authorizeHttpRequests((authorize) -> authorize
- .requestMatchers("/css/**").permitAll()
- .anyRequest().authenticated()
- )
- ----
- .Kotlin
- [source,kotlin,role="secondary"]
- ----
- http {
- authorizeHttpRequests {
- authorize("/css/**", permitAll)
- authorize(anyRequest, authenticated)
- }
- }
- ----
- ====
- It's more secure because even with static resources it's important to write secure headers, which Spring Security cannot do if the request is ignored.
- In this past, this came with a performance tradeoff since the session was consulted by Spring Security on every request.
- As of Spring Security 6, however, the session is no longer pinged unless required by the authorization rule.
- Because the performance impact is now addressed, Spring Security recommends using at least `permitAll` for all requests.
- [[migrate-authorize-requests]]
- == Migrating from `authorizeRequests`
- [NOTE]
- `AuthorizationFilter` supersedes {security-api-url}org/springframework/security/web/access/intercept/FilterSecurityInterceptor.html[`FilterSecurityInterceptor`].
- To remain backward compatible, `FilterSecurityInterceptor` remains the default.
- This section discusses how `AuthorizationFilter` works and how to override the default configuration.
- The {security-api-url}org/springframework/security/web/access/intercept/AuthorizationFilter.html[`AuthorizationFilter`] provides xref:servlet/authorization/index.adoc#servlet-authorization[authorization] for ``HttpServletRequest``s.
- It is inserted into the xref:servlet/architecture.adoc#servlet-filterchainproxy[FilterChainProxy] as one of the xref:servlet/architecture.adoc#servlet-security-filters[Security Filters].
- You can override the default when you declare a `SecurityFilterChain`.
- Instead of using {security-api-url}org/springframework/security/config/annotation/web/builders/HttpSecurity.html#authorizeRequests()[`authorizeRequests`], use `authorizeHttpRequests`, like so:
- .Use authorizeHttpRequests
- [tabs]
- ======
- Java::
- +
- [source,java,role="primary"]
- ----
- @Bean
- SecurityFilterChain web(HttpSecurity http) throws AuthenticationException {
- http
- .authorizeHttpRequests((authorize) -> authorize
- .anyRequest().authenticated();
- )
- // ...
- return http.build();
- }
- ----
- ======
- This improves on `authorizeRequests` in a number of ways:
- 1. Uses the simplified `AuthorizationManager` API instead of metadata sources, config attributes, decision managers, and voters.
- This simplifies reuse and customization.
- 2. Delays `Authentication` lookup.
- Instead of the authentication needing to be looked up for every request, it will only look it up in requests where an authorization decision requires authentication.
- 3. Bean-based configuration support.
- When `authorizeHttpRequests` is used instead of `authorizeRequests`, then {security-api-url}org/springframework/security/web/access/intercept/AuthorizationFilter.html[`AuthorizationFilter`] is used instead of {security-api-url}org/springframework/security/web/access/intercept/FilterSecurityInterceptor.html[`FilterSecurityInterceptor`].
- === Migrating Expressions
- Where possible, it is recommended that you use type-safe authorization managers instead of SpEL.
- For Java configuration, {security-api-url}org/springframework/security/web/access/expression/WebExpressionAuthorizationManager.html[`WebExpressionAuthorizationManager`] is available to help migrate legacy SpEL.
- To use `WebExpressionAuthorizationManager`, you can construct one with the expression you are trying to migrate, like so:
- [tabs]
- ======
- Java::
- +
- [source,java,role="primary"]
- ----
- .requestMatchers("/test/**").access(new WebExpressionAuthorizationManager("hasRole('ADMIN') && hasRole('USER')"))
- ----
- Kotlin::
- +
- [source,kotlin,role="secondary"]
- ----
- .requestMatchers("/test/**").access(WebExpressionAuthorizationManager("hasRole('ADMIN') && hasRole('USER')"))
- ----
- ======
- If you are referring to a bean in your expression like so: `@webSecurity.check(authentication, request)`, it's recommended that you instead call the bean directly, which will look something like the following:
- [tabs]
- ======
- Java::
- +
- [source,java,role="primary"]
- ----
- .requestMatchers("/test/**").access((authentication, context) ->
- new AuthorizationDecision(webSecurity.check(authentication.get(), context.getRequest())))
- ----
- Kotlin::
- +
- [source,kotlin,role="secondary"]
- ----
- .requestMatchers("/test/**").access((authentication, context): AuthorizationManager<RequestAuthorizationContext> ->
- AuthorizationDecision(webSecurity.check(authentication.get(), context.getRequest())))
- ----
- ======
- For complex instructions that include bean references as well as other expressions, it is recommended that you change those to implement `AuthorizationManager` and refer to them by calling `.access(AuthorizationManager)`.
- If you are not able to do that, you can configure a {security-api-url}org/springframework/security/web/access/expression/DefaultHttpSecurityExpressionHandler.html[`DefaultHttpSecurityExpressionHandler`] with a bean resolver and supply that to `WebExpressionAuthorizationManager#setExpressionhandler`.
- [[security-matchers]]
- == Security Matchers
- The {security-api-url}org/springframework/security/web/util/matcher/RequestMatcher.html[`RequestMatcher`] interface is used to determine if a request matches a given rule.
- We use `securityMatchers` to determine if xref:servlet/configuration/java.adoc#jc-httpsecurity[a given `HttpSecurity`] should be applied to a given request.
- The same way, we can use `requestMatchers` to determine the authorization rules that we should apply to a given request.
- Look at the following example:
- [tabs]
- ======
- Java::
- +
- [source,java,role="primary"]
- ----
- @Configuration
- @EnableWebSecurity
- public class SecurityConfig {
- @Bean
- public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
- http
- .securityMatcher("/api/**") <1>
- .authorizeHttpRequests(authorize -> authorize
- .requestMatchers("/user/**").hasRole("USER") <2>
- .requestMatchers("/admin/**").hasRole("ADMIN") <3>
- .anyRequest().authenticated() <4>
- )
- .formLogin(withDefaults());
- return http.build();
- }
- }
- ----
- Kotlin::
- +
- [source,kotlin,role="secondary"]
- ----
- @Configuration
- @EnableWebSecurity
- open class SecurityConfig {
- @Bean
- open fun web(http: HttpSecurity): SecurityFilterChain {
- http {
- securityMatcher("/api/**") <1>
- authorizeHttpRequests {
- authorize("/user/**", hasRole("USER")) <2>
- authorize("/admin/**", hasRole("ADMIN")) <3>
- authorize(anyRequest, authenticated) <4>
- }
- }
- return http.build()
- }
- }
- ----
- ======
- <1> Configure `HttpSecurity` to only be applied to URLs that start with `/api/`
- <2> Allow access to URLs that start with `/user/` to users with the `USER` role
- <3> Allow access to URLs that start with `/admin/` to users with the `ADMIN` role
- <4> Any other request that doesn't match the rules above, will require authentication
- The `securityMatcher(s)` and `requestMatcher(s)` methods will decide which `RequestMatcher` implementation fits best for your application: If {spring-framework-reference-url}web.html#spring-web[Spring MVC] is in the classpath, then {security-api-url}org/springframework/security/web/servlet/util/matcher/MvcRequestMatcher.html[`MvcRequestMatcher`] will be used, otherwise, {security-api-url}org/springframework/security/web/servlet/util/matcher/AntPathRequestMatcher.html[`AntPathRequestMatcher`] will be used.
- You can read more about the Spring MVC integration xref:servlet/integrations/mvc.adoc[here].
- If you want to use a specific `RequestMatcher`, just pass an implementation to the `securityMatcher` and/or `requestMatcher` methods:
- [tabs]
- ======
- Java::
- +
- [source,java,role="primary"]
- ----
- import static org.springframework.security.web.util.matcher.AntPathRequestMatcher.antMatcher; <1>
- import static org.springframework.security.web.util.matcher.RegexRequestMatcher.regexMatcher;
- @Configuration
- @EnableWebSecurity
- public class SecurityConfig {
- @Bean
- public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
- http
- .securityMatcher(antMatcher("/api/**")) <2>
- .authorizeHttpRequests(authorize -> authorize
- .requestMatchers(antMatcher("/user/**")).hasRole("USER") <3>
- .requestMatchers(regexMatcher("/admin/.*")).hasRole("ADMIN") <4>
- .requestMatchers(new MyCustomRequestMatcher()).hasRole("SUPERVISOR") <5>
- .anyRequest().authenticated()
- )
- .formLogin(withDefaults());
- return http.build();
- }
- }
- public class MyCustomRequestMatcher implements RequestMatcher {
- @Override
- public boolean matches(HttpServletRequest request) {
- // ...
- }
- }
- ----
- Kotlin::
- +
- [source,kotlin,role="secondary"]
- ----
- import org.springframework.security.web.util.matcher.AntPathRequestMatcher.antMatcher <1>
- import org.springframework.security.web.util.matcher.RegexRequestMatcher.regexMatcher
- @Configuration
- @EnableWebSecurity
- open class SecurityConfig {
- @Bean
- open fun web(http: HttpSecurity): SecurityFilterChain {
- http {
- securityMatcher(antMatcher("/api/**")) <2>
- authorizeHttpRequests {
- authorize(antMatcher("/user/**"), hasRole("USER")) <3>
- authorize(regexMatcher("/admin/**"), hasRole("ADMIN")) <4>
- authorize(MyCustomRequestMatcher(), hasRole("SUPERVISOR")) <5>
- authorize(anyRequest, authenticated)
- }
- }
- return http.build()
- }
- }
- ----
- ======
- <1> Import the static factory methods from `AntPathRequestMatcher` and `RegexRequestMatcher` to create `RequestMatcher` instances.
- <2> Configure `HttpSecurity` to only be applied to URLs that start with `/api/`, using `AntPathRequestMatcher`
- <3> Allow access to URLs that start with `/user/` to users with the `USER` role, using `AntPathRequestMatcher`
- <4> Allow access to URLs that start with `/admin/` to users with the `ADMIN` role, using `RegexRequestMatcher`
- <5> Allow access to URLs that match the `MyCustomRequestMatcher` to users with the `SUPERVISOR` role, using a custom `RequestMatcher`
- == Further Reading
- Now that you have secured your application's requests, consider xref:servlet/authorization/method-security.adoc[securing its methods].
- You can also read further on xref:servlet/test/index.adoc[testing your application] or on integrating Spring Security with other aspects of you application like xref:servlet/integrations/data.adoc[the data layer] or xref:servlet/integrations/observability.adoc[tracing and metrics].
|