☰
SAP ABAP调用HTTP接口:Token鉴权、缓存与自动重试
2026/10/7 5:31:04 网站建设 项目流程

1. 先聊清楚这件事到底在解决什么问题

10 月 24 日那天我在工位上收尾一个接口对接的需求,同事在群里发了一句"1024 节日快乐了鸭",顺手给我丢过来一个需求单:SAP 里要定时把物料主数据推给外部的一个业务中台,对方只提供 HTTP 接口,而且必须先拿 token 才能调业务方法。这其实是现在做 ABAP 集成最常见的一类活儿,难度不算高,但坑是真的多,尤其是 token 这一层——拿不到、过期了、传错了、中文乱码了,每一个都能让你在 SM59 和 SLG1 之间来回横跳半天。

这篇就围绕"SAP ABAP 调用 HTTP 接口并用 token 登录"这个场景,把我实际做下来的完整思路、代码、踩过的坑一次讲透。它解决的问题很具体:SAP 内部数据要跟外部系统交互,而外部系统用的是现在主流的 token 鉴权,不是简单的用户名密码 Basic 认证。适合的读者是已经会一点 ABAP、能看懂 Function Module 和类方法,但没怎么做过 HTTP 外部集成的同学;如果你已经做过几轮 REST 接口对接,这篇里的缓存和续签部分应该也能给你一点参考。

先给一个小结论方便你建立整体印象:整件事拆开只有四步——拿 token、存 token、带 token 调接口、token 快过期时换新的。听起来像废话,但真正写起来,80% 的调试时间会花在第一步和第三步的细节上。下面我按"为什么这么设计"到"具体怎么敲代码"的顺序展开,每一段我都会解释清楚背后的取舍理由,而不是只丢一段代码给你抄。

需要提前说明的是,本文涉及的接口地址、密钥都是示意值,实际项目里请替换成你自己环境的配置,并且绝对不要把真实的 client_secret 硬编码进代码——这一点后面会专门讲怎么规避。

2. 技术路线选型:为什么是 IF_HTTP_CLIENT 而不是别的

在 SAP 里做外部 HTTP 调用,其实不止一条路。我见过不少同事一上来就想着用 RFC 或者把对方的接口包装成 Web Service,但很多时候对方只给一个 REST 风格的 HTTP 地址加一个 JSON body,你根本没有选择余地。所以在动手之前,我习惯先把可选路线过一遍,确认自己选的是最省事的那条。

2.1 常见的三条集成路线对比

路线适用场景优点明显短板
IF_HTTP_CLIENT(原生 HTTP 客户端)对方只提供 REST/HTTP 接口、JSON 交互灵活,任何 HTTP 方法都能发,头部随便改需要自己处理 JSON、错误码、token
生成 Web Service 代理类(SE80 企业服务)对方提供标准 WSDL调用像本地方法,参数强类型对方给不出 WSDL 就废了,token 也不好塞
RFC / 中间件转发老系统对接,SAP 之间通信稳定、性能好外部系统基本不支持,需要额外部署中间层

我这次选的毫无疑问是第一条。理由是对方接口是标准的POST /oauth/token拿令牌、POST /api/v1/material推数据的 REST 风格,返回体是 JSON,没有 WSDL 可生成,也没有中间件可用。而且 token 认证本质上就是往 HTTP header 里塞一个Authorization字段,只有原生 HTTP 客户端能让我完全控制头部。

这里有个反直觉的点值得说:很多人以为CL_REST_HTTP_CLIENT是更"现代"的写法,实际上它是给 REST 服务端开发用的辅助类,做纯客户端调用时,IF_HTTP_CLIENT配合CL_HTTP_CLIENT=>CREATE_BY_DESTINATION才是更通用、资料更多、出问题更好查的选择。我踩过一次用 REST 客户端类做外部调用的坑,SSL 配置死活读不到 SM59 里的证书,最后还是换回 IF_HTTP_CLIENT 才通的。

2.2 token 模式到底比 Basic 认证强在哪

顺带说一下为什么现在大家都用 token。Basic 认证是把用户名密码 Base64 一下塞进每个请求头,等于你的密码在网络上反复出现,任何一个环节被抓包就泄露了。token 模式则是先用凭证换一张有时效的"通行证",后续请求只带这张通行证。通行证过期就失效,就算被截获,攻击窗口也只有几分钟到几小时。

