Oauth2 - FastJson对Oauth2结果序列化问题的处理过程记录_oath2 http.converter-程序员宅基地

技术标签: spring  fastjson  oauth2  springboot  

问题的暴露

起因: 在工作中,授权框架从 Spring Security 更换为 Oauth2,授权获取 token 的时候,发现登录后,使用 FastJson 和采用默认的Jackson2 颁发 token 返回结果格式完全不同,按照 Oauth2 官网提供的示例中的返回结果,对比使用 FastJson 返回结果完全不同,于是在这个情况下,决定先探一下 Jackson2 为什么会返回正确格式,而 FastJson 却存在问题。

FastJson 错误格式

{
    
    "additionalInformation": {
    },
    "expiration": "2021-04-15 16:22:09",
    "expired": false,
    "expiresIn": 7199,
    "refreshToken": null,
    "scope": [
        "all"
    ],
    "tokenType": "bearer",
    "value": "852fd42a-1765-402d-bd27-3c6dfbf37eef"
}

Jackson2 正确格式

{
    
    "access_token": "852fd42a-1765-402d-bd27-3c6dfbf37eef",
    "token_type": "bearer",
    "expires_in": 7199,
    "scope": "all"
}

1、寻找问题根源

最开始发现这个问题的时候,我也是一头雾水,反正就很懵。很显然我只是更换了一个消息序列化框架,并没有更换其他内容,所以问题就出在消息转换器上。既然问题定位到了后,那么久先从正确的一个消息序列化框架入手,先看看他是怎么完成对 Oauth2 定制化解析的。

根据以前看 MybatisSpring 源码打下的基础,决定从 Jackson2 源码入手,通过对源码 DEBUG 看看能不能找到一些端倪。

1.1 寻找 Jackson2 写出消息的方法

  1. 进入 MappingJackson2HttpMessageConverter 类中,查看每个方法,凭借直觉判断哪个方法是往外写结果的方法,最终在 org.springframework.http.converter.AbstractGenericHttpMessageConverter 类中找到了 org.springframework.http.converter.AbstractGenericHttpMessageConverter#write 方法,代码如下:
public final void write(final T t, @Nullable final Type type, @Nullable MediaType contentType,
		HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
    

	final HttpHeaders headers = outputMessage.getHeaders();
	addDefaultHeaders(headers, t, contentType);

	if (outputMessage instanceof StreamingHttpOutputMessage) {
    
		StreamingHttpOutputMessage streamingOutputMessage = (StreamingHttpOutputMessage) outputMessage;
		streamingOutputMessage.setBody(outputStream -> writeInternal(t, type, new HttpOutputMessage() {
    
			@Override
			public OutputStream getBody() {
    
				return outputStream;
			}
			@Override
			public HttpHeaders getHeaders() {
    
				return headers;
			}
		}));
	}
	else {
    
		writeInternal(t, type, outputMessage);
		outputMessage.getBody().flush();
	}
}

在这里插入图片描述
在这里插入图片描述

通过 DEBUG 模式,将断点打到 if 条件上,最终发现并没有进入 if 内,而是走的 else , 并且我能够确定到当前参数 t 的类型是 DefaultOAuth2AccessToken,并且这个对象和 FastJson 序列化后的 json 格式十分相似,那么我就确定了我之前的判断是对的,那么继续进入 writeInternal(t, type, outputMessage) 方法内;

1.2 确定 writeInternal(t, type, httpOutputMessage) 实现类

在这里插入图片描述

啊,这~, 不要紧,咱们只是为了解决问题,而不是学源码,三个实现类都来一个断点,走哪个类,就看哪个类嘛,但是根据类名,我猜测是在 AbstractJackson2HttpMessageConverter 中的。

1.3 进入 Jackson2 抽象消息转换器中

