|
|
@@ -2,18 +2,27 @@ package com.fs.app.integration.client.advertiser;
|
|
|
|
|
|
import cn.hutool.core.util.StrUtil;
|
|
|
import cn.hutool.http.HttpRequest;
|
|
|
+import cn.hutool.http.HttpResponse;
|
|
|
import cn.hutool.json.JSONUtil;
|
|
|
-import com.fs.newAdv.enums.AdvertiserTypeEnum;
|
|
|
+import com.fs.ad.yk.utils.Md5Util;
|
|
|
import com.fs.app.integration.client.AbstractApiClient;
|
|
|
+import com.fs.app.integration.client.IAccessTokenClient;
|
|
|
+import com.fs.baidu.utils.AESUtils;
|
|
|
import com.fs.common.constant.SystemConstant;
|
|
|
import com.fs.common.exception.ThirdPartyException;
|
|
|
+import com.fs.newAdv.domain.PromotionAccount;
|
|
|
+import com.fs.newAdv.enums.AdvertiserTypeEnum;
|
|
|
+import com.fs.newAdv.service.IPromotionAccountService;
|
|
|
+import com.google.gson.Gson;
|
|
|
import lombok.extern.slf4j.Slf4j;
|
|
|
+import org.json.JSONObject;
|
|
|
+import org.springframework.beans.factory.annotation.Autowired;
|
|
|
import org.springframework.stereotype.Component;
|
|
|
|
|
|
-import java.util.ArrayList;
|
|
|
-import java.util.HashMap;
|
|
|
-import java.util.List;
|
|
|
-import java.util.Map;
|
|
|
+import javax.crypto.SecretKey;
|
|
|
+import javax.servlet.http.HttpServletRequest;
|
|
|
+import java.nio.charset.StandardCharsets;
|
|
|
+import java.util.*;
|
|
|
|
|
|
/**
|
|
|
* 百度营销API客户端
|
|
|
@@ -25,13 +34,26 @@ import java.util.Map;
|
|
|
*/
|
|
|
@Slf4j
|
|
|
@Component
|
|
|
-public class BaiduApiClient extends AbstractApiClient {
|
|
|
+public class BaiduApiClient extends AbstractApiClient implements IAccessTokenClient {
|
|
|
|
|
|
/**
|
|
|
* 百度OCPC转化回传API地址
|
|
|
*/
|
|
|
private static final String CONVERSION_API_URL = "https://ocpc.baidu.com/ocpcapi/api/uploadConvertData";
|
|
|
|
|
|
+
|
|
|
+ private static final String APPID = "appId";
|
|
|
+ private static final String AUTH_CODE = "authCode";
|
|
|
+ private static final String USERID = "userId";
|
|
|
+ private static final String TIMESTAMP = "timestamp";
|
|
|
+ private static final String SIGNATURE = "signature";
|
|
|
+ private static final String STATE = "state";
|
|
|
+ private static final String ACCESSTOKEN_URL = "https://u.baidu.com/oauth/accessToken";
|
|
|
+ private static final String AES_OFFSET = "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0";
|
|
|
+
|
|
|
+ @Autowired
|
|
|
+ private IPromotionAccountService promotionAccountService;
|
|
|
+
|
|
|
/**
|
|
|
* 回传转化数据到百度
|
|
|
*
|
|
|
@@ -50,6 +72,7 @@ public class BaiduApiClient extends AbstractApiClient {
|
|
|
.timeout(SystemConstant.API_TIMEOUT)
|
|
|
.execute();
|
|
|
});
|
|
|
+
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
@@ -104,5 +127,185 @@ public class BaiduApiClient extends AbstractApiClient {
|
|
|
public AdvertiserTypeEnum getAdvertiserType() {
|
|
|
return AdvertiserTypeEnum.BAIDU;
|
|
|
}
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public Map<String, String> getAccessToken(Integer code, String appId, String appSecret, String callbackUrl) {
|
|
|
+ return Collections.emptyMap();
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public Map<String, String> refreshAccessToken(String appId, String appSecret, String refreshToken) {
|
|
|
+ return Collections.emptyMap();
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public String getAccessTokenByAuthCode(HttpServletRequest request) {
|
|
|
+
|
|
|
+ // 获取请求参数
|
|
|
+ Map<String, String> params = new HashMap<>();
|
|
|
+ this.fillParams(params, request);
|
|
|
+ log.info("callback: params = {}", JSONObject.valueToString(params));
|
|
|
+ int userIdInt = 0;
|
|
|
+ try {
|
|
|
+ userIdInt = Integer.parseInt(params.get(USERID));
|
|
|
+ } catch (Exception e) {
|
|
|
+ return this.getResponseJson(600011, "参数错误", new Object());
|
|
|
+ }
|
|
|
+ // 对签名内容进行判空
|
|
|
+ if (StrUtil.isBlank(params.get(SIGNATURE))) {
|
|
|
+ return this.getResponseJson(600011, "参数错误", new Object());
|
|
|
+ }
|
|
|
+
|
|
|
+ // 获取应用对应的密钥
|
|
|
+ PromotionAccount app = promotionAccountService.getAppByAppId(params.get(APPID), AdvertiserTypeEnum.BAIDU);
|
|
|
+ String sk = app.getAppSecret();
|
|
|
+ // 开发者进行验签
|
|
|
+ // 检查状态码
|
|
|
+ if (!this.checkState(params.get(APPID), Long.valueOf(app.getAdAccountId()), params.get(STATE))) {
|
|
|
+ log.info("callback: state check fail");
|
|
|
+ return this.getResponseJson(600011, "状态码错误", new Object());
|
|
|
+ }
|
|
|
+
|
|
|
+ // 签名验证
|
|
|
+ if (!this.checkSignature(params, sk)) {
|
|
|
+ log.info("callback: signature check fail");
|
|
|
+ return this.getResponseJson(600011, "签名错误", new Object());
|
|
|
+ }
|
|
|
+
|
|
|
+ // 调用接口换取授权令牌
|
|
|
+ Map<String, Object> response = getBaiDuAccessToken(params, sk, userIdInt);
|
|
|
+ log.info("callback:getAccesstoken, response={}", response);
|
|
|
+ String accessToken = null;
|
|
|
+ String refreshToken = null;
|
|
|
+
|
|
|
+ if (!response.isEmpty()) {
|
|
|
+ JSONObject res = new JSONObject(response);
|
|
|
+ int code = (int) res.get("code");
|
|
|
+ if (code == 0) {
|
|
|
+ JSONObject data = res.getJSONObject("data");
|
|
|
+ accessToken = (String) data.get("accessToken");
|
|
|
+ refreshToken = (String) data.get("refreshToken");
|
|
|
+ app.setAccessToken(accessToken);
|
|
|
+ app.setRefreshToken(refreshToken);
|
|
|
+ promotionAccountService.updateById(app);
|
|
|
+ } else {
|
|
|
+ return this.getResponseJson(600011, "未获取到 access_token", new Object());
|
|
|
+ }
|
|
|
+ }
|
|
|
+ Map<String, String> data = new HashMap<>();
|
|
|
+ data.put("accessToken", accessToken);
|
|
|
+ return this.getResponseJson(0, "success", data);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 填充请求参数,开发者可用实体代替map
|
|
|
+ *
|
|
|
+ * @param params
|
|
|
+ * @param request
|
|
|
+ */
|
|
|
+ private void fillParams(Map<String, String> params, HttpServletRequest request) {
|
|
|
+ params.put(APPID, request.getParameter(APPID));
|
|
|
+ params.put(AUTH_CODE, request.getParameter(AUTH_CODE));
|
|
|
+ params.put(USERID, request.getParameter(USERID));
|
|
|
+ params.put(TIMESTAMP, request.getParameter(TIMESTAMP));
|
|
|
+ params.put(SIGNATURE, request.getParameter(SIGNATURE));
|
|
|
+ params.put(STATE, request.getParameter(STATE));
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 校验状态码是否符合预期
|
|
|
+ *
|
|
|
+ * @param appId 应用ID
|
|
|
+ * @param userId 创建应用的ID
|
|
|
+ * @param state 请求参数中的状态码
|
|
|
+ * @return
|
|
|
+ */
|
|
|
+ private boolean checkState(String appId, Long userId, String state) {
|
|
|
+ // md5util 开发者自行开发即可
|
|
|
+ String newState = Md5Util.MD5(appId.concat("_").concat(String.valueOf(userId)));
|
|
|
+ return newState.toLowerCase().equals(state);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 封装返回json串
|
|
|
+ *
|
|
|
+ * @param code
|
|
|
+ * @param message
|
|
|
+ * @param data
|
|
|
+ * @return
|
|
|
+ */
|
|
|
+ private String getResponseJson(int code, String message, Object data) {
|
|
|
+ // 封装返回字段的map
|
|
|
+ Map<String, Object> responseMap = new HashMap<>();
|
|
|
+ responseMap.put("code", code);
|
|
|
+ responseMap.put("message", message);
|
|
|
+ responseMap.put("data", data);
|
|
|
+ return JSONObject.valueToString(responseMap);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 签名验证方法
|
|
|
+ *
|
|
|
+ * @param params
|
|
|
+ * @param sk
|
|
|
+ * @return
|
|
|
+ */
|
|
|
+ private boolean checkSignature(Map<String, String> params, String sk) {
|
|
|
+ // 获取验签字符串:按key自然排序,拼接成 json 字符串,
|
|
|
+ // 示例:str1 = {"appId": xxxxx, "authCode": xxx, "state": xxx,"timestamp": xxx}
|
|
|
+ TreeMap<String, Object> map = new TreeMap<>();
|
|
|
+ map.put(APPID, params.get(APPID));
|
|
|
+ map.put(AUTH_CODE, params.get(AUTH_CODE));
|
|
|
+ map.put(USERID, params.get(USERID));
|
|
|
+ map.put(STATE, params.get(STATE));
|
|
|
+ map.put(TIMESTAMP, params.get(TIMESTAMP));
|
|
|
+ // 根据上述签名算法对接收到的参数签名
|
|
|
+ String sign = null;
|
|
|
+ try {
|
|
|
+ sign = paramsSign(sk, map);
|
|
|
+ } catch (Exception e) {
|
|
|
+ throw new RuntimeException(e);
|
|
|
+ }
|
|
|
+ log.info("callback: signature = {}", sign);
|
|
|
+ // 判断签名和URL传参签名是否一致
|
|
|
+ return params.get(SIGNATURE).equals(sign);
|
|
|
+ }
|
|
|
+
|
|
|
+ public String paramsSign(String secretKey, TreeMap<String, Object> map) throws Exception {
|
|
|
+
|
|
|
+ // 拼接成 json 字符串, 保证生成的json串字段顺序和Map中的顺序一致即可
|
|
|
+ String json = new Gson().toJson(map); // base64编码
|
|
|
+ byte[] bytes = Base64.getEncoder()
|
|
|
+ .encodeToString(json.getBytes(StandardCharsets.UTF_8))
|
|
|
+ .getBytes(StandardCharsets.UTF_8);
|
|
|
+ // AES加密 密钥为 应用sk 的前16位字符
|
|
|
+ SecretKey keyAES = AESUtils.loadKeyAES(secretKey.substring(0, 16));
|
|
|
+ return AESUtils.encryptAES(bytes, keyAES, AES_OFFSET);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 换取授权令牌
|
|
|
+ *
|
|
|
+ * @param params
|
|
|
+ * @param sk
|
|
|
+ * @param userIdInt 授权账户ID
|
|
|
+ * @return
|
|
|
+ */
|
|
|
+ private Map<String, Object> getBaiDuAccessToken(Map<String, String> params, String sk, int userIdInt) {
|
|
|
+ Map<String, Object> requestMap = new HashMap<>();
|
|
|
+ requestMap.put(APPID, params.get(APPID));
|
|
|
+ requestMap.put("secretKey", sk);
|
|
|
+ requestMap.put(AUTH_CODE, params.get(AUTH_CODE));
|
|
|
+ requestMap.put("grantType", "access_token");
|
|
|
+ requestMap.put("userId", userIdInt);
|
|
|
+ String paramsJson = JSONObject.valueToString(requestMap);
|
|
|
+
|
|
|
+ HttpResponse execute = HttpRequest.post(ACCESSTOKEN_URL)
|
|
|
+ .header("Content-Type", "application/json")
|
|
|
+ .body(paramsJson)
|
|
|
+ .timeout(SystemConstant.API_TIMEOUT)
|
|
|
+ .execute();
|
|
|
+ return JSONUtil.parseObj(execute.body());
|
|
|
+ }
|
|
|
}
|
|
|
|