|
@@ -2256,14 +2256,13 @@ You can also add the Spring Boot property `spring.jackson.default-property-inclu
|
|
[[fallback-values-authorization-denied]]
|
|
[[fallback-values-authorization-denied]]
|
|
== Providing Fallback Values When Authorization is Denied
|
|
== Providing Fallback Values When Authorization is Denied
|
|
|
|
|
|
-There are some scenarios where you may not wish to throw an `AccessDeniedException` when a method is invoked without the required permissions.
|
|
|
|
-Instead, you might wish to return a post-processed result, like a masked result, or a default value in cases where access denied happened before invoking the method.
|
|
|
|
|
|
+There are some scenarios where you may not wish to throw an `AuthorizationDeniedException` when a method is invoked without the required permissions.
|
|
|
|
+Instead, you might wish to return a post-processed result, like a masked result, or a default value in cases where authorization denied happened before invoking the method.
|
|
|
|
|
|
-Spring Security provides support for handling and post-processing method access denied by combining {security-api-url}org/springframework/security/authorization/method/AuthorizationDeniedHandler.html[`@AuthorizationDeniedHandler`] with the <<authorizing-with-annotations,`@PreAuthorize` and `@PostAuthorize` annotations>> respectively.
|
|
|
|
|
|
+Spring Security provides support for handling authorization denied on method invocation by using the {security-api-url}org/springframework/security/authorization/method/HandleAuthorizationDenied.html[`@HandleAuthorizationDenied`].
|
|
|
|
+The handler works for denied authorizations that happened in the <<authorizing-with-annotations,`@PreAuthorize` and `@PostAuthorize` annotations>> as well as {security-api-url}org/springframework/security/authorization/AuthorizationDeniedException.html[`AuthorizationDeniedException`] thrown from the method invocation itself.
|
|
|
|
|
|
-=== Using with `@PreAuthorize`
|
|
|
|
-
|
|
|
|
-Let's consider the example from the <<authorize-object,previous section>>, but instead of creating the `AccessDeniedExceptionInterceptor` to transform an `AccessDeniedException` to a `null` return value, we will use the `handlerClass` attribute from `@AuthorizationDeniedHandler`:
|
|
|
|
|
|
+Let's consider the example from the <<authorize-object,previous section>>, but instead of creating the `AccessDeniedExceptionInterceptor` to transform an `AccessDeniedException` to a `null` return value, we will use the `handlerClass` attribute from `@HandleAuthorizationDenied`:
|
|
|
|
|
|
[tabs]
|
|
[tabs]
|
|
======
|
|
======
|
|
@@ -2274,7 +2273,7 @@ Java::
|
|
public class NullMethodAuthorizationDeniedHandler implements MethodAuthorizationDeniedHandler { <1>
|
|
public class NullMethodAuthorizationDeniedHandler implements MethodAuthorizationDeniedHandler { <1>
|
|
|
|
|
|
@Override
|
|
@Override
|
|
- public Object handle(MethodInvocation methodInvocation, AuthorizationResult authorizationResult) {
|
|
|
|
|
|
+ public Object handleDeniedInvocation(MethodInvocation methodInvocation, AuthorizationResult authorizationResult) {
|
|
return null;
|
|
return null;
|
|
}
|
|
}
|
|
|
|
|
|
@@ -2295,7 +2294,7 @@ public class User {
|
|
// ...
|
|
// ...
|
|
|
|
|
|
@PreAuthorize(value = "hasAuthority('user:read')")
|
|
@PreAuthorize(value = "hasAuthority('user:read')")
|
|
- @AuthorizationDeniedHandler(handlerClass = NullMethodAuthorizationDeniedHandler.class)
|
|
|
|
|
|
+ @HandleAuthorizationDenied(handlerClass = NullMethodAuthorizationDeniedHandler.class)
|
|
public String getEmail() {
|
|
public String getEmail() {
|
|
return this.email;
|
|
return this.email;
|
|
}
|
|
}
|
|
@@ -2308,7 +2307,7 @@ Kotlin::
|
|
----
|
|
----
|
|
class NullMethodAuthorizationDeniedHandler : MethodAuthorizationDeniedHandler { <1>
|
|
class NullMethodAuthorizationDeniedHandler : MethodAuthorizationDeniedHandler { <1>
|
|
|
|
|
|
- override fun handle(methodInvocation: MethodInvocation, authorizationResult: AuthorizationResult): Any {
|
|
|
|
|
|
+ override fun handleDeniedInvocation(methodInvocation: MethodInvocation, authorizationResult: AuthorizationResult): Any {
|
|
return null
|
|
return null
|
|
}
|
|
}
|
|
|
|
|
|
@@ -2325,13 +2324,13 @@ class SecurityConfig {
|
|
|
|
|
|
}
|
|
}
|
|
|
|
|
|
-class User (val name:String, @PreAuthorize(value = "hasAuthority('user:read')") @AuthorizationDeniedHandler(handlerClass = NullMethodAuthorizationDeniedHandler::class) val email:String) <3>
|
|
|
|
|
|
+class User (val name:String, @PreAuthorize(value = "hasAuthority('user:read')") @HandleAuthorizationDenied(handlerClass = NullMethodAuthorizationDeniedHandler::class) val email:String) <3>
|
|
----
|
|
----
|
|
======
|
|
======
|
|
|
|
|
|
<1> Create an implementation of `MethodAuthorizationDeniedHandler` that returns a `null` value
|
|
<1> Create an implementation of `MethodAuthorizationDeniedHandler` that returns a `null` value
|
|
<2> Register the `NullMethodAuthorizationDeniedHandler` as a bean
|
|
<2> Register the `NullMethodAuthorizationDeniedHandler` as a bean
|
|
-<3> Annotate the method with `@AuthorizationDeniedHandler` and pass the `NullMethodAuthorizationDeniedHandler` to the `handlerClass` attribute
|
|
|
|
|
|
+<3> Annotate the method with `@HandleAuthorizationDenied` and pass the `NullMethodAuthorizationDeniedHandler` to the `handlerClass` attribute
|
|
|
|
|
|
And then you can verify that a `null` value is returned instead of the `AccessDeniedException`:
|
|
And then you can verify that a `null` value is returned instead of the `AccessDeniedException`:
|
|
|
|
|
|
@@ -2371,9 +2370,12 @@ fun getEmailWhenProxiedThenNullEmail() {
|
|
----
|
|
----
|
|
======
|
|
======
|
|
|
|
|
|
-=== Using with `@PostAuthorize`
|
|
|
|
|
|
+=== Using the Denied Result From the Method Invocation
|
|
|
|
+
|
|
|
|
+There are some scenarios where you might want to return a secure result derived from the denied result.
|
|
|
|
+For example, if a user is not authorized to see email addresses, you might want to apply some masking on the original email address, i.e. _useremail@example.com_ would become _use\\******@example.com_.
|
|
|
|
|
|
-The same can be achieved with `@PostAuthorize`, however, since `@PostAuthorize` checks are performed after the method is invoked, we have access to the resulting value of the invocation, allowing you to provide fallback values based on the unauthorized results.
|
|
|
|
|
|
+For those scenarios, you can override the `handleDeniedInvocationResult` from the `MethodAuthorizationDeniedHandler`, which has the {security-api-url}org/springframework/security/authorization/method/MethodInvocationResult.html[`MethodInvocationResult`] as an argument.
|
|
Let's continue with the previous example, but instead of returning `null`, we will return a masked value of the email:
|
|
Let's continue with the previous example, but instead of returning `null`, we will return a masked value of the email:
|
|
|
|
|
|
[tabs]
|
|
[tabs]
|
|
@@ -2382,10 +2384,15 @@ Java::
|
|
+
|
|
+
|
|
[source,java,role="primary"]
|
|
[source,java,role="primary"]
|
|
----
|
|
----
|
|
-public class EmailMaskingMethodAuthorizationDeniedPostProcessor implements MethodAuthorizationDeniedPostProcessor { <1>
|
|
|
|
|
|
+public class EmailMaskingMethodAuthorizationDeniedHandler implements MethodAuthorizationDeniedHandler { <1>
|
|
|
|
+
|
|
|
|
+ @Override
|
|
|
|
+ public Object handleDeniedInvocation(MethodInvocation methodInvocation, AuthorizationResult authorizationResult) {
|
|
|
|
+ return "***";
|
|
|
|
+ }
|
|
|
|
|
|
@Override
|
|
@Override
|
|
- public Object postProcessResult(MethodInvocationResult methodInvocationResult, AuthorizationResult authorizationResult) {
|
|
|
|
|
|
+ public Object handleDeniedInvocationResult(MethodInvocationResult methodInvocationResult, AuthorizationResult authorizationResult) {
|
|
String email = (String) methodInvocationResult.getResult();
|
|
String email = (String) methodInvocationResult.getResult();
|
|
return email.replaceAll("(^[^@]{3}|(?!^)\\G)[^@]", "$1*");
|
|
return email.replaceAll("(^[^@]{3}|(?!^)\\G)[^@]", "$1*");
|
|
}
|
|
}
|
|
@@ -2397,8 +2404,8 @@ public class EmailMaskingMethodAuthorizationDeniedPostProcessor implements Metho
|
|
public class SecurityConfig {
|
|
public class SecurityConfig {
|
|
|
|
|
|
@Bean <2>
|
|
@Bean <2>
|
|
- public EmailMaskingMethodAuthorizationDeniedPostProcessor emailMaskingMethodAuthorizationDeniedPostProcessor() {
|
|
|
|
- return new EmailMaskingMethodAuthorizationDeniedPostProcessor();
|
|
|
|
|
|
+ public EmailMaskingMethodAuthorizationDeniedHandler emailMaskingMethodAuthorizationDeniedHandler() {
|
|
|
|
+ return new EmailMaskingMethodAuthorizationDeniedHandler();
|
|
}
|
|
}
|
|
|
|
|
|
}
|
|
}
|
|
@@ -2407,7 +2414,7 @@ public class User {
|
|
// ...
|
|
// ...
|
|
|
|
|
|
@PostAuthorize(value = "hasAuthority('user:read')")
|
|
@PostAuthorize(value = "hasAuthority('user:read')")
|
|
- @AuthorizationDeniedHandler(postProcessorClass = EmailMaskingMethodAuthorizationDeniedPostProcessor.class)
|
|
|
|
|
|
+ @HandleAuthorizationDenied(handlerClass = EmailMaskingMethodAuthorizationDeniedHandler.class)
|
|
public String getEmail() {
|
|
public String getEmail() {
|
|
return this.email;
|
|
return this.email;
|
|
}
|
|
}
|
|
@@ -2418,9 +2425,13 @@ Kotlin::
|
|
+
|
|
+
|
|
[source,kotlin,role="secondary"]
|
|
[source,kotlin,role="secondary"]
|
|
----
|
|
----
|
|
-class EmailMaskingMethodAuthorizationDeniedPostProcessor : MethodAuthorizationDeniedPostProcessor {
|
|
|
|
|
|
+class EmailMaskingMethodAuthorizationDeniedHandler : MethodAuthorizationDeniedHandler {
|
|
|
|
|
|
- override fun postProcessResult(methodInvocationResult: MethodInvocationResult, authorizationResult: AuthorizationResult): Any {
|
|
|
|
|
|
+ override fun handleDeniedInvocation(methodInvocation: MethodInvocation, authorizationResult: AuthorizationResult): Any {
|
|
|
|
+ return "***"
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ override fun handleDeniedInvocationResult(methodInvocationResult: MethodInvocationResult, authorizationResult: AuthorizationResult): Any {
|
|
val email = methodInvocationResult.result as String
|
|
val email = methodInvocationResult.result as String
|
|
return email.replace("(^[^@]{3}|(?!^)\\G)[^@]".toRegex(), "$1*")
|
|
return email.replace("(^[^@]{3}|(?!^)\\G)[^@]".toRegex(), "$1*")
|
|
}
|
|
}
|
|
@@ -2432,22 +2443,27 @@ class EmailMaskingMethodAuthorizationDeniedPostProcessor : MethodAuthorizationDe
|
|
class SecurityConfig {
|
|
class SecurityConfig {
|
|
|
|
|
|
@Bean
|
|
@Bean
|
|
- fun emailMaskingMethodAuthorizationDeniedPostProcessor(): EmailMaskingMethodAuthorizationDeniedPostProcessor {
|
|
|
|
- return EmailMaskingMethodAuthorizationDeniedPostProcessor()
|
|
|
|
|
|
+ fun emailMaskingMethodAuthorizationDeniedHandler(): EmailMaskingMethodAuthorizationDeniedHandler {
|
|
|
|
+ return EmailMaskingMethodAuthorizationDeniedHandler()
|
|
}
|
|
}
|
|
|
|
|
|
}
|
|
}
|
|
|
|
|
|
-class User (val name:String, @PostAuthorize(value = "hasAuthority('user:read')") @AuthorizationDeniedHandler(postProcessorClass = EmailMaskingMethodAuthorizationDeniedPostProcessor::class) val email:String) <3>
|
|
|
|
|
|
+class User (val name:String, @PostAuthorize(value = "hasAuthority('user:read')") @HandleAuthorizationDenied(handlerClass = EmailMaskingMethodAuthorizationDeniedHandler::class) val email:String) <3>
|
|
----
|
|
----
|
|
======
|
|
======
|
|
|
|
|
|
-<1> Create an implementation of `MethodAuthorizationDeniedPostProcessor` that returns a masked value of the unauthorized result value
|
|
|
|
-<2> Register the `EmailMaskingMethodAuthorizationDeniedPostProcessor` as a bean
|
|
|
|
-<3> Annotate the method with `@AuthorizationDeniedHandler` and pass the `EmailMaskingMethodAuthorizationDeniedPostProcessor` to the `postProcessorClass` attribute
|
|
|
|
|
|
+<1> Create an implementation of `MethodAuthorizationDeniedHandler` that returns a masked value of the unauthorized result value
|
|
|
|
+<2> Register the `EmailMaskingMethodAuthorizationDeniedHandler` as a bean
|
|
|
|
+<3> Annotate the method with `@HandleAuthorizationDenied` and pass the `EmailMaskingMethodAuthorizationDeniedHandler` to the `handlerClass` attribute
|
|
|
|
|
|
And then you can verify that a masked email is returned instead of an `AccessDeniedException`:
|
|
And then you can verify that a masked email is returned instead of an `AccessDeniedException`:
|
|
|
|
|
|
|
|
+[WARNING]
|
|
|
|
+====
|
|
|
|
+Since you have access to the original denied value, make sure that you correctly handle it and do not return it to the caller.
|
|
|
|
+====
|
|
|
|
+
|
|
[tabs]
|
|
[tabs]
|
|
======
|
|
======
|
|
Java::
|
|
Java::
|
|
@@ -2481,20 +2497,20 @@ fun getEmailWhenProxiedThenMaskedEmail() {
|
|
----
|
|
----
|
|
======
|
|
======
|
|
|
|
|
|
-When implementing the `MethodAuthorizationDeniedHandler` or the `MethodAuthorizationDeniedPostProcessor` you have a few options on what you can return:
|
|
|
|
|
|
+When implementing the `MethodAuthorizationDeniedHandler` you have a few options on what type you can return:
|
|
|
|
|
|
- A `null` value.
|
|
- A `null` value.
|
|
- A non-null value, respecting the method's return type.
|
|
- A non-null value, respecting the method's return type.
|
|
-- Throw an exception, usually an instance of `AccessDeniedException`. This is the default behavior.
|
|
|
|
|
|
+- Throw an exception, usually an instance of `AuthorizationDeniedException`. This is the default behavior.
|
|
- A `Mono` type for reactive applications.
|
|
- A `Mono` type for reactive applications.
|
|
|
|
|
|
-Note that since the handler and the post-processor must be registered as beans, you can inject dependencies into them if you need a more complex logic.
|
|
|
|
|
|
+Note that since the handler must be registered as beans in your application context, you can inject dependencies into them if you need a more complex logic.
|
|
In addition to that, you have available the `MethodInvocation` or the `MethodInvocationResult`, as well as the `AuthorizationResult` for more details related to the authorization decision.
|
|
In addition to that, you have available the `MethodInvocation` or the `MethodInvocationResult`, as well as the `AuthorizationResult` for more details related to the authorization decision.
|
|
|
|
|
|
[[deciding-return-based-parameters]]
|
|
[[deciding-return-based-parameters]]
|
|
=== Deciding What to Return Based on Available Parameters
|
|
=== Deciding What to Return Based on Available Parameters
|
|
|
|
|
|
-Consider a scenario where there might be multiple mask values for different methods, it would be not so productive if we had to create a handler or post-processor for each of those methods, although it is perfectly fine to do that.
|
|
|
|
|
|
+Consider a scenario where there might be multiple mask values for different methods, it would be not so productive if we had to create a handler for each of those methods, although it is perfectly fine to do that.
|
|
In such cases, we can use the information passed via parameters to decide what to do.
|
|
In such cases, we can use the information passed via parameters to decide what to do.
|
|
For example, we can create a custom `@Mask` annotation and a handler that detects that annotation to decide what mask value to return:
|
|
For example, we can create a custom `@Mask` annotation and a handler that detects that annotation to decide what mask value to return:
|
|
|
|
|
|
@@ -2517,7 +2533,7 @@ public @interface Mask {
|
|
public class MaskAnnotationDeniedHandler implements MethodAuthorizationDeniedHandler {
|
|
public class MaskAnnotationDeniedHandler implements MethodAuthorizationDeniedHandler {
|
|
|
|
|
|
@Override
|
|
@Override
|
|
- public Object handle(MethodInvocation methodInvocation, AuthorizationResult authorizationResult) {
|
|
|
|
|
|
+ public Object handleDeniedInvocation(MethodInvocation methodInvocation, AuthorizationResult authorizationResult) {
|
|
Mask mask = AnnotationUtils.getAnnotation(methodInvocation.getMethod(), Mask.class);
|
|
Mask mask = AnnotationUtils.getAnnotation(methodInvocation.getMethod(), Mask.class);
|
|
return mask.value();
|
|
return mask.value();
|
|
}
|
|
}
|
|
@@ -2539,14 +2555,14 @@ public class SecurityConfig {
|
|
public class MyService {
|
|
public class MyService {
|
|
|
|
|
|
@PreAuthorize(value = "hasAuthority('user:read')")
|
|
@PreAuthorize(value = "hasAuthority('user:read')")
|
|
- @AuthorizationDeniedHandler(handlerClass = MaskAnnotationDeniedHandler.class)
|
|
|
|
|
|
+ @HandleAuthorizationDenied(handlerClass = MaskAnnotationDeniedHandler.class)
|
|
@Mask("***")
|
|
@Mask("***")
|
|
public String foo() {
|
|
public String foo() {
|
|
return "foo";
|
|
return "foo";
|
|
}
|
|
}
|
|
|
|
|
|
@PreAuthorize(value = "hasAuthority('user:read')")
|
|
@PreAuthorize(value = "hasAuthority('user:read')")
|
|
- @AuthorizationDeniedHandler(handlerClass = MaskAnnotationDeniedHandler.class)
|
|
|
|
|
|
+ @HandleAuthorizationDenied(handlerClass = MaskAnnotationDeniedHandler.class)
|
|
@Mask("???")
|
|
@Mask("???")
|
|
public String bar() {
|
|
public String bar() {
|
|
return "bar";
|
|
return "bar";
|
|
@@ -2567,7 +2583,7 @@ annotation class Mask(val value: String)
|
|
|
|
|
|
class MaskAnnotationDeniedHandler : MethodAuthorizationDeniedHandler {
|
|
class MaskAnnotationDeniedHandler : MethodAuthorizationDeniedHandler {
|
|
|
|
|
|
- override fun handle(methodInvocation: MethodInvocation, authorizationResult: AuthorizationResult): Any {
|
|
|
|
|
|
+ override fun handleDeniedInvocation(methodInvocation: MethodInvocation, authorizationResult: AuthorizationResult): Any {
|
|
val mask = AnnotationUtils.getAnnotation(methodInvocation.method, Mask::class.java)
|
|
val mask = AnnotationUtils.getAnnotation(methodInvocation.method, Mask::class.java)
|
|
return mask.value
|
|
return mask.value
|
|
}
|
|
}
|
|
@@ -2589,14 +2605,14 @@ class SecurityConfig {
|
|
class MyService {
|
|
class MyService {
|
|
|
|
|
|
@PreAuthorize(value = "hasAuthority('user:read')")
|
|
@PreAuthorize(value = "hasAuthority('user:read')")
|
|
- @AuthorizationDeniedHandler(handlerClass = MaskAnnotationDeniedHandler::class)
|
|
|
|
|
|
+ @HandleAuthorizationDenied(handlerClass = MaskAnnotationDeniedHandler::class)
|
|
@Mask("***")
|
|
@Mask("***")
|
|
fun foo(): String {
|
|
fun foo(): String {
|
|
return "foo"
|
|
return "foo"
|
|
}
|
|
}
|
|
|
|
|
|
@PreAuthorize(value = "hasAuthority('user:read')")
|
|
@PreAuthorize(value = "hasAuthority('user:read')")
|
|
- @AuthorizationDeniedHandler(handlerClass = MaskAnnotationDeniedHandler::class)
|
|
|
|
|
|
+ @HandleAuthorizationDenied(handlerClass = MaskAnnotationDeniedHandler::class)
|
|
@Mask("???")
|
|
@Mask("???")
|
|
fun bar(): String {
|
|
fun bar(): String {
|
|
return "bar"
|
|
return "bar"
|
|
@@ -2653,8 +2669,8 @@ fun barWhenDeniedThenReturnQuestionMarks() {
|
|
|
|
|
|
=== Combining with Meta Annotation Support
|
|
=== Combining with Meta Annotation Support
|
|
|
|
|
|
-You can also combine the `@AuthorizationDeniedHandler` with other annotations in order to reduce and simplify the annotations in a method.
|
|
|
|
-Let's consider the <<deciding-return-based-parameters,example from the previous section>> and merge `@AuthorizationDeniedHandler` with `@Mask`:
|
|
|
|
|
|
+You can also combine the `@HandleAuthorizationDenied` with other annotations in order to reduce and simplify the annotations in a method.
|
|
|
|
+Let's consider the <<deciding-return-based-parameters,example from the previous section>> and merge `@HandleAuthorizationDenied` with `@Mask`:
|
|
|
|
|
|
[tabs]
|
|
[tabs]
|
|
======
|
|
======
|
|
@@ -2664,7 +2680,7 @@ Java::
|
|
----
|
|
----
|
|
@Target({ ElementType.METHOD, ElementType.TYPE })
|
|
@Target({ ElementType.METHOD, ElementType.TYPE })
|
|
@Retention(RetentionPolicy.RUNTIME)
|
|
@Retention(RetentionPolicy.RUNTIME)
|
|
-@AuthorizationDeniedHandler(handlerClass = MaskAnnotationDeniedHandler.class)
|
|
|
|
|
|
+@HandleAuthorizationDenied(handlerClass = MaskAnnotationDeniedHandler.class)
|
|
public @interface Mask {
|
|
public @interface Mask {
|
|
|
|
|
|
String value();
|
|
String value();
|
|
@@ -2683,7 +2699,7 @@ Kotlin::
|
|
----
|
|
----
|
|
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS)
|
|
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS)
|
|
@Retention(AnnotationRetention.RUNTIME)
|
|
@Retention(AnnotationRetention.RUNTIME)
|
|
-@AuthorizationDeniedHandler(handlerClass = MaskAnnotationDeniedHandler::class)
|
|
|
|
|
|
+@HandleAuthorizationDenied(handlerClass = MaskAnnotationDeniedHandler::class)
|
|
annotation class Mask(val value: String)
|
|
annotation class Mask(val value: String)
|
|
|
|
|
|
@Mask("***")
|
|
@Mask("***")
|