123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215 |
- [[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
- [.invert-dark]
- 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}reference/web/servlet.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
- public 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
- fun web(http: HttpSecurity): SecurityFilterChain {
- 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 javadoc:org.springframework.security.web.util.matcher.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
- [tabs]
- ======
- 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]]
- === Matching by Servlet Path
- Generally speaking, you can use `requestMatchers(String)` as demonstrated above.
- However, if you have authorization rules from multiple servlets, you need to specify those:
- .Match by PathPatternRequestMatcher
- [tabs]
- ======
- Java::
- +
- [source,java,role="primary"]
- ----
- import static org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher.withDefaults;
- @Bean
- SecurityFilterChain appEndpoints(HttpSecurity http) {
- PathPatternRequestMatcher.Builder mvc = withDefaults().basePath("/spring-mvc");
- http
- .authorizeHttpRequests((authorize) -> authorize
- .requestMatchers(mvc.matcher("/admin/**")).hasAuthority("admin")
- .requestMatchers(mvc.matcher("/my/controller/**")).hasAuthority("controller")
- .anyRequest().authenticated()
- );
- return http.build();
- }
- ----
- Kotlin::
- +
- [source,kotlin,role="secondary"]
- ----
- @Bean
- fun appEndpoints(http: HttpSecurity): SecurityFilterChain {
- http {
- authorizeHttpRequests {
- authorize("/spring-mvc", "/admin/**", hasAuthority("admin"))
- authorize("/spring-mvc", "/my/controller/**", hasAuthority("controller"))
- authorize(anyRequest, authenticated)
- }
- }
- }
- ----
- Xml::
- +
- [source,xml,role="secondary"]
- ----
- <http>
- <intercept-url servlet-path="/spring-mvc" pattern="/admin/**" access="hasAuthority('admin')"/>
- <intercept-url servlet-path="/spring-mvc" pattern="/my/controller/**" access="hasAuthority('controller')"/>
- <intercept-url pattern="/**" access="authenticated"/>
- </http>
- ----
- ======
- This is because Spring Security requires all URIs to be absolute (minus the context path).
- [TIP]
- =====
- There are several other components that create request matchers for you like {spring-boot-api-url}org/springframework/boot/autoconfigure/security/servlet/PathRequest.html[`PathRequest#toStaticResources#atCommonLocations`]
- =====
- [[match-by-custom]]
- === Using a Custom Matcher
- [NOTE]
- This feature is not currently supported in XML
- In Java configuration, you can create your own javadoc:org.springframework.security.web.util.matcher.RequestMatcher[] and supply it to the DSL like so:
- .Authorize by Dispatcher Type
- [tabs]
- ======
- 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 javadoc:org.springframework.security.web.util.matcher.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 pattern 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 AuthorizationResult authorize(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
- [tabs]
- ======
- 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 javadoc:org.springframework.security.web.access.intercept.FilterSecurityInterceptor[].
- To remain backward compatible, `FilterSecurityInterceptor` remains the default.
- This section discusses how `AuthorizationFilter` works and how to override the default configuration.
- The javadoc:org.springframework.security.web.access.intercept.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 javadoc:org.springframework.security.config.annotation.web.builders.HttpSecurity#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 javadoc:org.springframework.security.web.access.intercept.AuthorizationFilter[] is used instead of javadoc:org.springframework.security.web.access.intercept.FilterSecurityInterceptor[].
- === Migrating Expressions
- Where possible, it is recommended that you use type-safe authorization managers instead of SpEL.
- For Java configuration, javadoc:org.springframework.security.web.access.expression.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')"))
- ----
- ======
- To migrate several, you can use `WebExpressionAuthorizationManager#withDefaults`:
- [tabs]
- ======
- Java::
- +
- [source,java,role="primary"]
- ----
- WebExpressionAuthorizationManager.Builder authz = WebExpressionAuthorizationManager.withDefaults();
- .requestMatchers("/test/**").access(authz.expression("hasRole('ADMIN') && hasRole('USER')"))
- .requestMatchers("/test/**").access(authz.expression("permitAll"))
- ----
- Kotlin::
- +
- [source,kotlin,role="secondary"]
- ----
- var authz = WebExpressionAuthorizationManager.withDefaults()
- .requestMatchers("/test/**").access(authz.expression("hasRole('ADMIN') && hasRole('USER')"))
- .requestMatchers("/test/**").access(authz.expression("permitAll"))
- ----
- ======
- 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 publish javadoc:org.springframework.security.web.access.expression.WebExpressionAuthorizationManager$Builder[] as a bean:
- [tabs]
- ======
- Java::
- +
- [source,java,role="primary"]
- ----
- @Bean
- WebExpressionAuthorizationManager.Builder authz() {
- return WebExpressionAuthorizationManager.withDefaults();
- }
- ----
- Kotlin::
- +
- [source,kotlin,role="secondary"]
- ----
- @Bean
- fun authz(): WebExpressionAuthorizationManager.Builder {
- return WebExpressionAuthorizationManager.withDefaults()
- }
- ----
- ======
- Then, expressions passed to that builder will be able to refer to beans.
- [[security-matchers]]
- == Security Matchers
- The javadoc:org.springframework.security.web.util.matcher.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("/api/user/**").hasRole("USER") <2>
- .requestMatchers("/api/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("/api/user/**", hasRole("USER")) <2>
- authorize("/api/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 `/api/user/` to users with the `USER` role
- <3> Allow access to URLs that start with `/api/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 construct ``RequestMatcher``s using a javadoc:org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher$Builder[] bean, if available.
- 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.servlet.util.matcher.PathPatternRequestMatcher.withDefaults; <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(withDefaults().matcher("/api/user/**")).hasRole("USER") <3>
- .requestMatchers(regexMatcher("/api/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.servlet.util.matcher.PathPatternRequestMatcher.withDefaults <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(withDefaults().matcher("/api/user/**"), hasRole("USER")) <3>
- authorize(regexMatcher("/api/admin/**"), hasRole("ADMIN")) <4>
- authorize(MyCustomRequestMatcher(), hasRole("SUPERVISOR")) <5>
- authorize(anyRequest, authenticated)
- }
- }
- return http.build()
- }
- }
- ----
- ======
- <1> Import the static factory methods from `PathPatternRequestMatcher` and `RegexRequestMatcher` to create `RequestMatcher` instances.
- <2> Configure `HttpSecurity` to only be applied to URLs that start with `/api/`, using `PathPatternRequestMatcher`
- <3> Allow access to URLs that start with `/api/user/` to users with the `USER` role, using `PathPatternRequestMatcher`
- <4> Allow access to URLs that start with `/api/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].
|