对 ABAP 开发者来说,这个设计带来的直接影响是:你的程序必须有能力判断 token 是否还有效,并且能在它失效前主动换新。如果你只是每次调用都重新申请一遍 token,功能上没问题,但对方接口通常有限流,高频申请会被直接拒掉,而且平白多了一倍的网络往返。这就是为什么后面一定要做缓存。

2.3 两种 token 获取方式的差异

常见的 token 获取方式主要有两种,选错方向会让后面所有代码都要返工,所以这里单独拎出来说:

  • client_credentials(客户端凭证模式):用固定的 client_id 和 client_secret 换 token,不涉及具体用户。适合系统对系统的数据同步,也就是我这次的场景。
  • password 模式:用某个真实用户的账号密码换 token,token 代表这个用户的权限。适合要按用户权限操作、需要记录操作人的场景。

我这次用的是第一种,因为物料推送是系统行为,不该绑定任何一个员工账号——万一那个人离职账号被锁,接口直接就挂了。这个判断在做需求评审的时候就要定下来,不要等到联调时才发现对方只支持 password 模式而你没有可用的测试账号。

3. 动手前必须搞定的环境和概念

代码写得再漂亮,SM59 目标配错一个勾选,你也会得到各种莫名其妙的报错。这部分我把它单独拎出来,是因为它真的值得花时间先弄明白。

3.1 SM59 目标配置的关键几项

在 SM59 里新建一个类型为"G - HTTP 连接到外部服务器"的目标,比如叫ZAPI_MATERIAL。填写的时候有几个地方是新手最容易翻车的:

  • 主机名和端口:注意 HTTPS 默认 443,不要把https://前缀写进主机名里,主机名只填域名。
  • 路径前缀:如果你的接口都有统一的/api/v1前缀,可以填在这里,代码里就只需要写后半段,方便以后统一改。
  • SSL 相关:如果对方是 HTTPS,必须在这里激活 SSL,并且选择正确的证书列表。这一步不做,你会在send的时候收到http_communication_failure,而且错误信息非常含糊。

我实测下来,SSL 报错是最难查的一类,因为 IF_HTTP_CLIENT 抛出的异常只有一句话,看不出是证书问题还是网络问题。我的经验是:先在 SM59 里用"连接测试"按钮测一次,如果能通,说明配置没问题,问题一定在代码;如果连接测试就失败,那别改代码了,先解决证书和网络。

3.2 用字符串类型还是用 XSTRING 传数据

ABAP 里 HTTP 报文有两个载体:set_cdata处理字符串,set_data处理二进制(XSTRING)。这次的接口是 JSON,看起来应该用字符串,但有个前提——你必须确认对方和你的系统都是 UTF-8。

我们系统里有中文物料描述,第一次联调的时候对方收到的全是问号。原因是我用set_cdata传了一个内部编码的字符串,SAP 在发送时做了默认转换。后来改成先用CL_ABAP_CONV_OUT_CE把内容转成 UTF-8 的 XSTRING,再用set_data发出去,中文就正常了。这个细节在英文环境里永远暴露不出来,但只要有中文就必修。

3.3 token 放在哪里传

token 的携带方式有好几种,对方接口文档里一定会写清楚,但如果你拿不到文档,可以从常见实践里猜一猜:

携带位置典型写法出现频率
请求头 Authorization BearerAuthorization: Bearer <token>最高,主流 REST 接口
请求头自定义字段X-Auth-Token: <token>常见于国内一些中台
URL 查询参数?access_token=<token>少数老接口,不推荐

注意:token 一定不要放在 URL 里传。URL 会被记进各种服务器访问日志、浏览器历史和监控系统,等于把通行证打印出来贴在墙上。如果对方文档要求放 URL,能沟通就沟通,改成 header。

4. 完整实操:从拿 token 到推数据

前面铺垫够了,下面是真正落地的部分。我把整个流程拆成一个可复用的工具类,大概两百来行,能覆盖大部分场景。

4.1 封装一个发送请求的核心方法

所有 HTTP 交互都走同一个底层方法,这样超时、错误处理、日志都只需要维护一份。下面是我实际用的简化版:

