序列化时未使用 Jackson PropertyFilter XML

Jackson PropertyFilter not used when serializing XML

我已经创建了一个 Jackson PropertyFilter 并将其注册到 XmlMapper,但它不用于过滤从 Spring @RestController 返回的属性。

我创建并使用了 Jackson PropertyFilter 来过滤 JSON ObjectMapper 为 Spring @RestController 生成的结果。我正在尝试为 XML 启用相同的功能,但无法正常工作。

我试过直接在 XmlMapper 实例上并通过 Jackson2ObjectMapperBuilder 注册过滤器。在这两种情况下都不会调用它。

我已逐步完成代码,XmlBeanSerializer 似乎引用了过滤器,但过滤器从未被调用。

我创建了一个 LogAllPropertyFilter class 来记录过滤器是否被调用并且没有生成任何日志消息。

public class LogAllPropertyFilter extends SimpleBeanPropertyFilter implements PropertyFilter {
private Logger logger = LoggerFactory.getLogger(getClass());

@Override
public void serializeAsField(Object pojo, JsonGenerator gen, SerializerProvider prov, PropertyWriter writer)
        throws Exception {
    logger.info(" *** *** serializeAsField {}.{}", 
            pojo.getClass().getSimpleName(),
            writer.getName());
    super.serializeAsField(pojo, gen, prov, writer);
}

@Override
public void serializeAsElement(Object elementValue, JsonGenerator gen, SerializerProvider prov,
        PropertyWriter writer) throws Exception {
    logger.info(" *** *** serializeAsElement {}.{}", 
            elementValue.getClass().getSimpleName(),
            writer.getName());
    super.serializeAsElement(elementValue, gen, prov, writer);
}

@SuppressWarnings("deprecation")
@Override
public void depositSchemaProperty(PropertyWriter writer, ObjectNode propertiesNode, SerializerProvider provider)
        throws JsonMappingException {
    logger.info(" *** *** depositSchemaProperty {} (deprecated)",
            writer.getName());
    super.depositSchemaProperty(writer, propertiesNode, provider);
}

@Override
public void depositSchemaProperty(PropertyWriter writer, JsonObjectFormatVisitor objectVisitor,
        SerializerProvider provider) throws JsonMappingException {
    logger.info(" *** *** depositSchemaProperty {} (deprecated)",
            writer.getName());
    super.depositSchemaProperty(writer, objectVisitor, provider);
}
}

我正在这样创建和注册 PropertyFilter:

<bean id="logAllFilter" class="calpers.eai.config.auth.jacksonpropertyfilter.LogAllPropertyFilter" />

<bean id="logAllFilterProvider"
    class="com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider">
    <constructor-arg>
        <map>
            <entry key="logAllFilter"
                value-ref="logAllFilter" />
        </map>
    </constructor-arg>
</bean>

<bean id="xmlObjectMapper"
    class="com.fasterxml.jackson.dataformat.xml.XmlMapper" />

<bean  class="org.springframework.beans.factory.config.MethodInvokingFactoryBean">
    <property name="targetObject" ref="xmlObjectMapper" />
    <property name="targetMethod" value="setFilterProvider" />
    <property name="arguments" ref="logAllFilterProvider" />
</bean>

<bean class="org.springframework.beans.factory.config.MethodInvokingFactoryBean">
    <property name="targetObject" ref="xmlObjectMapper" />
    <property name="targetMethod" value="disable" />
    <property name="arguments" value="WRITE_DATES_AS_TIMESTAMPS" />
</bean>

<!-- indent json - disable this in prod -->
<bean class="org.springframework.beans.factory.config.MethodInvokingFactoryBean">
    <property name="targetObject" ref="xmlObjectMapper" />
    <property name="targetMethod" value="enable" />
    <property name="arguments" value="INDENT_OUTPUT" />
</bean>

<bean id="xmlConverter" class="org.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConverter">
    <constructor-arg ref="xmlObjectMapper" />
</bean>


<mvc:annotation-driven>
    <mvc:message-converters>
        <!-- json works -->
        <ref bean="jsonConverter" />

                    <!-- xml doesn't work -->
        <ref bean="xmlConverter" />  
    </mvc:message-converters>
</mvc:annotation-driven>

XML 输出 缩进的,所以我知道 XmlMapper 实例正在被拾取。但是,从不调用 PropertyFilter 方法。我被难住了。

除非 class 以某种方式链接到过滤器,否则不会应用过滤器。通常使用注释,但在这种情况下,我需要过滤所有对象的属性,而不管它们的出处,因此我们将在所有 [=20= 的公共 base-class 上使用 mix-in ] 对象:

<bean class="org.springframework.beans.factory.config.MethodInvokingFactoryBean">
    <property name="targetObject" ref="xmlObjectMapper" />
    <property name="targetMethod" value="addMixIn" />
    <property name="arguments">
        <list>
            <value type="java.lang.Class">java.lang.Object</value>
            <value type="java.lang.Class">eai.config.auth.jacksonpropertyfilter.SecurityRoleAwareJacksonMixIn</value>
        </list>
    </property>
