随着REST API 和微服务的兴起, 越来越多的应用都需要用到接口Oauth2.0授权和Callout调用,今天就来和大家讲一讲如何在其他应用中恰当地调用Salesforce提供的Apex Rest API 服务!
基本组件:
- Connected App
- Integration Profile
- Connect user with integration profile
- APEX REST Class
组件用途:
Connected App:
主要可以生成专属的client id 和client secret, 为两串无规律字符,且一般生成以后不会发生变化,分享给消费者方调用时需要配置,有些鉴权方式下必须要具备这俩个参数时才可以成功调用。
同时需要配置相应的授权范畴(哪些可以被访问),以及IP限制,refresh token 的有效期 等等;
授权机制可以根据需要进行设置和编辑, 例如, 设置不同的访问授权范畴以及IP限制,refresh token 的有效期之类的。
Api Integration profile:
主要可以设置,该profile 只能授权访问指定的connected app, 以及该连接需要的apex class(暴露具体业务api 的apex class), object(具体查询获取的数据对象),的权限之类的,以及对应object 可访问的字段之类的,
这样可以做到最大程度地限制该连接用户所请求的内容在授权的范围之内, 从而能有效地防止接口数据被滥用或者权限被开放过大;
创建一个用户来作为登录连接connected app 的user.
该user 必须设置为指定创建的profile.
编码实现:
根据需要创建相应的 apex class, 头部必须标注:
@RestResource(urlMapping='/api/v1.0/salary/*') global with sharing class TSP_SalaryRestApi { //… … }
头部的@RestResource 相对应的Url Mapping 为必须设置的项; 否则无法正确映射请求路径;
Get 方法:
@HttpGet global static void getSalaryByEmployeeIdApi() { //根据需要调用内部私有方法,获取相应的业务数据并返回… … }
需要在头部标注 @HttpGet
Post 方法:
@HttpPost global static void getSalaryListApi() { //根据需要调用内部私有方法,获取相应的业务数据并返回… …
}
需要在头部标注 @HttpPost
如何获取请求参数:
RestResponse res = RestContext.response; res.addHeader('Content-Type', 'application/json');// 添加返回格式限制为json RestRequest req = RestContext.request;// 获取request body 中的请求参数 TSP_GetSalaryReqVO reqBody = (TSP_GetSalaryReqVO)JSON.deserialize(req.requestBody.toString(), TSP_GetSalaryReqVO.class);
//TSP_GetSalaryReqVO 为自定义的请求参数数据模型,可以进行json 泛解析,最好加上异常处理防止参数异常导致解析错误
System.debug('this is the request body:>>>>> ' + reqBody);
List<Salary__c> resList = getSalaryList(reqBody); // 通过内部方法获取数据结果
建议通过 RestRequest 获取requestBody 然后通过反解析将获取的json 解析成相应的模型,通过requestModel 获取需要的请求参数;
如何处理返回结果:
大家先看看这个方法? 觉得有没有什么问题呢?
@HttpPost global static string getSalaryListApiStringReturn(){ RestResponse res = RestContext.response; res.addHeader('Content-Type', 'application/json'); RestRequest req = RestContext.request; TSP_GetSalaryReqVO reqBody = (TSP_GetSalaryReqVO)JSON.deserialize(req.requestBody.toString(), TSP_GetSalaryReqVO.class); System.debug('this is the request body:>>>>> ' + reqBody); List<Salary__c> resList = getSalaryList(reqBody); if(resList != null){ return JSON.serialize(resList); // 直接进行json 序列化,并返回序列化后的string }else { return '{"error_message":"No data found","error_code":"-1"}'; // 返回自定义的异常错误结果string }
}
很明显, 返回结果为string 类型的, 虽然进行了json 序列化,但responseBody返回的本质内容还是 string,
返回结果如下:
"[{"attributes":{"type":"Salary__c","url":"/services/data/v50.0/sobjects/Salary__c/a007F00000BOCRDQA5"},"Id":"a007F00000BOCRDQA5","Salary_Code__c":"MLFSLC-0002","Income_Type_Reference__c":"Normal_Pay","SalaryAmount__c":12000.00,"Salary_Level__c":"COO","Active__c":true,"Start_Date__c":"2017-01-01","End_Date__c":"2020-12-31"}]"
由于定义的返回类型为 string, 所以导致最终返回的responseBody 并非标准的json 格式, 而是json 格式的字符串转移后的字符串输出,简言之, 输出的还是string 而非json. 这可能与接收方需要的结果不同。
/*
此处有人会说,为了程序便利,可以直接return string 然后接收方可以很方便的通过返回的string进行处理,
话虽如此,但这种方法在有些时候还是会有问题,带项目上线你自己品就知道弊端在哪儿了,调用方的消费者可能会在背后骂你埋下的祸根... 而且还有一些特殊的字符,如果编码没有处理好,可能会有意想不到的结果,尤其对于处理多语言的内容。.
*/
各位看官, 先独自思考3-5 分钟,想想该怎么改?
怎么改, 怎么改?。。。。。。
怎么改, 怎么改?。。。。。。
好了, 不卖关子了,那咱们来看看具体正解的返回方式应该是怎样的呢?
上代码:
@HttpPost global static void getSalaryListApi() { RestResponse res = RestContext.response; res.addHeader('Content-Type', 'application/json'); RestRequest req = RestContext.request; TSP_GetSalaryReqVO reqBody = (TSP_GetSalaryReqVO)JSON.deserialize(req.requestBody.toString(), TSP_GetSalaryReqVO.class); System.debug('this is the request body:>>>>> ' + reqBody); List<Salary__c> resList = getSalaryList(reqBody); if(resList != null){ res.responseBody = Blob.valueOf(JSON.serialize(resList)); }else { res.responseBody = Blob.valueOf('{"error_message":"No data found","error_code":"-1"}'); } }
可能这乍一看, 没啥区别啊? 你品, 你再细品。。。。
首先要将获取的request body 发序列化,转换为指定的 Model , 并获取相应的请求参数,
拿请求参数做程序处理,或者数据查询,经过业务逻辑和请求参数后,获取查询结果的数据并返回数据,然后将获取的数据结果 json 序列化,并且用 Blob.Valueof(json string) 填充到responseBody 中,
这样实际上所有的result 都被存进了 responseBody 中了, 你会发现根本没有了return?
原来定义的方法返回结果 也是void 类型??
WHY?
这里没有直接return string 之说了,因为定义返回类型的时候, 用的就是 void 而非string, 这就是这两种方法最大的区别;
实质上,时将返回结果已经存进了responseBody 里面了,而没有返回方法的结果。
可以看一下返回结果:
[ { "attributes": { "type": "Salary__c", "url": "/services/data/v50.0/sobjects/Salary__c/a007F00000BOCR8QAP" }, "Id": "a007F00000BOCR8QAP", "Salary_Code__c": "MLFSLC-0001", "Income_Type_Reference__c": "Normal_Pay", "SalaryAmount__c": 5000, "Salary_Level__c": "Normal_Staff", "Active__c": true, "Start_Date__c": "2017-01-01", "End_Date__c": "2020-12-31" }, { "attributes": { "type": "Salary__c", "url": "/services/data/v50.0/sobjects/Salary__c/a007F00000GZuP9QAL" }, "Id": "a007F00000GZuP9QAL", "Salary_Code__c": "MLFSLC-0003", "Income_Type_Reference__c": "Bonus_Project", "SalaryAmount__c": 888.08, "Salary_Level__c": "Normal_Staff", "Active__c": true, "Start_Date__c": "2018-02-09", "End_Date__c": "2018-10-31" } ]
Or return as error:
{ "error_message": "No data found", "error_code": "-1" }
都是标准的json 格式 内容,而非string。
Token 获取:
另外, 再说一下需要请求的token 以及业务Api服务的参数, :
请求的连接格式基本比较固定,除了域名有差异之外, 后面部分都是一样的,可以直接套用,
这里说一下 grant_type 这个参数, 这里用的是password, 也有其他的方式,大家可以参考一下 Oauth2.0 的授权方式。
{ "access_token": "00D7F0000048IGHXXXXXXXXXXXXXXXXXX_OInU18cLuQ8BXxXXxxXxxbiifPASqZeheIm6KqluXXXXXXXXX_2fGlXXXX", "instance_url": "https://xxxx0x-xx-ed.my.salesforce.com", "id": "https://login.salesforce.com/id/00D7F00000X0x0x0XX0x/0057F00000X0X0X0X0", "token_type": "Bearer", "issued_at": "16086204000000", "signature": "xxxxFZl+h9Tzxxxxcm0dphNzrxmmxxxxxx/4nSgw=" }
然后使用获取的access_token, 来访问业务API;
requestBody 中的请求参数按照具体业务定义来填充即可。
{ "rowNumber": 100, "incomeType":"", "isActive":true, "salaryType":"" }
有了这些, 就可以大功告成了。
Tips :常见问题:
Q1, 请求token 返回400/401,
- 查看是否配置正确的账号/密码, 最好从web 登录一下试试看;
- 检查请求的Url 格式是否正确,web直接输入查看是否可达;
- 查看请求的参数是否都满足条件,或者请示方式是否对;
Q2, invalid_session_id
- 查看access_token 是否超期,
- 请求的目标业务URL service 地址是否正确;
- 请求方式是否再业务 代码中定义了比如 Get Post, Put.
Q3 目标业务地址不可达
- 查看profile中对应的app user 是否设置了IP 登录限制;
- 检查请求URL大小写问题, 是否与RESTResource URL代码中定义的是否相同,
Q4, 查询结果显示无权限
- 查看设置的profile 是否开放了代码中查询用到的apex class, object ,
- 查询数据object的相应字段是否授权,对于object read/edit/delete 权限是否都已经放开相应权限;