写在前边

最近在写 OAuth2 对接的代码,由于授权服务器(竹云BambooCloud IAM)部署在甲方内网,所以想着自己 Mock 一下授权方的返回体,验证一下我的代码。我这才踩到了坑……

故事背景

选择的 Mock 框架是 国产开源的 Moco(https://github.com/dreamhead/moco),先下载moco-runner-1.3.0-standalone.jar

再根据 Moco的官方文档(https://github.com/dreamhead/moco/blob/master/moco-doc/apis.md)和竹云对接文档配置了以下的mock配置:

BambooCloud-IAM-OAuth2-Moco.json

[
{
"description": "授权回调接口",
"request": {
"uri": "/idp/oauth2/authorize",
"method": "get",
"queries": {
"client_id": "client-id-test",
"redirect_uri": "http://localhost:8188/api/oauth2/callback",
"response_type": "code"
}
},
"redirectTo" : "http://localhost:8188/api/oauth2/callback?code=123456"
},
{
"description": "获取token接口",
"request": {
"uri": "/idp/oauth2/getToken",
"method": "post",
"headers": {
"content-type": "application/x-www-form-urlencoded"
},
"forms": {
"client_id" : "client-id-test",
"client_secret" : "client-secret-test",
"grant_type" : "authorization_code",
"code" : "123456"
}
},
"response": {
"json": {
"access_token" : "123456789"
}
}
},
{
"description": "获取用户信息接口",
"request": {
"uri": "/idp/oauth2/getUserInfo",
"method": "get",
"queries": {
"client_id": "client-id-test",
"access_token": "123456789"
}
},
"response": {
"json": {
"spRoleList":["zhangsan"],
"uid":"20190904124905344-F4BE-C2C9EFF24",
"sorgId":null,
"displayName":"张三",
"loginName":"zhangsan",
"secAccValid":1,
"givenName":"729026",
"pinyinShortName":null,
"spNameList":["portalID","tyxtest","data","certificate","sysCertification","customer"],
"employeeNumber":null
}
}
}
]

启动 Moco

java -Dfile.encoding=UTF-8 -jar moco-runner-1.3.0-standalone.jar http -p 12306 -c BambooCloud-IAM-OAuth2-Moco.json
  • 作者不愧是国人,官方文档里的端口号竟然是12306火车票订票网站,等等,该不会作者是方便模拟 12306 开发抢票功能才写的 Moco 吧,这里也用12306端口~

  • -Dfile.encoding=UTF-8 - 指定 Moco 启动服务的字符集,作者默认在程序里写了 GBK,不指定返回中文可能乱码

然后配置 SpringBoot 服务配置文件,这里就是给大家看看,不一定具备通用性:

#                 OAuth2配置                   #
################################################
#是否开启oauth2单点登录,true/false,默认不启用。
oauth2.sso.enabled=true
#授权方颁发的clientId
oauth2.sso.clientId=client-id-test
#授权方颁发的clientSecret
oauth2.sso.clientSecret=client-secret-test
#登录地址,授权方提供,示例:https://{host}:{port}/idp/oauth2/authorize
oauth2.sso.authUrl=http://localhost:12306/idp/oauth2/authorize
#获取token地址,授权方提供,示例:https://{host}:{port}/idp/oauth2/getToken
oauth2.sso.tokenUrl=http://localhost:12306/idp/oauth2/getToken
#获取用户信息地址,授权方提供,示例:https://{host}:{port}/idp/oauth2/getUserInfo
oauth2.sso.userInfoUrl=http://localhost:12306/idp/oauth2/getUserInfo
#授权回调地址,由【Nginx代理当前服务的地址 或 当前服务地址+"/api/oauth2/callback"】 组成,示例:https://{host}:{port}/api/oauth2/callback
oauth2.sso.redirectUri=http://localhost:8188/api/oauth2/callback

启动项目(8188端口,有两个api地址: /api/oauth2/sso/api/oauth2/callback),先通过程序 http://localhost:8188/api/oauth2/sso

第一、二个请求会重定向请求到 Moco 服务上的 授权回调接口 并带上一些参数

第三个请求由 Moco 服务的 授权回调接口 回调项目的回调地址(授权码回调,项目会先调用 Moco 的 获取token接口 ),然后因为全局错误拦截返回了左侧的 JSON,提示 "400 Bad Request: [no body]"

发现问题

我还以为是 Moco 服务的问题,就用 Postman 调了下 获取token接口,结果是通的!!

然后再看下 Moco 的控制台输出:

经过找不同,发现项目调用接口时请求头 Content-Type 加了 ;chartset=UTF-8 !!!

由于 Moco 配置文件里指定请求头必须一致,才能正确返回,否则就是 400状态码,而且没有返回体。

定位问题

先看看出问题的代码部分:

怀疑 MediaType.APPLICATION_FORM_URLENCODED 值有UTF-8字符集,点进去

看注释时写的是不带字符集的,debug 看下 setContentType后的 headers 内容

继续debug

看来还没改变,继续debug,进入 RestTemplateexchange() 方法

resolveUrl() 这个方法我看了下,没涉及改请求头,问题应出在 doExecute() 方法中,断点打在 request 对象后,看看 request 对象中的 headers,发现是空的,而下边有一个方法是 requestCallback.doWithRequest(request); 看起来是对 request 对象进行了处理,直接跟进去

RequestCallback 有两个最终的实现,Xhr 对应 XMLHttpRequest,这里发的是HTTP请求,所以是 HttpEntityRequestCallback 的实现

在怀疑的位置打两个断点,跟进循环看看,首先进了下边的判断,messageConverter 对象是 AllEncompassingFormHttpMessageConverter 的实例,而且在write()方法执行前,headers 仍是正常的

查看 write() 方法实现,经典 3 选 1,由于我们的 Content-Typeapplication/x-www-form-urlencoded,理应进入 FormHttpMessageConverter 实现中

writeForm() 执行前 ContentType 仍未改变,进入 writeForm() 方法看看

终于,在 org.springframework.http.converter.FormHttpMessageConverter.getFormContentType(MediaType) 的注释和代码中发现了问题。

对于表单内容类型的请求的 ContentType 设置分三种情况:

  • 如果没有 ContentType 就默认添加 application/x-www-form-urlencoded;charset=UTF-8
  • 如果有 ContentType 但没字符集,也会自动加字符集
  • 如果 ContentType 存在且有字符集设置,则直接返回

跳出 getFormContentType(),可以看到 contenType 对象已经有字符集了。

联想到作者信誓旦旦地注释 // should never occur 这就很说明问题了

解决问题

RestTemplate 自动使用 org.springframework.http.converter.FormHttpMessageConverter 添加字符集可能是出于对请求乱码的担忧。而我们这里的 Moco 配置添加了请求头判断,值全转成大写或小宝一定要一致,所以改一下 Moco 问题接口配置的content-type的值,加上字符集验证效果。

有1说1 ,保存完后,看下 Moco 控制台它已经自动刷新配置文件了,Moco 真好用。

重新调用接口验证通过。

RestTemplate踩坑 之 ContentType 自动添加字符集的更多相关文章

  1. djangorestframework+vue-cli+axios,为axios添加token作为headers踩坑记

    情况是这样的,项目用的restful规范,后端用的django+djangorestframework,前端用的vue-cli框架+webpack,前端与后端交互用的axios,然后再用户登录之后,a ...

  2. ABP框架入门踩坑-添加实体

    添加实体 ABP踩坑记录-目录 这里我以问答模块为例,记录一下我在创建实体类过程中碰到的一些坑. 审计属性 具体什么是审计属性我这里就不再介绍了,大家可以参考官方文档. 这里我是通过继承定义好的基类来 ...

  3. github webhook 实现代码自动部署 踩坑!! 附加git&coding webhook部署代码

    踩坑: 1.php程序执行linux命令是以webserver的user用户(如apache .www……)操作的,需要在/etc/sudoers添加用户免密码操作权限; %apache ALL=(A ...

  4. ffmpeg 踩坑实录 添加实时水印(二)

    一.背景介绍 最近领导要求做一个视频录制的相关项目.其中,需要对视频文件进行添加 实时时间水印.于是,我想到了使用之前的ffmpeg来做. 二.ffmpeg实际操作 首先把需要添加水印的视频文件,上传 ...

  5. k8s删除节点后再重新添加进去(踩坑)

    开启本地集群,发现一台节点出问题了,想删除再换一台节点,结果就踩坑了,还好本地有好几套环境. 再master节点执行以下命令 [root@k8s-master ~]# kubectl drain k8 ...

  6. 一次flume exec source采集日志到kafka因为单条日志数据非常大同步失败的踩坑带来的思考

    本次遇到的问题描述,日志采集同步时,当单条日志(日志文件中一行日志)超过2M大小,数据无法采集同步到kafka,分析后,共踩到如下几个坑.1.flume采集时,通过shell+EXEC(tail -F ...

  7. Jeecg踩坑不完全指南

    公司用了这个叫做jeecg的快速开发框架,我不知道有多少公司在用这个框架,园子里有的可以吱一声.个人觉得这框架唯一优势就是可以让不会ssh的人也能进行开发,只要你会J2SE,有web后台发开经验即可. ...

  8. 你真的了解字典(Dictionary)吗? C# Memory Cache 踩坑记录 .net 泛型 结构化CSS设计思维 WinForm POST上传与后台接收 高效实用的.NET开源项目 .net 笔试面试总结(3) .net 笔试面试总结(2) 依赖注入 C# RSA 加密 C#与Java AES 加密解密

    你真的了解字典(Dictionary)吗?   从一道亲身经历的面试题说起 半年前,我参加我现在所在公司的面试,面试官给了一道题,说有一个Y形的链表,知道起始节点,找出交叉节点.为了便于描述,我把上面 ...

  9. C# -- HttpWebRequest 和 HttpWebResponse 的使用 C#编写扫雷游戏 使用IIS调试ASP.NET网站程序 WCF入门教程 ASP.Net Core开发(踩坑)指南 ASP.Net Core Razor+AdminLTE 小试牛刀 webservice创建、部署和调用 .net接收post请求并把数据转为字典格式

    C# -- HttpWebRequest 和 HttpWebResponse 的使用 C# -- HttpWebRequest 和 HttpWebResponse 的使用 结合使用HttpWebReq ...

随机推荐

  1. Eureka Server启动过程

    前面对Eureka的服务端及客户端的使用均已成功实践,对比Zookeeper注册中心的使用区别还是蛮大的: P:分区容错性(⼀定的要满⾜的)C:数据⼀致性 A:⾼可⽤:CAP不可能同时满⾜三个,要么是 ...

  2. 轻松理解JS中的面向对象,顺便搞懂prototype和__proto__的原理介绍

    这篇文章主要讲一下JS中面向对象以及 __proto__,ptototype和construcator,这几个概念都是相关的,所以一起讲了. 在讲这个之前我们先来说说类,了解面向对象的朋友应该都知道, ...

  3. spring IOC的理解,原理与底层实现?

    从总体到局部 总 控制反转:理论思想,原来的对象是由使用者来进行控制,有了spring之后,可以把整个对象交给spring来帮我们进行管理                DI(依赖注入):把对应的属性 ...

  4. [题解]UVA10801 Lift Hopping

    链接:http://vjudge.net/problem/viewProblem.action?id=22172 描述:有n部电梯,每部电梯都有不能停下的楼层,要求搭乘电梯从第0层到第k层. 思路:单 ...

  5. .Net Core之JWT授权

    一.什么是JWT 文章参考:https://www.leo96.com/article/detail/55 JSON Web令牌(JWT)是一个开放标准(RFC 7519),它定义 了一种紧凑且自包含 ...

  6. WPF中ComboBox控件的SelectedItem和SelectedValue的MVVM绑定

    问题描述:左侧是一个ListView控件,用于显示User类的Name属性,右侧显示其SelectedItem的其他属性,包括Age, Address,和Category.其中Category用Com ...

  7. go 中 sort 如何排序,源码解读

    sort 包源码解读 前言 如何使用 基本数据类型切片的排序 自定义 Less 排序比较器 自定义数据结构的排序 分析下源码 不稳定排序 稳定排序 查找 Interface 总结 参考 sort 包源 ...

  8. Java:IO流(二)——InputStream/OutputStream具体用法:FileXXXStream、ByteArrayXXXStream

    1.说明 InputStream和OutputStream是Java标准库中最基本的IO流,它们都位于java.io包中,该包提供了所有同步IO的功能. 2.模块:java.io.InputStrea ...

  9. Pycharm:一直connecting to console的解决办法

    方法一: 1.打开Anaconda cmd(也就是Anaconda Prompt,在启动栏Anaconda目录里应该有)2.输入echo %PATH% 获得PATH value3.在PyCharm中, ...

  10. VS常用的快捷键

    整理代码          Ctrl+k+f 注释                 Ctrl+k+c 取消注释          Ctrl+k+u 帮助文档          F1 无调试启动     ...