@Override
protected void writeInternal(Object object, @Nullable Type type, HttpOutputMessage outputMessage)
		throws IOException, HttpMessageNotWritableException {
    

	// ********************************断点打在此处***************************************

	MediaType contentType = outputMessage.getHeaders().getContentType();
	JsonEncoding encoding = getJsonEncoding(contentType);

	OutputStream outputStream = StreamUtils.nonClosing(outputMessage.getBody());
	JsonGenerator generator = this.objectMapper.getFactory().createGenerator(outputStream, encoding);
	try {
    
		writePrefix(generator, object);

		Object value = object;
		Class<?> serializationView = null;
		FilterProvider filters = null;
		JavaType javaType = null;

		if (object instanceof MappingJacksonValue) {
    
			MappingJacksonValue container = (MappingJacksonValue) object;
			value = container.getValue();
			serializationView = container.getSerializationView();
			filters = container.getFilters();
		}
		if (type != null && TypeUtils.isAssignable(type, value.getClass())) {
    
			// 通过 DEBUG ,看到这句话被执行了,很显然在 1.1 步中,我就知道这一步执行的答案了
			javaType = getJavaType(type, null);
		}

		ObjectWriter objectWriter = (serializationView != null ?
				this.objectMapper.writerWithView(serializationView) : this.objectMapper.writer());
		if (filters != null) {
    
			objectWriter = objectWriter.with(filters);
		}
		if (javaType != null && javaType.isContainerType()) {
    
			objectWriter = objectWriter.forType(javaType);
		}
		SerializationConfig config = objectWriter.getConfig();
		if (contentType != null && contentType.isCompatibleWith(MediaType.TEXT_EVENT_STREAM) &&
				config.isEnabled(SerializationFeature.INDENT_OUTPUT)) {
    
			objectWriter = objectWriter.with(this.ssePrettyPrinter);
		}
		// 最终进入了这个方法
		objectWriter.writeValue(generator, value);

		writeSuffix(generator, object);
		generator.flush();
		generator.close();
	}
	catch (InvalidDefinitionException ex) {
    
		throw new HttpMessageConversionException("Type definition error: " + ex.getType(), ex);
	}
	catch (JsonProcessingException ex) {
    
		throw new HttpMessageNotWritableException("Could not write JSON: " + ex.getOriginalMessage(), ex);
	}
}

在这里插入图片描述

1.4 进入写对象的处理类中 ObjectWrite

1.3 中,通过 DEBUG 发现方法进入了 objectWriter.writeValue(generator, value); 内,那么这一步就 DEBUG 这个方法内部

public void writeValue(JsonGenerator g, Object value) throws IOException {
    
     _assertNotNull("g", g);
     _configureGenerator(g);
     if (_config.isEnabled(SerializationFeature.CLOSE_CLOSEABLE)
             && (value instanceof Closeable)) {
    

         Closeable toClose = (Closeable) value;
         try {
    
             _prefetch.serialize(g, value, _serializerProvider());
             if (_config.isEnabled(SerializationFeature.FLUSH_AFTER_WRITE_VALUE)) {
    
                 g.flush();
             }
         } catch (Exception e) {
    
             ClassUtil.closeOnFailAndThrowAsIOE(null, toClose, e);
             return;
         }
         toClose.close();
     } else {
    
         _prefetch.serialize(g, value, _serializerProvider());
         if (_config.isEnabled(SerializationFeature.FLUSH_AFTER_WRITE_VALUE)) {
    
             g.flush();
         }
     }
 }

在这里插入图片描述

通过 DEBUG 此方法,最终进入了 else 的逻辑,执行了 _prefetch.serialize(g, value, _serializerProvider()) 这句代码,那么继续进入该方法

1.5 进入 com.fasterxml.jackson.databind.ObjectWriter.Prefetch#serialize 方法内

public void serialize(JsonGenerator gen, Object value, DefaultSerializerProvider prov) throws IOException {
    
    if (typeSerializer != null) {
    
        prov.serializePolymorphic(gen, value, rootType, valueSerializer, typeSerializer);
    } else  if (valueSerializer != null) {
    
        prov.serializeValue(gen, value, rootType, valueSerializer);
    } else if (rootType != null) {
    
        prov.serializeValue(gen, value, rootType);
    } else {
    
        prov.serializeValue(gen, value);
    }
}

在这里插入图片描述

通过 DEBUG,不难发现最终走到了 else 逻辑,进入 prov.serializeValue(gen, value)

1.6 进入 DefaultSerializerProvider 类中

通过该类名就能知道,这个类主要是提供一个消息序列化器,那么这个类是一个重头戏,最终今天要分析的源头也就在这个类中

public void serializeValue(JsonGenerator gen, Object value) throws IOException {
    
    _generator = gen;
    if (value == null) {
    
        _serializeNull(gen);
        return;
    }
    final Class<?> cls = value.getClass();

    // ******************************************************************
    final JsonSerializer<Object> ser = findTypedValueSerializer(cls, true, null);
	// ******************************************************************

    PropertyName rootName = _config.getFullRootName();
    if (rootName == null) {
     // not explicitly specified
        if (_config.isEnabled(SerializationFeature.WRAP_ROOT_VALUE)) {
    
            _serialize(gen, value, ser, _config.findRootName(cls));
            return;
        }
    } else if (!rootName.isEmpty()) {
    
        _serialize(gen, value, ser, rootName);
        return;
    }
    _serialize(gen, value, ser);
}

