Skip to content

Commit e625dff

Browse files
committed
Improve serialize of proxy objects generated by AuthorizeReturnObject
Issue gh-15561
1 parent 84fc5a7 commit e625dff

File tree

6 files changed

+176
-6
lines changed

6 files changed

+176
-6
lines changed

core/src/main/java/org/springframework/security/authorization/method/AuthorizationAdvisorProxyFactory.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,7 @@ public Object proxy(Object target) {
172172
factory.addAdvisors(advisor);
173173
}
174174
factory.setProxyTargetClass(!Modifier.isFinal(target.getClass().getModifiers()));
175+
factory.addInterface(AuthorizationProxy.class);
175176
return factory.getProxy();
176177
}
177178

@@ -357,6 +358,7 @@ public Object visit(AuthorizationAdvisorProxyFactory proxyFactory, Object object
357358
ProxyFactory factory = new ProxyFactory();
358359
factory.setTargetClass(targetClass);
359360
factory.setInterfaces(ClassUtils.getAllInterfacesForClass(targetClass));
361+
factory.addInterface(AuthorizationProxy.class);
360362
factory.setProxyTargetClass(!Modifier.isFinal(targetClass.getModifiers()));
361363
for (Advisor advisor : proxyFactory) {
362364
factory.addAdvisors(advisor);
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*
2+
* Copyright 2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.security.authorization.method;
18+
19+
/**
20+
* Marker interface implemented by Authorization proxies. Used to detect whether objects
21+
* are AuthorizeReturnObject proxies.
22+
*
23+
* @author DingHao
24+
* @since 6.4
25+
* @see org.springframework.security.authorization.method.AuthorizeReturnObject
26+
*/
27+
public interface AuthorizationProxy {
28+
29+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/*
2+
* Copyright 2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.security.jackson2;
18+
19+
import java.io.IOException;
20+
21+
import com.fasterxml.jackson.core.JsonGenerator;
22+
import com.fasterxml.jackson.databind.JsonSerializer;
23+
import com.fasterxml.jackson.databind.SerializerProvider;
24+
import com.fasterxml.jackson.databind.jsontype.TypeSerializer;
25+
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
26+
27+
import org.springframework.aop.framework.AopProxyUtils;
28+
import org.springframework.security.authorization.method.AuthorizationProxy;
29+
import org.springframework.security.authorization.method.AuthorizeReturnObject;
30+
31+
/**
32+
* Serialize AuthorizationProxy objects generated by {@link AuthorizeReturnObject}
33+
*
34+
* @author DingHao
35+
* @since 6.4
36+
*/
37+
public final class AuthorizationProxySerializer extends StdSerializer<AuthorizationProxy> {
38+
39+
public AuthorizationProxySerializer() {
40+
super(AuthorizationProxy.class);
41+
}
42+
43+
@Override
44+
public void serialize(AuthorizationProxy value, JsonGenerator gen, SerializerProvider serializers)
45+
throws IOException {
46+
JsonSerializer<Object> serializer = serializers.findValueSerializer(AopProxyUtils.ultimateTargetClass(value));
47+
serializer.serialize(value, gen, serializers);
48+
}
49+
50+
@Override
51+
public void serializeWithType(AuthorizationProxy value, JsonGenerator gen, SerializerProvider serializers,
52+
TypeSerializer typeSer) throws IOException {
53+
serialize(value, gen, serializers);
54+
}
55+
56+
}

core/src/main/java/org/springframework/security/jackson2/CoreJackson2Module.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,10 +52,12 @@ public class CoreJackson2Module extends SimpleModule {
5252

5353
public CoreJackson2Module() {
5454
super(CoreJackson2Module.class.getName(), new Version(1, 0, 0, null, null, null));
55+
addSerializer(new AuthorizationProxySerializer());
5556
}
5657

5758
@Override
5859
public void setupModule(SetupContext context) {
60+
super.setupModule(context);
5961
SecurityJackson2Modules.enableDefaultTyping(context.getOwner());
6062
context.setMixInAnnotations(AnonymousAuthenticationToken.class, AnonymousAuthenticationTokenMixin.class);
6163
context.setMixInAnnotations(RememberMeAuthenticationToken.class, RememberMeAuthenticationTokenMixin.class);

core/src/test/java/org/springframework/security/authorization/AuthorizationAdvisorProxyFactoryTests.java

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@
3434
import java.util.function.Supplier;
3535
import java.util.stream.Stream;
3636

37+
import com.fasterxml.jackson.core.JsonProcessingException;
38+
import com.fasterxml.jackson.databind.ObjectMapper;
39+
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
40+
import com.fasterxml.jackson.databind.exc.InvalidDefinitionException;
3741
import org.jetbrains.annotations.NotNull;
3842
import org.junit.jupiter.api.Test;
3943

@@ -46,6 +50,7 @@
4650
import org.springframework.security.authorization.method.AuthorizationAdvisorProxyFactory.TargetVisitor;
4751
import org.springframework.security.core.Authentication;
4852
import org.springframework.security.core.context.SecurityContextHolder;
53+
import org.springframework.security.jackson2.SecurityJackson2Modules;
4954

5055
import static org.assertj.core.api.Assertions.assertThat;
5156
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
@@ -336,6 +341,34 @@ public void setTargetVisitorIgnoreValueTypesThenIgnores() {
336341
assertThat(factory.proxy(35)).isEqualTo(35);
337342
}
338343

344+
@Test
345+
public void serializeAuthorizationProxyObjectWhenProvideJsonSerialize() throws JsonProcessingException {
346+
SecurityContextHolder.getContext().setAuthentication(this.admin);
347+
AuthorizationAdvisorProxyFactory factory = AuthorizationAdvisorProxyFactory.withDefaults();
348+
JsonSerializeUser user1 = new JsonSerializeUser("used JsonSerialize annotation");
349+
NoJsonSerializeUser user2 = new NoJsonSerializeUser("unused JsonSerialize annotation");
350+
351+
ObjectMapper mapper = new ObjectMapper();
352+
mapper.registerModules(SecurityJackson2Modules.getModules(getClass().getClassLoader()));
353+
354+
assertThat(mapper.writeValueAsString(proxy(factory, user1))).doesNotContain("description");
355+
assertThat(mapper.writeValueAsString(proxy(factory, user2))).contains("description");
356+
}
357+
358+
@Test
359+
public void serializeAuthorizationProxyObject() throws JsonProcessingException {
360+
SecurityContextHolder.getContext().setAuthentication(this.admin);
361+
AuthorizationAdvisorProxyFactory factory = AuthorizationAdvisorProxyFactory.withDefaults();
362+
User user = proxy(factory, this.alan);
363+
ObjectMapper mapper = new ObjectMapper();
364+
assertThatExceptionOfType(InvalidDefinitionException.class).isThrownBy(() -> mapper.writeValueAsString(user));
365+
366+
ObjectMapper objectMapper = new ObjectMapper();
367+
objectMapper.registerModules(SecurityJackson2Modules.getModules(getClass().getClassLoader()));
368+
String actual = objectMapper.writeValueAsString(user);
369+
assertThat(actual).isInstanceOf(String.class);
370+
}
371+
339372
private Authentication authenticated(String user, String... authorities) {
340373
return TestAuthentication.authenticated(TestAuthentication.withUsername(user).authorities(authorities).build());
341374
}
@@ -363,6 +396,37 @@ interface Identifiable {
363396

364397
}
365398

399+
@JsonSerialize(as = User.class)
400+
public static class JsonSerializeUser extends User {
401+
402+
private final String description;
403+
404+
JsonSerializeUser(String description) {
405+
super("alan", "alan", "turing");
406+
this.description = description;
407+
}
408+
409+
public String getDescription() {
410+
return this.description;
411+
}
412+
413+
}
414+
415+
public static class NoJsonSerializeUser extends User {
416+
417+
private final String description;
418+
419+
NoJsonSerializeUser(String description) {
420+
super("alan", "alan", "turing");
421+
this.description = description;
422+
}
423+
424+
public String getDescription() {
425+
return this.description;
426+
}
427+
428+
}
429+
366430
public static class User implements Identifiable, Comparable<User> {
367431

368432
private final String id;

docs/modules/ROOT/pages/servlet/authorization/method-security.adoc

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2235,29 +2235,46 @@ com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Direct self-refer
22352235
====
22362236

22372237
This is due to how Jackson works with CGLIB proxies.
2238-
To address this, add the following annotation to the top of the `User` class:
2238+
To address this, register the SecurityJackson2Modules.getModules(ClassLoader) with ObjectMapper
2239+
2240+
[source,java]
2241+
----
2242+
ObjectMapper mapper = new ObjectMapper();
2243+
ClassLoader loader = getClass().getClassLoader();
2244+
List<Module> modules = SecurityJackson2Modules.getModules(loader);
2245+
mapper.registerModules(modules);
2246+
----
2247+
2248+
If you are using Spring Boot, you can also publish module bean and add `AuthorizationProxySerializer`:
22392249

22402250
[tabs]
22412251
======
22422252
Java::
22432253
+
22442254
[source,java,role="primary"]
22452255
----
2246-
@JsonSerialize(as = User.class)
2247-
public class User {
2248-
2256+
@Bean
2257+
SimpleModule authorizationProxyModule() {
2258+
SimpleModule simpleModule = new SimpleModule();
2259+
simpleModule.addSerializer(new AuthorizationProxySerializer());
2260+
return simpleModule;
22492261
}
22502262
----
22512263
22522264
Kotlin::
22532265
+
22542266
[source,kotlin,role="secondary"]
22552267
----
2256-
@JsonSerialize(`as` = User::class)
2257-
class User
2268+
@Bean
2269+
fun authorizationProxyModule(): SimpleModule {
2270+
val simpleModule = SimpleModule()
2271+
simpleModule.addSerializer(AuthorizationProxySerializer())
2272+
return simpleModule
2273+
}
22582274
----
22592275
======
22602276

2277+
22612278
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:
22622279

22632280
[tabs]

0 commit comments

Comments
 (0)