METHOD send_request. " iv_method: GET / POST / PUT / DELETE " iv_path : 相对路径,如 /oauth/token " iv_body : 请求体字符串,可为空 " it_headers: 附加请求头,KV 结构 " 返回:响应状态码和响应体 DATA: lo_client TYPE REF TO if_http_client, lv_url TYPE string. " 通过 SM59 目标创建客户端,证书和主机信息都从这里读 cl_http_client=>create_by_destination( EXPORTING destination = 'ZAPI_MATERIAL' IMPORTING client = lo_client EXCEPTIONS argument_not_found = 1 destination_not_found = 2 destination_no_authority = 3 plugin_not_active = 4 internal_error = 5 OTHERS = 6 ). IF sy-subrc <> 0. RAISE EXCEPTION TYPE zcx_api_error EXPORTING textid = zcx_api_error=>client_create_failed. ENDIF. " 设置超时,单位秒;不设的话默认等待可能长达几百秒 lo_client->propertytype_logon_popup = lo_client->co_disabled. cl_http_utility=>set_request_uri( request = lo_client->request uri = iv_path ). lo_client->request->set_method( iv_method ). lo_client->request->set_content_type( 'application/json; charset=utf-8' ). lo_client->request->set_header_field( name = 'Accept' value = 'application/json' ). " 逐个塞入调用方传进来的头部 LOOP AT it_headers ASSIGNING FIELD-SYMBOL(<ls_hdr>). lo_client->request->set_header_field( name = <ls_hdr>-name value = <ls_hdr>-value ). ENDLOOP. " 请求体统一转成 UTF-8 的 XSTRING,避免中文乱码 IF iv_body IS NOT INITIAL. DATA(lv_xbody) = cl_abap_conv_out_ce=>uccp( iv_body ). lo_client->request->set_data( lv_xbody ). ENDIF. lo_client->send( EXCEPTIONS http_communication_failure = 1 http_invalid_state = 2 http_processing_failed = 3 OTHERS = 4 ). IF sy-subrc <> 0. lo_client->get_last_error( IMPORTING message = DATA(lv_err) ). RAISE EXCEPTION TYPE zcx_api_error EXPORTING textid = zcx_api_error=>send_failed msg = lv_err. ENDIF. lo_client->receive( EXCEPTIONS http_communication_failure = 1 http_invalid_state = 2 http_processing_failed = 3 OTHERS = 4 ). IF sy-subrc <> 0. lo_client->get_last_error( IMPORTING message = lv_err ). RAISE EXCEPTION TYPE zcx_api_error EXPORTING textid = zcx_api_error=>receive_failed msg = lv_err. ENDIF. ev_status = lo_client->response->get_status_code( ). ev_body = lo_client->response->get_cdata( ). lo_client->close( ). ENDMETHOD.

这段代码里有几个地方是刻意这么写的,值得解释一下。

第一,create_by_destination而不是create。用目标名创建的好处是所有连接参数集中在 SM59,改地址、换证书不用动代码,而且传输到生产环境时 SM59 配置走的也是传输机制,安全可控。

第二,propertytype_logon_popup设成 disabled。如果不设,在某些配置下系统会弹出一个登录对话框问你要账号密码,这个框在后台任务里会直接把程序卡死。定时任务场景下这个设置是必须的。

第三,cl_abap_conv_out_ce=>uccp负责把内部编码转成 UTF-8。这里要注意方向,是"输出"转换,别用错成输入的那个类。

第四,close( )一定要调。IF_HTTP_CLIENT 底层会占用连接资源,不关的话在循环里会越积越多,最后报http_invalid_state。我见过一个哥们儿在一个批量推送报表里忘了关闭,推到第 40 条就报错,查了两小时才找到原因。

4.2 获取 token 并解析出来

有了底层方法,拿 token 就变成一个很短的函数:

METHOD get_token. " 命中缓存直接返回,避免重复申请 IF mt_token IS NOT INITIAL AND sy-datum <= mv_valid_date. ev_token = mv_token. RETURN. ENDIF. " 构造请求体,凭证从配置表读取,绝不硬编码 DATA(lv_body) = |\{| && |"grant_type":"client_credentials",| && |"client_id":"{ mv_client_id }",| && |"client_secret":"{ mv_client_secret }"| && |\}|. DATA: lt_headers TYPE ztt_headers. APPEND VALUE #( name = 'Content-Type' value = 'application/json' ) TO lt_headers. send_request( EXPORTING iv_method = 'POST' iv_path = '/oauth/token' iv_body = lv_body it_headers = lt_headers IMPORTING ev_status = DATA(lv_status) ev_body = DATA(lv_resp) ). IF lv_status <> 200. RAISE EXCEPTION TYPE zcx_api_error EXPORTING textid = zcx_api_error=>token_failed msg = |HTTP { lv_status } : { lv_resp }|. ENDIF. " 用标准 JSON 工具反序列化 TYPES: BEGIN OF ty_token, access_token TYPE string, token_type TYPE string, expires_in TYPE i, END OF ty_token. DATA ls_token TYPE ty_token. /ui2/cl_json=>deserialize( EXPORTING json = lv_resp pretty_name = /ui2/cl_json=>pretty_mode-camel_case CHANGING data = ls_token ). IF ls_token-access_token IS INITIAL. RAISE EXCEPTION TYPE zcx_api_error EXPORTING textid = zcx_api_error=>token_empty. ENDIF. " 写入缓存,留出 60 秒的安全余量 mv_token = ls_token-access_token. mv_valid_date = sy-datum. mv_valid_time = sy-uzeit + ls_token-expires_in - 60. ev_token = mv_token. ENDMETHOD.

pretty_mode-camel_case这个参数是必须的,因为对方的 JSON 用的是下划线命名(access_token),而 ABAP 的约定是下划线。如果不指定,反序列化会匹配不上,你会得到一堆空值,然后对着正确的响应报文怀疑人生。

关于缓存,我这里用的是实例属性加上有效期,简单但够用。它的局限是只在单次程序运行期间有效,如果你的推送是分成好几个独立作业跑的,每个作业还是会各申请一次 token。要跨会话共享,就得把 token 存到数据库表里,加上过期时间字段,读的时候比较一下sy-datum和sy-uzeit。这个方案我一般只在对方限流特别严或者接口调用特别频繁时才会上,因为它引入了一个新的维护点——表数据脏了会导致所有人都拿不到 token,反而更难排查。

4.3 带 token 调用业务接口

真正推数据的时候,只需要把拿到的 token 拼进 Authorization 头:

DATA(lv_token) = get_token( ). DATA: lt_headers TYPE ztt_headers. APPEND VALUE #( name = 'Authorization' value = |Bearer { lv_token }| ) TO lt_headers. " 业务报文,这里假设是物料主数据 DATA(lv_payload) = /ui2/cl_json=>serialize( data = ls_material pretty_name = /ui2/cl_json=>pretty_mode-camel_case ). send_request( EXPORTING iv_method = 'POST' iv_path = '/api/v1/material' iv_body = lv_payload it_headers = lt_headers IMPORTING ev_status = DATA(lv_status) ev_body = DATA(lv_resp) ).

4.4 token 失效时的自动重试

即使做了缓存,还是有可能会撞上 token 在两次检查之间失效的情况,比如对方提前作废了令牌。稳妥的做法是:当业务接口返回 401 时,强制清掉缓存并重新申请一次,然后重试一次请求。注意只重试一次,避免无限循环。

IF lv_status = 401. clear_token_cache( ). " 清空缓存的 token lv_token = get_token( ). " 重新申请 " 更新头部后重试一次 MODIFY lt_headers FROM VALUE #( name = 'Authorization' value = |Bearer { lv_token }| ) TRANSPORTING value WHERE name = 'Authorization'. send_request( ... ). " 仅重试一次 ENDIF.

这个模式几乎能兜住所有 token 类问题。我实际跑下来,正常情况下一天也就多申请一两次 token,对方限流完全不受影响。

5. 踩过的坑和排查速查表

这部分是我觉得最有价值的内容,因为文档里通常不会写。

5.1 常见报错对照表

现象大概率原因处理方式
send 抛 http_communication_failureSSL 证书、网络不通、主机名写错先在 SM59 做连接测试
HTTP 401token 无效或已过期清缓存重取,检查是否带对了头部
HTTP 403权限不足,或 token 拿错了 scope核对 client_id 对应的权限范围
中文变问号编码转换缺失用 CL_ABAP_CONV_OUT_CE 转 UTF-8
返回体是乱码 XSTRING用了 get_data 而不是 get_cdata确认编码后再转字符串
循环几次后 http_invalid_state忘记 close( )每次请求后必须关闭
后台作业卡死弹窗没禁用 logon_popup设置 propertytype_logon_popup = disabled

