在springboot程序中jackson自定义注解和字段解析器

Published on 2021-10-27 18:17 in 分类: 博客 with 狂盗一枝梅
分类: 博客

一、需求提出和两种解决方案

最近有个需求,需要在springboot程序中在返回给前端json串的时候将部分字段加密。在之前的一篇文章中,曾经说过对整个请求体进行加密的方法,可以使用spring扩展的参数解析器做处理:spring mvc请求体偷梁换柱:HandlerMethodArgumentResolver ,那如果想要对返回值中部分字段的值做加密处理呢?这里想到了两种方式

  • 使用扩展的参数解析器的方式,在resolveArgument方法中利用反射一一查看相对应的字段需不需要进行加密,如果需要加密则将相应的字段进行加密转换
  • 使用jackson相关的功能处理,虽然还暂时不知道怎么处理,但是肯定有办法。

那就一一分析两种方式的利弊

1、使用扩展的参数解析器的方式

能处理,但是转换类型之后没有相应的类型进行存储,举个例子,有个对象如下所示

{
	"code": "",
	"data": {
		"accessToken": "",
		"expireTime": 0
	},
	"msg": ""
}

我想对data字段进行加密,加密前的类型是个具体类型,比如是个TokenResp,加密后是个字符串类型,那么转换后的对象整体是拥有code、data字符串和msg字段的对象,可以考虑使用map存储(虽然没试验到底行不行)。可以确定的是使用这种方式会比较复杂,毕竟要使用反射获取很多字段并取到值,这势必会破坏原springboot关于序列化功能的完整性。

2、使用jackson相关的功能

在经过参数解析器处理之后,最终参数会被jackson的ObjectMapper转换成json字符串并返回给调用方,所以从最终结果上来看,jakson是最终执行序列化的工具,从它入手解决这个问题更加合理。google一下相关功能,还真的找到了相关的功能:https://stackoverflow.com/questions/34965201/customize-jackson-objectmapper-to-read-custom-annotation-and-mask-fields-annotat ,使用回答者提供的方式,成功解决了这个问题。

二、使用jackson解决部分序列化字段加密的功能

1、定义注解

首先定义一个注解,该注解加在类上表示整个对象加密,加在字段上表示相应的字段加密

@Documented
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface RsaEncrypt {

    /**
     * 是否启用加密
     *
     * @see
     */
    boolean encrypt() default true;
}

2、继承标准序列化类

该类的作用是对具体的字段转换成json字符串后再加密,这里demo是使用RSA进行了加密和签名。

@Slf4j
public class RsaDataSerializer extends StdSerializer<Object> {

    public RsaDataSerializer() {
        super(Object.class);
    }

    @Override
    public void serialize(Object value, JsonGenerator gen, SerializerProvider provider) throws IOException {
        if (Objects.isNull(value)) {
            return;
        }
        LoginUser principal = (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        ClientDetails user = principal.getUser();
        String publicKey = user.getPublicKey();
        RsaProperties bean = SpringUtils.getBean(RsaProperties.class);
        String privateKey = bean.getPrivateKey();
        try {
            String base64Content = RsaUtils.encryptAndSign(ObjectMapperFactory.getObjectMapper().writeValueAsString(value), publicKey, privateKey);
            gen.writeString(base64Content);
        } catch (JsonProcessingException e) {
            log.error("", e);
        }
    }
}

3、绑定序列化类和相关字段的关联关系

如何确定被自定义注解修饰的某个字段应该用哪个序列化类进行序列化?继承NopAnnotationIntrospector类,实现findSerializer方法即可。

@Slf4j
public class RsaDataAnnotationIntrospector extends NopAnnotationIntrospector {

    @Override
    public Object findSerializer(Annotated am) {
        RsaEncrypt annotation = am.getAnnotation(RsaEncrypt.class);
        if (annotation != null) {
            return RsaDataSerializer.class;
        }
        return null;
    }
}

4、定制ObjectMapper

做起来非常简单

@Bean
@Primary
ObjectMapper jacksonObjectMapper(Jackson2ObjectMapperBuilder builder) {
    ObjectMapper mapper = builder.createXmlMapper(false).build();
    AnnotationIntrospector sis = mapper.getSerializationConfig().getAnnotationIntrospector();
    AnnotationIntrospector is1 = AnnotationIntrospectorPair.pair(sis, new RsaDataAnnotationIntrospector());
    mapper.setAnnotationIntrospector(is1);
    return mapper;
}

但是要知道其原理。

打开方法定义:JacksonAutoConfiguration.JacksonObjectMapperConfiguration#jacksonObjectMapper(org.springframework.http.converter.json.Jackson2ObjectMapperBuilder)

image-20211027181239692

可以看到,如果我们不定义ObjectMapper对象,则springboot程序会帮我们生成一个默认的,我们只需要根据Jackson2ObjectMapperBuilder对象生成ObjectMapper对象,既不破坏原有功能的完整性,也能自由添加jackson的特性,两全其美。


#jackson #springboot
目录