Add Authorization Proxy Support
Closes gh-14596
This commit is contained in:
@@ -1702,6 +1702,397 @@ This works on both classes and interfaces.
|
||||
This does not work for interfaces, since they do not have debug information about the parameter names.
|
||||
For interfaces, either annotations or the `-parameters` approach must be used.
|
||||
|
||||
[[authorize-object]]
|
||||
== Authorizing Arbitrary Objects
|
||||
|
||||
Spring Security also supports wrapping any object that is annotated its method security annotations.
|
||||
|
||||
To achieve this, you can autowire the provided `AuthorizationProxyFactory` instance, which is based on which method security interceptors you have configured.
|
||||
If you are using `@EnableMethodSecurity`, then this means that it will by default have the interceptors for `@PreAuthorize`, `@PostAuthorize`, `@PreFilter`, and `@PostFilter`.
|
||||
|
||||
For example, consider the following `User` class:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
public class User {
|
||||
private String name;
|
||||
private String email;
|
||||
|
||||
public User(String name, String email) {
|
||||
this.name = name;
|
||||
this.email = email;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
@PreAuthorize("hasAuthority('user:read')")
|
||||
public String getEmail() {
|
||||
return this.email;
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
class User (val name:String, @get:PreAuthorize("hasAuthority('user:read')") val email:String)
|
||||
----
|
||||
======
|
||||
|
||||
You can proxy an instance of user in the following way:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Autowired
|
||||
AuthorizationProxyFactory proxyFactory;
|
||||
|
||||
@Test
|
||||
void getEmailWhenProxiedThenAuthorizes() {
|
||||
User user = new User("name", "email");
|
||||
assertThat(user.getEmail()).isNotNull();
|
||||
User securedUser = proxyFactory.proxy(user);
|
||||
assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(securedUser::getEmail);
|
||||
}
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Autowired
|
||||
var proxyFactory:AuthorizationProxyFactory? = null
|
||||
|
||||
@Test
|
||||
fun getEmailWhenProxiedThenAuthorizes() {
|
||||
val user: User = User("name", "email")
|
||||
assertThat(user.getEmail()).isNotNull()
|
||||
val securedUser: User = proxyFactory.proxy(user)
|
||||
assertThatExceptionOfType(AccessDeniedException::class.java).isThrownBy(securedUser::getEmail)
|
||||
}
|
||||
----
|
||||
======
|
||||
|
||||
=== Manual Construction
|
||||
|
||||
You can also define your own instance if you need something different from the Spring Security default.
|
||||
|
||||
For example, if you define an `AuthorizationProxyFactory` instance like so:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
import static org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor.preAuthorize;
|
||||
|
||||
// ...
|
||||
|
||||
AuthorizationProxyFactory proxyFactory = new AuthorizationProxyFactory(preAuthorize());
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
import org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor.preAuthorize
|
||||
|
||||
// ...
|
||||
|
||||
val proxyFactory: AuthorizationProxyFactory = AuthorizationProxyFactory(preAuthorize())
|
||||
----
|
||||
======
|
||||
|
||||
Then you can wrap any instance of `User` as follows:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Test
|
||||
void getEmailWhenProxiedThenAuthorizes() {
|
||||
AuthorizationProxyFactory proxyFactory = new AuthorizationProxyFactory(preAuthorize());
|
||||
User user = new User("name", "email");
|
||||
assertThat(user.getEmail()).isNotNull();
|
||||
User securedUser = proxyFactory.proxy(user);
|
||||
assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(securedUser::getEmail);
|
||||
}
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Test
|
||||
fun getEmailWhenProxiedThenAuthorizes() {
|
||||
val proxyFactory: AuthorizationProxyFactory = AuthorizationProxyFactory(preAuthorize())
|
||||
val user: User = User("name", "email")
|
||||
assertThat(user.getEmail()).isNotNull()
|
||||
val securedUser: User = proxyFactory.proxy(user)
|
||||
assertThatExceptionOfType(AccessDeniedException::class.java).isThrownBy(securedUser::getEmail)
|
||||
}
|
||||
----
|
||||
======
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
This feature does not yet support Spring AOT
|
||||
====
|
||||
|
||||
=== Proxying Collections
|
||||
|
||||
`AuthorizationProxyFactory` supports Java collections, streams, arrays, optionals, and iterators by proxying the element type and maps by proxying the value type.
|
||||
|
||||
This means that when proxying a `List` of objects, the following also works:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Test
|
||||
void getEmailWhenProxiedThenAuthorizes() {
|
||||
AuthorizationProxyFactory proxyFactory = new AuthorizationProxyFactory(preAuthorize());
|
||||
List<User> users = List.of(ada, albert, marie);
|
||||
List<User> securedUsers = proxyFactory.proxy(users);
|
||||
securedUsers.forEach((securedUser) ->
|
||||
assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(securedUser::getEmail));
|
||||
}
|
||||
----
|
||||
======
|
||||
|
||||
=== Proxying Classes
|
||||
|
||||
In limited circumstances, it may be valuable to proxy a `Class` itself, and `AuthorizationProxyFactory` also supports this.
|
||||
This is roughly the equivalent of calling `ProxyFactory#getProxyClass` in Spring Framework's support for creating proxies.
|
||||
|
||||
One place where this is handy is when you need to construct the proxy class ahead-of-time, like with Spring AOT.
|
||||
|
||||
=== Support for All Method Security Annotations
|
||||
|
||||
`AuthorizationProxyFactory` supports whichever method security annotations are enabled in your application.
|
||||
It is based off of whatever `AuthorizationAdvisor` classes are published as a bean.
|
||||
|
||||
Since `@EnableMethodSecurity` publishes `@PreAuthorize`, `@PostAuthorize`, `@PreFilter`, and `@PostFilter` advisors by default, you will typically need to do nothing to activate the ability.
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
SpEL expressions that use `returnObject` or `filterObject` sit behind the proxy and so have full access to the object.
|
||||
====
|
||||
|
||||
[#custom_advice]
|
||||
=== Custom Advice
|
||||
|
||||
If you have security advice that you also want applied, you can publish your own `AuthorizationAdvisor` like so:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@EnableMethodSecurity
|
||||
class SecurityConfig {
|
||||
@Bean
|
||||
static AuthorizationAdvisor myAuthorizationAdvisor() {
|
||||
return new AuthorizationAdvisor();
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@EnableMethodSecurity
|
||||
internal class SecurityConfig {
|
||||
@Bean
|
||||
fun myAuthorizationAdvisor(): AuthorizationAdvisor {
|
||||
return AuthorizationAdvisor()
|
||||
}
|
||||
]
|
||||
----
|
||||
======
|
||||
|
||||
And Spring Security will add that advisor into the set of advice that `AuthorizationProxyFactory` adds when proxying an object.
|
||||
|
||||
=== Working with Jackson
|
||||
|
||||
One powerful use of this feature is to return a secured value from a controller like so:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@RestController
|
||||
public class UserController {
|
||||
@Autowired
|
||||
AuthorizationProxyFactory proxyFactory;
|
||||
|
||||
@GetMapping
|
||||
User currentUser(@AuthenticationPrincipal User user) {
|
||||
return this.proxyFactory.proxy(user);
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@RestController
|
||||
class UserController {
|
||||
@Autowired
|
||||
var proxyFactory: AuthorizationProxyFactory? = null
|
||||
|
||||
@GetMapping
|
||||
fun currentUser(@AuthenticationPrincipal user:User?): User {
|
||||
return proxyFactory.proxy(user)
|
||||
}
|
||||
}
|
||||
----
|
||||
======
|
||||
|
||||
If you are using Jackson, though, this may result in a serialization error like the following:
|
||||
|
||||
[source,bash]
|
||||
====
|
||||
com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Direct self-reference leading to cycle
|
||||
====
|
||||
|
||||
This is due to how Jackson works with CGLIB proxies.
|
||||
To address this, add the following annotation to the top of the `User` class:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@JsonSerialize(as = User.class)
|
||||
public class User {
|
||||
|
||||
}
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@JsonSerialize(`as` = User::class)
|
||||
class User
|
||||
----
|
||||
======
|
||||
|
||||
Finally, you will need to publish a <<custom_advice, custom interceptor>> to catch the `AccessDeniedException` thrown for each field, which you can do like so:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Component
|
||||
public class AccessDeniedExceptionInterceptor implements AuthorizationAdvisor {
|
||||
private final AuthorizationAdvisor advisor = AuthorizationManagerBeforeMethodInterceptor.preAuthorize();
|
||||
|
||||
@Override
|
||||
public Object invoke(MethodInvocation invocation) throws Throwable {
|
||||
try {
|
||||
return invocation.proceed();
|
||||
} catch (AccessDeniedException ex) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Pointcut getPointcut() {
|
||||
return this.advisor.getPointcut();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Advice getAdvice() {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOrder() {
|
||||
return this.advisor.getOrder() - 1;
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Component
|
||||
class AccessDeniedExceptionInterceptor: AuthorizationAdvisor {
|
||||
var advisor: AuthorizationAdvisor = AuthorizationManagerBeforeMethodInterceptor.preAuthorize()
|
||||
|
||||
@Throws(Throwable::class)
|
||||
fun invoke(invocation: MethodInvocation): Any? {
|
||||
return try {
|
||||
invocation.proceed()
|
||||
} catch (ex:AccessDeniedException) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
val pointcut: Pointcut
|
||||
get() = advisor.getPointcut()
|
||||
|
||||
val advice: Advice
|
||||
get() = this
|
||||
|
||||
val order: Int
|
||||
get() = advisor.getOrder() - 1
|
||||
}
|
||||
----
|
||||
======
|
||||
|
||||
Then, you'll see a different JSON serialization based on the authorization level of the user.
|
||||
If they don't have the `user:read` authority, then they'll see:
|
||||
|
||||
[source,json]
|
||||
----
|
||||
{
|
||||
"name" : "name",
|
||||
"email" : null
|
||||
}
|
||||
----
|
||||
|
||||
And if they do have that authority, they'll see:
|
||||
|
||||
[source,json]
|
||||
----
|
||||
{
|
||||
"name" : "name",
|
||||
"email" : "email"
|
||||
}
|
||||
----
|
||||
|
||||
[TIP]
|
||||
====
|
||||
You can also add the Spring Boot property `spring.jackson.default-property-inclusion=non_null` to exclude the null value, if you also don't want to reveal the JSON key to an unauthorized user.
|
||||
====
|
||||
|
||||
[[migration-enableglobalmethodsecurity]]
|
||||
== Migrating from `@EnableGlobalMethodSecurity`
|
||||
|
||||
|
||||
@@ -8,6 +8,10 @@ Below are the highlights of the release.
|
||||
|
||||
- https://spring.io/blog/2024/01/19/spring-security-6-3-adds-passive-jdk-serialization-deserialization-for[blog post] - Added Passive JDK Serialization/Deserialization for Seamless Upgrades
|
||||
|
||||
== Authorization
|
||||
|
||||
- https://github.com/spring-projects/spring-security/issues/14596[gh-14596] - xref:servlet/authorization/method-security.adoc[docs] - Add Programmatic Proxy Support for Method Security
|
||||
|
||||
== Configuration
|
||||
|
||||
- https://github.com/spring-projects/spring-security/issues/6192[gh-6192] - xref:reactive/authentication/concurrent-sessions-control.adoc[(docs)] - Add Concurrent Sessions Control on WebFlux
|
||||
|
||||
Reference in New Issue
Block a user