• RestTemplate踩坑 之 ContentType 自动添加字符集


    写在前边

    最近在写 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 真好用。

    重新调用接口验证通过。

  • 相关阅读:
    react中React.createRef()的使用
    react中this指向问题
    react中对props进行限制
    react中this问题
    react 中this问题
    类中方法的this
    类中方法的this
    react中render函数里面的this指向?
    Android一对一直播系统源码开发,仿朋友圈发布动态的实现
    Android游戏陪玩源码开发中,阴影效果的实现
  • 原文地址:https://www.cnblogs.com/hellxz/p/16054502.html
Copyright © 2020-2023  润新知