5.2 一些只有真做过才知道的细节

关于超时。SAP 默认的 HTTP 等待时间非常长,对方如果挂着不响应,你的后台作业会一直等下去。虽然在这个高层接口里设置超时不太直观,但至少要在 SM59 目标里调整"超时时间"这一项,一般设 30 到 60 秒比较合理。批量推送时如果一条卡住,整个链条都会延误,这个值必须重视。

关于 JSON 里的日期。/ui2/cl_json反序列化日期和时间时会做格式转换,如果对方传的是2025-10-24T10:30:00这种带 T 的格式,直接映射到 DATS/TIMS 字段会失败。稳妥做法是先用 string 接住,再用自己的转换逻辑处理,别指望自动转换。

关于调试。看响应内容最直接的方式是在receive之后打个断点,把lo_client->response->get_cdata( )加到监视器里看。如果你觉得每次都要改代码太麻烦,可以写一个小的 Z 报表,传入目标名和路径,把响应原样打印出来,联调阶段能省很多时间。

关于日志。生产环境出问题时,我当时手头的日志只有状态码,完全看不出对方返回了什么。后来我在工具类里加了一处,把请求路径、状态码、响应体前 500 个字符写进一张自定义日志表。这个动作在出故障时救命——有一次对方说没收到我们的数据,我调出日志表一看,明明是对方返回了 500,责任一目了然。

提示:写日志时注意不要把 token 和 client_secret 记进去。我见过有人图方便把整个请求头打印出来,等于把密钥写进了数据库,审计过不了。

6. 生产上线的几点加固建议

6.1 凭证管理不要图省事

client_secret 最忌讳直接写在代码里。代码会进版本库、会被传输、会被打印,任何一个环节都可能泄露。我推荐的做法是建一张简单的配置表,存放接口地址、client_id、client_secret,通过 SM30 维护,表本身设置成只有特定权限组才能查看。更好的做法是把 secret 存到安全存储里通过接口读取,但对大多数项目来说配置表加权限控制已经够了。

还有一点,如果你们的系统有多个环境(开发、测试、生产),每个环境的 client_secret 通常是不一样的,别偷懒用同一套,否则测试环境的密钥泄露会直接影响生产。

6.2 幂等和重复推送

网络超时是最恶心的场景——你的请求发出去对方收到了,但响应回来的路上断掉了,你的程序认为失败于是重试,对方就收到了两条重复数据。如果你的业务接口不是幂等的,重试就可能造成重复过账。

实操上的做法有几种:一是在报文里带一个唯一请求号,让对方按这个号去重;二是发现超时后不要立刻重试,而是先调用一个查询接口确认上次是否成功;三是控制重试次数并且加上退避,比如第一次等 2 秒、第二次等 8 秒。我一般优先用第一种,前提是对方愿意配合。

6.3 后台作业的分批策略

如果是批量推送几千条物料,不要放在一个作业里一路推到底。我习惯每 200 条提交一次、失败的分开记录,这样即使中途网络抖动也只是丢一批,而不是整个作业报错回滚。提交的时候注意用COMMIT WORK配合WAIT UP TO 1 SECONDS,给对方接口留一点喘息,避免被限流。

7. 我个人的一些实际体会

这个需求从接到到上线,我前后花了大概三天,其中写代码只用了半天,剩下两天半全在调证书和验证 token 的各种边界情况。所以如果你也要做类似的活,我的建议是把联调时间往多了估。

第一,别怕在 SM59 上多花时间。连接测试能通,后面 90% 的诡异问题都跟你无关了。第二,token 缓存和自动重试这两件事看着是"优化",实际上是刚需,跳过它们你的程序在上线第一周就会出问题。第三,日志和密钥隔离这种"加分项",在出故障的时候会变成"必须项"。

最后分享一个小技巧:联调阶段可以申请让对方提供一个测试用的长效 token,专门用来验证业务逻辑,不用每次都被 token 获取环节干扰。等业务跑通了,再把获取 token 的逻辑接上。这样排查问题时能快速定位到底是鉴权层还是业务层出了问题,比眉毛胡子一把抓高效得多。

至于那个 1024 的梗——程序员的节日送自己一个能跑通的接口,我觉得比什么礼物都实在。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询