在代码中,我标注了一句代码,该代码是产生今天这个问题的核心原因,那么这句代码到底都做了什么内容?
通过方法名, findTypedValSerializer ,翻译过来就是 通过被序列化的对象的Class对象,获取到该类的序列化器
在这里插入图片描述
通过以上的代码,你发现了什么了没,是不是 Oauth2 针对了 Jackson2 开发了一个单独的消息序列化器,却并没有对 FastJson 开发,
既然你不给我开发,我自己开发一个就完事儿了,说干就干。

2. 针对 Oauth2 开发 FastJson 序列化器

2.1 通过 第1 点 的分析,我们知道 Oauth2Jackson2 开发了一个消息序列化器,那么我们需要自己写一个,就先看看别人的处理逻辑是怎么样的!

进入 OAuth2AccessTokenJackson2Serializer 类中

public final class OAuth2AccessTokenJackson2Serializer extends StdSerializer<OAuth2AccessToken> {
    

	public OAuth2AccessTokenJackson2Serializer() {
    
		super(OAuth2AccessToken.class);
	}

	@Override
	public void serialize(OAuth2AccessToken token, JsonGenerator jgen, SerializerProvider provider) throws IOException,
			JsonGenerationException {
    
		jgen.writeStartObject();
		// OAuth2AccessToken.ACCESS_TOKEN = access_token
		jgen.writeStringField(OAuth2AccessToken.ACCESS_TOKEN, token.getValue());
		// OAuth2AccessToken.TOKEN_TYPE = token_type
		jgen.writeStringField(OAuth2AccessToken.TOKEN_TYPE, token.getTokenType());
		OAuth2RefreshToken refreshToken = token.getRefreshToken();
		if (refreshToken != null) {
    
			// OAuth2AccessToken.REFRESH_TOKEN = refresh_token
			jgen.writeStringField(OAuth2AccessToken.REFRESH_TOKEN, refreshToken.getValue());
		}
		Date expiration = token.getExpiration();
		if (expiration != null) {
    
			long now = System.currentTimeMillis();
			// OAuth2AccessToken.EXPIRES_IN = expires_in
			jgen.writeNumberField(OAuth2AccessToken.EXPIRES_IN, (expiration.getTime() - now) / 1000);
		}
		Set<String> scope = token.getScope();
		if (scope != null && !scope.isEmpty()) {
    
			StringBuffer scopes = new StringBuffer();
			for (String s : scope) {
    
				Assert.hasLength(s, "Scopes cannot be null or empty. Got " + scope + "");
				scopes.append(s);
				scopes.append(" ");
			}
			// OAuth2AccessToken.SCOPE = scope
			jgen.writeStringField(OAuth2AccessToken.SCOPE, scopes.substring(0, scopes.length() - 1));
		}
		Map<String, Object> additionalInformation = token.getAdditionalInformation();
		for (String key : additionalInformation.keySet()) {
    
			jgen.writeObjectField(key, additionalInformation.get(key));
		}
		jgen.writeEndObject();
	}
}

通过以上的类,发现好像这并不难啊,我自己也能写一个,那么开始动手写一个 FastJson 的消息序列化器

2.2 自己开发 Oauth2AccessTokenFastJsonSerializer

OAuth2AccessTokenJackson2Serializer 都是取值赋值,我搞一个 Map,然后序列化成 String,然后写出去,效果是一样的,那么说干就干

public class Oauth2AccessTokenFastJsonSerializer implements ObjectSerializer {
    

    @Override
    public void write(JSONSerializer serializer, Object object, Object fieldName, Type fieldType, int features) throws IOException {
    
        SerializeWriter out = serializer.out;
        Map<String, Object> token = this.fastJsonSerializerOAuth2Token((OAuth2AccessToken)object);

        String strToken = JSONObject.toJSONString(token);

        out.write(strToken);
    }

