diff --git a/core/src/main/java/org/springframework/security/authorization/method/AuthorizationAdvisorProxyFactory.java b/core/src/main/java/org/springframework/security/authorization/method/AuthorizationAdvisorProxyFactory.java index 8ab87a8c4dd..ed24562a005 100644 --- a/core/src/main/java/org/springframework/security/authorization/method/AuthorizationAdvisorProxyFactory.java +++ b/core/src/main/java/org/springframework/security/authorization/method/AuthorizationAdvisorProxyFactory.java @@ -172,6 +172,7 @@ public Object proxy(Object target) { factory.addAdvisors(advisor); } factory.setProxyTargetClass(!Modifier.isFinal(target.getClass().getModifiers())); + factory.addInterface(AuthorizationProxy.class); return factory.getProxy(); } @@ -357,6 +358,7 @@ public Object visit(AuthorizationAdvisorProxyFactory proxyFactory, Object object ProxyFactory factory = new ProxyFactory(); factory.setTargetClass(targetClass); factory.setInterfaces(ClassUtils.getAllInterfacesForClass(targetClass)); + factory.addInterface(AuthorizationProxy.class); factory.setProxyTargetClass(!Modifier.isFinal(targetClass.getModifiers())); for (Advisor advisor : proxyFactory) { factory.addAdvisors(advisor); diff --git a/core/src/main/java/org/springframework/security/authorization/method/AuthorizationProxy.java b/core/src/main/java/org/springframework/security/authorization/method/AuthorizationProxy.java new file mode 100644 index 00000000000..05107dc8a56 --- /dev/null +++ b/core/src/main/java/org/springframework/security/authorization/method/AuthorizationProxy.java @@ -0,0 +1,29 @@ +/* + * Copyright 2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.authorization.method; + +/** + * Marker interface implemented by Authorization proxies. Used to detect whether objects + * are AuthorizeReturnObject proxies. + * + * @author DingHao + * @since 6.4 + * @see org.springframework.security.authorization.method.AuthorizeReturnObject + */ +public interface AuthorizationProxy { + +} diff --git a/core/src/main/java/org/springframework/security/jackson2/AuthorizationProxySerializer.java b/core/src/main/java/org/springframework/security/jackson2/AuthorizationProxySerializer.java new file mode 100644 index 00000000000..25690d3d986 --- /dev/null +++ b/core/src/main/java/org/springframework/security/jackson2/AuthorizationProxySerializer.java @@ -0,0 +1,56 @@ +/* + * Copyright 2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.jackson2; + +import java.io.IOException; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.jsontype.TypeSerializer; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; + +import org.springframework.aop.framework.AopProxyUtils; +import org.springframework.security.authorization.method.AuthorizationProxy; +import org.springframework.security.authorization.method.AuthorizeReturnObject; + +/** + * Serialize AuthorizationProxy objects generated by {@link AuthorizeReturnObject} + * + * @author DingHao + * @since 6.4 + */ +public final class AuthorizationProxySerializer extends StdSerializer { + + public AuthorizationProxySerializer() { + super(AuthorizationProxy.class); + } + + @Override + public void serialize(AuthorizationProxy value, JsonGenerator gen, SerializerProvider serializers) + throws IOException { + JsonSerializer serializer = serializers.findValueSerializer(AopProxyUtils.ultimateTargetClass(value)); + serializer.serialize(value, gen, serializers); + } + + @Override + public void serializeWithType(AuthorizationProxy value, JsonGenerator gen, SerializerProvider serializers, + TypeSerializer typeSer) throws IOException { + serialize(value, gen, serializers); + } + +} diff --git a/core/src/main/java/org/springframework/security/jackson2/CoreJackson2Module.java b/core/src/main/java/org/springframework/security/jackson2/CoreJackson2Module.java index 3a91f0c1369..0e4ef5b14fd 100644 --- a/core/src/main/java/org/springframework/security/jackson2/CoreJackson2Module.java +++ b/core/src/main/java/org/springframework/security/jackson2/CoreJackson2Module.java @@ -52,10 +52,12 @@ public class CoreJackson2Module extends SimpleModule { public CoreJackson2Module() { super(CoreJackson2Module.class.getName(), new Version(1, 0, 0, null, null, null)); + addSerializer(new AuthorizationProxySerializer()); } @Override public void setupModule(SetupContext context) { + super.setupModule(context); SecurityJackson2Modules.enableDefaultTyping(context.getOwner()); context.setMixInAnnotations(AnonymousAuthenticationToken.class, AnonymousAuthenticationTokenMixin.class); context.setMixInAnnotations(RememberMeAuthenticationToken.class, RememberMeAuthenticationTokenMixin.class); diff --git a/core/src/test/java/org/springframework/security/authorization/AuthorizationAdvisorProxyFactoryTests.java b/core/src/test/java/org/springframework/security/authorization/AuthorizationAdvisorProxyFactoryTests.java index 15389c2ec2c..680bef61d4c 100644 --- a/core/src/test/java/org/springframework/security/authorization/AuthorizationAdvisorProxyFactoryTests.java +++ b/core/src/test/java/org/springframework/security/authorization/AuthorizationAdvisorProxyFactoryTests.java @@ -34,6 +34,10 @@ import java.util.function.Supplier; import java.util.stream.Stream; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.exc.InvalidDefinitionException; import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.Test; @@ -46,6 +50,7 @@ import org.springframework.security.authorization.method.AuthorizationAdvisorProxyFactory.TargetVisitor; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.jackson2.SecurityJackson2Modules; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; @@ -336,6 +341,34 @@ public void setTargetVisitorIgnoreValueTypesThenIgnores() { assertThat(factory.proxy(35)).isEqualTo(35); } + @Test + public void serializeAuthorizationProxyObjectWhenProvideJsonSerialize() throws JsonProcessingException { + SecurityContextHolder.getContext().setAuthentication(this.admin); + AuthorizationAdvisorProxyFactory factory = AuthorizationAdvisorProxyFactory.withDefaults(); + JsonSerializeUser user1 = new JsonSerializeUser("used JsonSerialize annotation"); + NoJsonSerializeUser user2 = new NoJsonSerializeUser("unused JsonSerialize annotation"); + + ObjectMapper mapper = new ObjectMapper(); + mapper.registerModules(SecurityJackson2Modules.getModules(getClass().getClassLoader())); + + assertThat(mapper.writeValueAsString(proxy(factory, user1))).doesNotContain("description"); + assertThat(mapper.writeValueAsString(proxy(factory, user2))).contains("description"); + } + + @Test + public void serializeAuthorizationProxyObject() throws JsonProcessingException { + SecurityContextHolder.getContext().setAuthentication(this.admin); + AuthorizationAdvisorProxyFactory factory = AuthorizationAdvisorProxyFactory.withDefaults(); + User user = proxy(factory, this.alan); + ObjectMapper mapper = new ObjectMapper(); + assertThatExceptionOfType(InvalidDefinitionException.class).isThrownBy(() -> mapper.writeValueAsString(user)); + + ObjectMapper objectMapper = new ObjectMapper(); + objectMapper.registerModules(SecurityJackson2Modules.getModules(getClass().getClassLoader())); + String actual = objectMapper.writeValueAsString(user); + assertThat(actual).isInstanceOf(String.class); + } + private Authentication authenticated(String user, String... authorities) { return TestAuthentication.authenticated(TestAuthentication.withUsername(user).authorities(authorities).build()); } @@ -363,6 +396,37 @@ interface Identifiable { } + @JsonSerialize(as = User.class) + public static class JsonSerializeUser extends User { + + private final String description; + + JsonSerializeUser(String description) { + super("alan", "alan", "turing"); + this.description = description; + } + + public String getDescription() { + return this.description; + } + + } + + public static class NoJsonSerializeUser extends User { + + private final String description; + + NoJsonSerializeUser(String description) { + super("alan", "alan", "turing"); + this.description = description; + } + + public String getDescription() { + return this.description; + } + + } + public static class User implements Identifiable, Comparable { private final String id; diff --git a/docs/modules/ROOT/pages/servlet/authorization/method-security.adoc b/docs/modules/ROOT/pages/servlet/authorization/method-security.adoc index 8eeddf5efc7..25de5c99e86 100644 --- a/docs/modules/ROOT/pages/servlet/authorization/method-security.adoc +++ b/docs/modules/ROOT/pages/servlet/authorization/method-security.adoc @@ -2235,7 +2235,17 @@ com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Direct self-refer ==== This is due to how Jackson works with CGLIB proxies. -To address this, add the following annotation to the top of the `User` class: +To address this, register the SecurityJackson2Modules.getModules(ClassLoader) with ObjectMapper + +[source,java] +---- +ObjectMapper mapper = new ObjectMapper(); +ClassLoader loader = getClass().getClassLoader(); +List modules = SecurityJackson2Modules.getModules(loader); +mapper.registerModules(modules); +---- + +If you are using Spring Boot, you can also publish module bean and add `AuthorizationProxySerializer`: [tabs] ====== @@ -2243,9 +2253,11 @@ Java:: + [source,java,role="primary"] ---- -@JsonSerialize(as = User.class) -public class User { - +@Bean +SimpleModule authorizationProxyModule() { + SimpleModule simpleModule = new SimpleModule(); + simpleModule.addSerializer(new AuthorizationProxySerializer()); + return simpleModule; } ---- @@ -2253,11 +2265,16 @@ Kotlin:: + [source,kotlin,role="secondary"] ---- -@JsonSerialize(`as` = User::class) -class User +@Bean +fun authorizationProxyModule(): SimpleModule { + val simpleModule = SimpleModule() + simpleModule.addSerializer(AuthorizationProxySerializer()) + return simpleModule +} ---- ====== + Finally, you will need to publish a <> to catch the `AccessDeniedException` thrown for each field, which you can do like so: [tabs]