</bean>

将此添加到配置后,我的过滤器在每个 XML 对象上 运行 从我的 Spring MVC @RestController 提供。

这是一个方便的过滤器,可根据 Spring 安全性中的安全角色控制对 class 属性的访问。享受吧!

package eai.config.auth.jacksonpropertyfilter;

import java.io.IOException;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.ldap.userdetails.LdapAuthority;

import com.fasterxml.jackson.databind.introspect.AnnotatedMember;
import com.fasterxml.jackson.databind.ser.BeanPropertyWriter;
import com.fasterxml.jackson.databind.ser.PropertyWriter;
import com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter;

import eai.config.auth.jacksonpropertyfilter.xml.SecurityRole;
import eai.config.refreshable.Refreshable;

/**
 * Filters based on the union of properties a principal can view. In JsonViewConfiguration a user
 * with multiple views will be assigned the highest ranked view and only see the properties that are
 * included in that view. With SecurityRoleAwareJacksonFilterImpl, the user will see any property they
 * have access to based on ALL the groups they are members of. Therefore, it is the union of
 * all @JsonView's.
 *
 * This class should be instantiated as a Spring Bean, probably in the XML config to maximize
 * configuration options that avoid a re-compile.
 *
 * @author TPerry2
 */
public class SecurityRoleAwareJacksonFilterImpl extends SimpleBeanPropertyFilter
        implements SecurityRoleAwareJacksonFilter, Refreshable {
    private final Logger logger = LoggerFactory.getLogger(
            SecurityRoleAwareJacksonFilterImpl.class);
    Map<Class<?>, Map<String, Collection<SecurityRole>>> classPropertyRoles = 
            new HashMap<>();
    List<SecurityRoleToClassPropertyReader> securityRoleToClassPropertyReaders = 
            new ArrayList<>();

    private ConcurrentHashMap<String, String> knownUserNoRole = 
            new ConcurrentHashMap<>();
    private ConcurrentHashMap<Class<?>, Set<String>> classPropsWithNoAccess = 
            new ConcurrentHashMap<>();


    /**
     * Add mapping for what class properties a LDAP role can view.
     *
     * @param securityRoleToClassPropertyXmlReaders to obtain mapping data from.
     * @throws ClassNotFoundException if the java class can not be found.
     * @throws IOException when security role to class property XML files can't be read.
     */
    @Override
    @Autowired
    public void setSecurityRoleToClassPropertyReaders(
            List<SecurityRoleToClassPropertyReader> securityRoleToClassPropertyReaders)
            throws ClassNotFoundException, IOException {
        this.securityRoleToClassPropertyReaders = securityRoleToClassPropertyReaders;
        loadClassPropertyRoles();
    }


    /**
     * Method called to determine whether property will be included
     * (if 'true' returned) or filtered out (if 'false' returned)
     */
    protected boolean include(BeanPropertyWriter writer) {
        AnnotatedMember memberToSerialize = writer.getMember();
        if (memberToSerialize == null) {
            logger.warn("Could not get member to serialize for writer {}",
                    writer.getClass().getName());
            return false;
        }
        final Class<?> clazz = memberToSerialize.getDeclaringClass();
        return include(clazz, writer.getName());
    }

    /**
     * Method called to determine whether property will be included
     * (if 'true' returned) or filtered out (if 'false' returned)
     */
    protected boolean include(PropertyWriter writer) {
        AnnotatedMember memberToSerialize = writer.getMember();
        if (memberToSerialize == null) {
            logger.warn("Could not get member to serialize for writer {}",
                    writer.getClass().getName());
            return false;
        }
        final Class<?> clazz = memberToSerialize.getDeclaringClass();
        return include(clazz, writer.getName());
    }

    protected boolean include(
            Class<?> clazz,
            String propertyName) {
        logger.info("Checking {}.{}", clazz.getSimpleName(), propertyName);
        final Map<String, Collection<SecurityRole>> propertyLdapRoleMap = 
                classPropertyRoles.get(clazz);
        if (propertyLdapRoleMap != null) {
            final Collection<SecurityRole> securityRoles = 
                    propertyLdapRoleMap.get(propertyName);
            if (securityRoles != null && securityRoles.size() > 0) {
                Authentication auth = getAuthentication();

                if (isAuthorized(getGrantedAuthorities(auth), securityRoles)) {
                    logger.info("allowing {}.{}", clazz.getSimpleName(), propertyName);
                    return true;
                } else {
                    logUserNoRole(clazz, propertyName, securityRoles, auth);
                }
            } else {
                logPropertyWithNoAccess(clazz, propertyName);
            }
        } else {
            logPropertyWithNoAccess(clazz, "-- all properties --");
        }
        return false;
    }

    private void logUserNoRole(
            Class<?> clazz, 
            String propertyName, 
            Collection<SecurityRole> allowedRoles,
            Authentication auth) {
        if (!logger.isDebugEnabled()) {
            return;
        }

        String username = (auth == null ? "anonymous" : auth.getName());

        final String knownUserNoRoleString = "" 
                + clazz.getName() + "." + propertyName + "." 
                + username;

        boolean known = knownUserNoRole.containsKey(knownUserNoRoleString);
        if (!known) {           
            knownUserNoRole.put(knownUserNoRoleString, "");
            logger.debug("User {} does not have valid role for {}.{}. "
                    + "Requires one of {}", username, clazz.getName(), 
                    propertyName, allowedRoles);
        }
    }

    private void logPropertyWithNoAccess(Class<?> clazz, String propertyName) {
        Set<String> knownPropsWithNoAccess = classPropsWithNoAccess.get(clazz);

        if (knownPropsWithNoAccess == null) {
            logger.warn("No roles enable access to {}.{}", 
                    clazz.getSimpleName(), propertyName);
            knownPropsWithNoAccess = new HashSet<>();           
            classPropsWithNoAccess.put(clazz, knownPropsWithNoAccess);          
        }

        boolean wasAdded = false;
        synchronized (knownPropsWithNoAccess) {         
            wasAdded = knownPropsWithNoAccess.add(propertyName);
        }

        if (wasAdded) {
            logger.warn("No roles enable access to {}.{}", 
                    clazz.getSimpleName(), propertyName);           
        }
    }

    private boolean isAuthorized(
            Collection<? extends GrantedAuthority> grantedAuths,
            Collection<SecurityRole> securityRoles) {
        try {
            if (grantedAuths == null) {
                return false;
            }

            for (GrantedAuthority grantedAuth : grantedAuths) {
                if (grantedAuth instanceof LdapAuthority) {
                    LdapAuthority ldapAuth = (LdapAuthority) grantedAuth;


                    for (SecurityRole secRole : securityRoles) {
                        if (secRole.distinguishedNameIsAuthorized(
                        ldapAuth.getDn())) {
                            return true;
                        }

                        if (secRole.displayNameIsAuthorized(
                        ldapAuth.getAuthority())) {
                            return true;
                        }
                    }                   
                } else  {
                    for (SecurityRole secRole : securityRoles) {
                        if (secRole.displayNameIsAuthorized(
                        grantedAuth.getAuthority())) {
                            return true;
                        }
                    }
                }

            }

            return false;
        } catch (NullPointerException npe) {
            logger.error("FIXME", npe);
            return false;
        }
    }

    private Collection<? extends GrantedAuthority> getGrantedAuthorities(
            Authentication auth) {
        if (auth == null) {
            return Collections.emptyList();
        }

        try {
            return auth.getAuthorities();
        }
        catch (Exception e) {
            logger.error("Could not retrieve authorities", e);
            return Collections.emptyList();
        }
    }

    private Authentication getAuthentication() {
        try {
            SecurityContext secCtxt = SecurityContextHolder.getContext();
            if (secCtxt == null) {
                logger.warn("SecurityContextHolder.getContext() returned null, " +
                        + "no authorities present");
                return null;
            }
            Authentication auth = secCtxt.getAuthentication();
            if (auth == null) {
                logger.warn("SecurityContextHolder.getContext().getAuthentication() "
                        + "returned null, no authorities present");
            }
            return auth;
        } catch (Exception e) {
            logger.error("Could not retrieve Authentication", e);
            return null;
        }
    }


    private void loadClassPropertyRoles() {
        Map<Class<?>, Map<String, Collection<SecurityRole>>> newClassPropertyRoles = 
                new HashMap<>();

        for (SecurityRoleToClassPropertyReader reader : securityRoleToClassPropertyReaders) {
            Map<Class<?>, Map<String, Collection<SecurityRole>>> readerClassPropertyRoles = 
                    reader.loadClassPropertyRoles();

            for (Class<?> clazz : readerClassPropertyRoles.keySet()) {
                Map<String, Collection<SecurityRole>> propertyRoles = 
                        newClassPropertyRoles.get(clazz);
                if (propertyRoles == null) {
                    propertyRoles = new HashMap<>();
                    newClassPropertyRoles.put(clazz, propertyRoles);
                }

                for (String propertyName : readerClassPropertyRoles.get(clazz).keySet()) {
                    Collection<SecurityRole> allowedRolesForProp = 
                            propertyRoles.get(propertyName);

                    if (allowedRolesForProp == null) {
                        allowedRolesForProp = new ArrayList<>();
                        propertyRoles.put(propertyName, allowedRolesForProp);
                    }

                    Collection<SecurityRole> newLdapRoles = 
                            readerClassPropertyRoles.get(clazz).get(propertyName);
                    for (SecurityRole securityRole : newLdapRoles) {
                        if (!allowedRolesForProp.contains(securityRole)) {
                            allowedRolesForProp.add(securityRole);
                        }
                    }
                }
            }
        }

        this.classPropertyRoles = newClassPropertyRoles;
    }
}