    private Map<String, Object> fastJsonSerializerOAuth2Token(OAuth2AccessToken token) {
    
    	// 创建结果集包装容器
        Map<String, Object> tokenMap = new HashMap<>();
		// access_token 赋值
        tokenMap.put(OAuth2AccessToken.ACCESS_TOKEN, token.getValue());
        // token_type 赋值
        tokenMap.put(OAuth2AccessToken.TOKEN_TYPE, token.getTokenType());
		// 是否存在 refresh_token
        OAuth2RefreshToken refreshToken = token.getRefreshToken();
        if (refreshToken != null) {
    
            tokenMap.put(OAuth2AccessToken.REFRESH_TOKEN, refreshToken.getValue());
        }
		// 是否存在超时时间
        Date expiration = token.getExpiration();
        if (expiration != null) {
    
            long now = System.currentTimeMillis();
            tokenMap.put(OAuth2AccessToken.EXPIRES_IN, (expiration.getTime() - now) / 1000);
        }
		// 多个token作用范围需要处理,单不能为空
        Set<String> scope = token.getScope();
        if (scope != null && !scope.isEmpty()) {
    
            StringBuilder scopes = new StringBuilder();
            for (String s : scope) {
    
                Assert.hasLength(s, "Scopes cannot be null or empty. Got " + scope + "");
                scopes.append(s);
                scopes.append(" ");
            }
            tokenMap.put(OAuth2AccessToken.SCOPE, scopes.substring(0, scopes.length() - 1));
        }
		// 其余参数的处理
        Map<String, Object> additionalInformation = token.getAdditionalInformation();
        for (String key : additionalInformation.keySet()) {
    
            tokenMap.put(key, additionalInformation.get(key));
        }

        return tokenMap;
    }
}

注意: Jackson2 是实现的 com.fasterxml.jackson.databind.ser.std.StdSerializer 这个类,然后重写 serialize(OAuth2AccessToken token, JsonGenerator jgen, SerializerProvider provider) 方法,而这个类确是 Jackson2 提供的,FastJson 重写序列化器需要实现 com.alibaba.fastjson.serializer.ObjectSerializer 类,重写 write(JSONSerializer serializer, Object object, Object fieldName, Type fieldType, int features) 方法

2.3 将自己的序列化器注册到序列化器管理器中

@Configuration
public class GlobalFastJsonHttpMessageConvertConfiguration implements WebMvcConfigurer {
    

    @Override
    public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
    
        converters.clear();
//        converters.removeIf(convert -> convert instanceof FastJsonHttpMessageConverter);
//        converters.removeIf(convert -> convert instanceof MappingJackson2HttpMessageConverter);
        converters.add(this.fastJsonHttpMessageConvert());
    }

    private HttpMessageConverter<?> fastJsonHttpMessageConvert() {
    
        FastJsonHttpMessageConverter fc = new FastJsonHttpMessageConverter();

        FastJsonConfig fjc = new FastJsonConfig();
        fjc.setSerializerFeatures(
                // 引用字段名称
                SerializerFeature.QuoteFieldNames,
                // 写入映射空值
                SerializerFeature.WriteMapNullValue,
                // 禁用循环参考检测
                SerializerFeature.DisableCircularReferenceDetect,
                // 写入日期使用日期格式
                SerializerFeature.WriteDateUseDateFormat,
                // 将空字符串写入为空
                SerializerFeature.WriteNullStringAsEmpty
        );

		// **************************** 注册序列化器到容器中 ************************************
        fjc.getSerializeConfig().put(DefaultOAuth2AccessToken.class, new Oauth2AccessTokenFastJsonSerializer());
		// **************************** 注册序列化器到容器中 ************************************

        List<MediaType> mediaTypeList = new ArrayList<>();
        mediaTypeList.add(MediaType.APPLICATION_JSON);
        fc.setSupportedMediaTypes(mediaTypeList);
        fc.setFastJsonConfig(fjc);

        return fc;
    }
}

fjc.getSerializeConfig().put(DefaultOAuth2AccessToken.class, new Oauth2AccessTokenFastJsonSerializer()) 这句代码,就是将自定义的 Oauth2 定制化序列化器注册到配置容器中,值得注意得是, SerializeConfig 中,keyDefaultOAuth2AccessToken.class 并不是 OAuth2AccessToken.class

2.4 测试 Oauth2AccessTokenFastJsonSerializer

完全符合需求

在这里插入图片描述

3. 总结

解决问题其实并不难,只是需要一双慧眼,去发现问题所在的点,然后一点点的推进,就能解决问题,阅读源码需要坚持,有了基础的条件下,很多代码配合注释是嫩看懂的,千万不要遇到问题就打退堂鼓。

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/qq_38800175/article/details/115723255

