Skip to content

Commit 756d00b

Browse files
committed
Document Programmatic Authorization in Reactive
1 parent 60cd8fd commit 756d00b

File tree

1 file changed

+209
-0
lines changed
  • docs/modules/ROOT/pages/reactive/authorization

1 file changed

+209
-0
lines changed

docs/modules/ROOT/pages/reactive/authorization/method.adoc

Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,215 @@ We expose `GrantedAuthorityDefaults` using a `static` method to ensure that Spri
118118
Since the `GrantedAuthorityDefaults` bean is part of internal workings of Spring Security, we should also expose it as an infrastructural bean effectively avoiding some warnings related to bean post-processing (see https://github.com/spring-projects/spring-security/issues/14751[gh-14751]).
119119
====
120120

121+
[[use-programmatic-authorization]]
122+
== Authorizing Methods Programmatically
123+
124+
As you've already seen, there are several ways that you can specify non-trivial authorization rules using xref:servlet/authorization/method-security.adoc#authorization-expressions[Method Security SpEL expressions].
125+
126+
There are a number of ways that you can instead allow your logic to be Java-based instead of SpEL-based.
127+
This gives use access the entire Java language for increased testability and flow control.
128+
129+
=== Using a Custom Bean in SpEL
130+
131+
The first way to authorize a method programmatically is a two-step process.
132+
133+
First, declare a bean that has a method that takes a `MethodSecurityExpressionOperations` instance like the following:
134+
135+
[tabs]
136+
======
137+
Java::
138+
+
139+
[source,java,role="primary"]
140+
----
141+
@Component("authz")
142+
public class AuthorizationLogic {
143+
public decide(MethodSecurityExpressionOperations operations): Mono<Boolean> {
144+
// ... authorization logic
145+
}
146+
}
147+
----
148+
149+
Kotlin::
150+
+
151+
[source,kotlin,role="secondary"]
152+
----
153+
@Component("authz")
154+
open class AuthorizationLogic {
155+
fun decide(val operations: MethodSecurityExpressionOperations): Mono<Boolean> {
156+
// ... authorization logic
157+
}
158+
}
159+
----
160+
======
161+
162+
Then, reference that bean in your annotations in the following way:
163+
164+
[tabs]
165+
======
166+
Java::
167+
+
168+
[source,java,role="primary"]
169+
----
170+
@Controller
171+
public class MyController {
172+
@PreAuthorize("@authz.decide(#root)")
173+
@GetMapping("/endpoint")
174+
public Mono<String> endpoint() {
175+
// ...
176+
}
177+
}
178+
----
179+
180+
Kotlin::
181+
+
182+
[source,kotlin,role="secondary"]
183+
----
184+
@Controller
185+
open class MyController {
186+
@PreAuthorize("@authz.decide(#root)")
187+
@GetMapping("/endpoint")
188+
fun endpoint(): Mono<String> {
189+
// ...
190+
}
191+
}
192+
----
193+
======
194+
195+
Spring Security will invoke the given method on that bean for each method invocation.
196+
197+
What's nice about this is all your authorization logic is in a separate class that can be independently unit tested and verified for correctness.
198+
It also has access to the full Java language.
199+
200+
[TIP]
201+
In addition to returning a `Mono<Boolean>`, you can also return `Mono.empty()` to indicate that the code abstains from making a decision.
202+
203+
If you want to include more information about the nature of the decision, you can instead return a custom `AuthorizationDecision` like this:
204+
205+
[tabs]
206+
======
207+
Java::
208+
+
209+
[source,java,role="primary"]
210+
----
211+
@Component("authz")
212+
public class AuthorizationLogic {
213+
public Mono<AuthorizationDecision> decide(MethodSecurityExpressionOperations operations) {
214+
// ... authorization logic
215+
return Mono.just(new MyAuthorizationDecision(false, details));
216+
}
217+
}
218+
----
219+
220+
Kotlin::
221+
+
222+
[source,kotlin,role="secondary"]
223+
----
224+
@Component("authz")
225+
open class AuthorizationLogic {
226+
fun decide(val operations: MethodSecurityExpressionOperations): Mono<AuthorizationDecision> {
227+
// ... authorization logic
228+
return Mono.just(MyAuthorizationDecision(false, details))
229+
}
230+
}
231+
----
232+
======
233+
234+
Or throw a custom `AuthorizationDeniedException` instance.
235+
Note, though, that returning an object is preferred as this doesn't incur the expense of generating a stacktrace.
236+
237+
Then, you can access the custom details when you xref:servlet/authorization/method-security.adoc#fallback-values-authorization-denied[customize how the authorization result is handled].
238+
239+
[[custom-authorization-managers]]
240+
=== Using a Custom Authorization Manager
241+
242+
The second way to authorize a method programmatically is to create a custom xref:servlet/authorization/architecture.adoc#_the_authorizationmanager[`AuthorizationManager`].
243+
244+
First, declare an authorization manager instance, perhaps like this one:
245+
246+
[tabs]
247+
======
248+
Java::
249+
+
250+
[source,java,role="primary"]
251+
----
252+
@Component
253+
public class MyPreAuthorizeAuthorizationManager implements ReactiveAuthorizationManager<MethodInvocation> {
254+
@Override
255+
public Mono<AuthorizationDecision> check(Supplier<Authentication> authentication, MethodInvocation invocation) {
256+
// ... authorization logic
257+
}
258+
259+
}
260+
----
261+
262+
Kotlin::
263+
+
264+
[source,kotlin,role="secondary"]
265+
----
266+
@Component
267+
class MyPreAuthorizeAuthorizationManager : ReactiveAuthorizationManager<MethodInvocation> {
268+
override fun check(authentication: Supplier<Authentication>, invocation: MethodInvocation): Mono<AuthorizationDecision> {
269+
// ... authorization logic
270+
}
271+
272+
}
273+
----
274+
======
275+
276+
Then, publish the method interceptor with a pointcut that corresponds to when you want that `ReactiveAuthorizationManager` to run.
277+
For example, you could replace how `@PreAuthorize` and `@PostAuthorize` work like so:
278+
279+
.Only @PreAuthorize and @PostAuthorize Configuration
280+
[tabs]
281+
======
282+
Java::
283+
+
284+
[source,java,role="primary"]
285+
----
286+
@Configuration
287+
@EnableMethodSecurity(prePostEnabled = false)
288+
class MethodSecurityConfig {
289+
@Bean
290+
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
291+
Advisor preAuthorize(MyPreAuthorizeAuthorizationManager manager) {
292+
return AuthorizationManagerBeforeReactiveMethodInterceptor.preAuthorize(manager);
293+
}
294+
295+
@Bean
296+
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
297+
Advisor postAuthorize(MyPostAuthorizeAuthorizationManager manager) {
298+
return AuthorizationManagerAfterReactiveMethodInterceptor.postAuthorize(manager);
299+
}
300+
}
301+
----
302+
303+
Kotlin::
304+
+
305+
[source,kotlin,role="secondary"]
306+
----
307+
@Configuration
308+
@EnableMethodSecurity(prePostEnabled = false)
309+
class MethodSecurityConfig {
310+
@Bean
311+
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
312+
fun preAuthorize(val manager: MyPreAuthorizeAuthorizationManager) : Advisor {
313+
return AuthorizationManagerBeforeReactiveMethodInterceptor.preAuthorize(manager)
314+
}
315+
316+
@Bean
317+
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
318+
fun postAuthorize(val manager: MyPostAuthorizeAuthorizationManager) : Advisor {
319+
return AuthorizationManagerAfterReactiveMethodInterceptor.postAuthorize(manager)
320+
}
321+
}
322+
----
323+
======
324+
325+
[TIP]
326+
====
327+
You can place your interceptor in between Spring Security method interceptors using the order constants specified in `AuthorizationInterceptorsOrder`.
328+
====
329+
121330
[[customizing-expression-handling]]
122331
=== Customizing Expression Handling
123332

0 commit comments

Comments
 (0)