智能推荐

html Canvas粒子文字特效_html canvas 效果-程序员宅基地

文章浏览阅读757次,点赞19次,收藏9次。文字动态特效_html canvas 效果

el-table-column 表格列自适应宽度的组件封装说明

针对组件业务上的需求,需要给 el-table-column 加上限制,需保证表头在一行展示,部分列的内容要一行展示,自适应单项列的宽度;

Ali-Sentinel-链路控制

Ali-Sentinel-链路控制

C语言实现SM4(基于GMSSL)_使用c语言调用openssl实现sm4代码-程序员宅基地

文章浏览阅读4.2k次。环境:vs2019 gmssl 32位编译1、首先新建项目2、在VS的工程设置工程属性(参考连接https://blog.csdn.net/zhonghua_csdn/article/details/99011892)右击工程名 ——> 选择“属性” 在“VC++目录”——> “包含目录”中添加openSSL的include文件(在您安装openssl的文件下) 在“VC++目录”——> “库目录”中添加openSSL的lib文件(在您安装openssl的文件下) 在“._使用c语言调用openssl实现sm4代码

让Windows免疫Autorun病毒-程序员宅基地

文章浏览阅读73次。来源:http://www.bysjhf.com.cn目前,U盘病毒的情况非常严重,几乎所有带病毒的U盘,根目录里都有一个autorun.inf。右键菜单多了“自动播放”、“Open”、“Browser”等项目。由于我们习惯用双击来打开磁盘,但现在我们双击,通常不是打开U盘,而是让autorun.inf里所设的程序自动播放。所以对于很多人来说相当麻烦。其实Autorun...._linux怎么为windows做autorun免疫

随便推点

Qt报错:Error while building/deploying project *** (kit: Desktop Qt 5.12.9 MSVC2017...)_error while building/deploying project xianzhazhi -程序员宅基地

文章浏览阅读1.5k次。Qt Creator 报错:Error while building/deploying project helloworld (kit: Desktop Qt 5.6.2 MinGW 32bit) When executing step "qmake" - zhangjunwu - 博客园 (cnblogs.com)https://www.cnblogs.com/zhangjunwu/p/7417566.html注意:Qt文件路径不要出现中文名字和空格!!!......_error while building/deploying project xianzhazhi (kit: desktop qt 5.12.9 ms

解决create-react-app创建项目出错_installing packages. this might take a couple of m-程序员宅基地

文章浏览阅读1.3k次。Installing packages. This might take a couple of minutes.Installing react, react-dom, and react-scripts with cra-template-typescript...npm ERR! code 1npm ERR! path C:\Users\MHX\Desktop\react-demo\node_modules\canvasnpm ERR! command failednpm ERR! comm_installing packages. this might take a couple of minutes. installing react,

关于西电计科本科学习的一些经验分享与资料汇总_西电毕设拿良容易吗-程序员宅基地

文章浏览阅读1.9w次,点赞43次,收藏214次。关于西电计科本科学习的一些经验分享与资料汇总_西电毕设拿良容易吗

【nodejs】使用express-generator快速搭建项目框架-程序员宅基地

文章浏览阅读279次,点赞9次,收藏3次。项目根目录打开终端,执行以下命令,安装依赖。执行以下命令后,在浏览器中打开。就可以打开这个项目了。

c++二维vector_c++ 二维vector-程序员宅基地

文章浏览阅读8.5k次,点赞4次,收藏24次。关于C++中二维vector使用vector本来就是可以用来代替一维数组的,vector提供了operator[]函数,可以像数组一样的操作,而且还有边界检查,动态改变大小。这里只介绍用它来代替二维的数组,二维以上的可以依此类推。1、定义二维vectorvector<vector<int>> A;//错误的定义方式vector<vector<int> > A;//正缺的定义方式vector<vector<int> > v;/_c++ 二维vector

python算法题_python算法题-程序员宅基地

文章浏览阅读187次。广告关闭腾讯云11.11云上盛惠 ,精选热门产品助力上云,云服务器首年88元起,买的越多返的越多,最高返5000元!导言:记录下学习的算法题,写练多,脑子才能转的快! 今日算法题:二分法查找说下我对于二分法查找的理解:【和猜数字游戏差不多】 要在一个有序数列中找到一个与对应给定数字。 1、找到有序数列中最中间的数字2、若中间值大于给定值,则在左边数列重新二分查找3、若中间值小于给定值,则在右边数列..._python服务端算法题