我在调用第三方和为第三方提供接口时的流程及常见问题的解决方案

2023-11-14 18:53:33 浏览数 (2)

最近在忙和第三方厂商的接口对接,正好趁热打铁,梳理下我在调用第三方和为第三方提供接口时的流程及常见问题的解决方案,事不宜迟,我们直接开始!

确定接口文档

在和第三方沟通确定开发方案和大体思路后就可以开始出接口文档了。接口文档中一般需要包括接口认证方式、认证方式对应加密算法介绍、接口基础路径(baseUrl)、数据流向;接口包括接口功能简述、请求方式(POST/GET)、接口url请求地址、header请求头、请求参数字段说明及参数类型(包括参数、含义、数据类型、是否必填、其他说明)、成功响应返回参数字段说明、失败状态码及说明。我出了一个接口文档模版的md格式,大家可以在公众号后台留言“接口文档”获取。

确定接口认证方式

由于系统中的API会暴露在互联网上,你的接口将遭遇所有人可以调用的风险,那么就需要验证当前发起请求的人是否你是允许请求的人。首先可以增加些必要的安全措施,如对传递的密码等关键字段进行MD5加密,以确保密码等敏感数据不会明文传递。

可以采用Hmac接口认证方式,平台提供了appId和appSecret,它们一一对应而且可以作为唯一标识,然后根据HmacSHA算法计算出加密信息,这个认证接口中除了传入appId、appSecret,还需要传递当前时间戳,以供服务器辨识非法请求以及加强生成Authorization的唯一性。

另外还可以使用token机制,token是由服务器端根据特定规则生成的一串加密字符串下发给客户端,客户端在请求服务端所有资源时都会携带上这个 Token(一般设置在 header 中)。服务端来校验这个 token 的合法性,我以JWT token为例大致展示下token的生成和验证。

代码语言:javascript复制
const JWTSECRET = "yourJWTSecret" // 第三方用户JWT密钥
var TOKENEXPIRETIME = time.Hour * 5 // 第三方用户授权过期时间,如:5小时
// 生成JWT token
func generateToken(userName string) (string, error) {
    // 创建一个新的 JWT token
    token := jwt.New(jwt.SigningMethodHS256)

    // 设置 token 的声明(payload)
    claims := token.Claims.(jwt.MapClaims)
    claims["userName"] = userName
    claims["exp"] = time.Now().Add(TOKENEXPIRETIME).Unix() // 设置 token 的过期时间

    // 使用密钥对 token 进行签名
    tokenString, err := token.SignedString([]byte(JWTSECRET))
    if err != nil {
        log.Println("Failed to generate token:", err)
        return "", err
    }
    // 将 token 存储到 Redis 中
    err = conf.RedisClient.Set("jwt_token:" userName, tokenString, time.Hour*5).Err()
    if err != nil {
        return "", fmt.Errorf("failed to store token in Redis: %v", err)
    }
    ...
    return tokenString, nil
}

JWT由三部分组成:header、payload、signature。其中头部为{'typ': 'JWT', 'alg': 'HS256'}, payload中存放有效信息,如jwt过期时间、业务需要的信息(不建议放敏感信息),signature为base64加密后的header和base64加密后的payload连接组成的字符串(头部在前),然后通过header中声明的加密方式进行加盐secret组合加密。

这里我将生成的JWT token及对应的用户信息存储到了Redis中,并且设置对redis的键值对设置了过期时间。在验证时可以拿到用户名及对应的token来判断是否通过认证,如果token过期则会自动刷新重新生成。

代码语言:javascript复制
// 验证Token
func ValidateToken(tokenString string) bool {
    // 解析 JWT token
    token, _ := jwt.Parse(tokenString, func(jwtToken *jwt.Token) (interface{}, error) {
         // 验证签名算法是否为 HS256
        if _, ok := jwtToken.Method.(*jwt.SigningMethodHMAC); !ok {
            return false, fmt.Errorf("unexpected signing method: %v", jwtToken.Header["alg"])
        }
         // 返回用于验证签名的密钥
         return []byte(JWTSECRET), nil
    })
    // 验证 token 是否有效
    if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
         // 获取用户名
         userName := claims["userName"].(string)
         // 从 Redis 中获取存储的 token
         storedToken, err := conf.RedisClient.Get("jwt_token:"   userName).Result()
         log.Println("storedToken=", storedToken)
         if err != nil {
              return false
         }
         // 验证 token 是否与存储的 token 一致
         if tokenString != storedToken {
              return false
         }
         // 检查 token 是否过期, 如果过期重新生成
         expirationTime := time.Unix(int64(claims["exp"].(float64)), 0)
         if time.Now().After(expirationTime) {
              // 重新生成 token
              newToken, err := generateToken(userName)
              if err != nil {
                  return false
              }
              // 更新 Redis 中存储的 token
              err = conf.RedisClient.Set("jwt_token:" userName, newToken, time.Hour*24*7).Err()
              if err != nil {
                  return false
              }
          }
          return true
      } else {
          return false
      }
}

确定token过期时间

上面JWT验证部分我们提到了在token过期时要自动重新生成,为什么要自动续期呢?首先我们需要根据具体业务情况确定token过期时间,JWT设置了过期时间之后,一旦超时,所有接口就无法访问了,需要用户重新登录进行认证才能重新拿到token,但是这样会影响到业务正常运转。一般情况下都会设置自动续期,特定条件下才会让用户重新登录。

双方用户推送及授权

可能在对接三方时对方需要将用户信息推送到我们的平台上,那么就要为三方提供用户推送的接口,包括了用户的增删改功能。这里要注意第三方推送过来的用户要和系统中原本用户做以区分,如增加外部用户标识字段,区分后以便对外部用户进行特殊授权,例如针对不同用户对不同接口请求权限的限制,可以基于当前用户系统设置一个简单的RBAC来为用户分配角色,并对不同角色的用户授予不同接口的权限。

特殊登录方式请求转发

在你的开发过程中这个环节不一定涉及到,我这边的业务需求是三方接入的用户是需要通过他们提供的人脸识别接口进行登录的,使用到的用户也是先前推送给我们的这部分用户。由客户端将识别的人脸base64图片,以及在我们系统上登录的token,一并发到服务端,由服务端拿着Authorization验证token访问三方提供的人脸识别接口,三方返回的人脸匹配结果及匹配得分经服务端处理后返回到客户端。

服务端处理代码逻辑示例如下:

代码语言:javascript复制
func (U *UserServer) FaceRecognition(ctx context.Context, req *pb.FaceRecognitionReq) (resp *pb.FaceRecognitionResp, err error) {
      cuser, err := U.Auth(ctx)
      if err != nil {
          return nil, errors.New("用户验证未通过")
      }
      userInfo, err := new(models.User).View(cuser.UserId)
      url := BaseURL   "/faceRecognition" // 三方人脸识别url
      // 构造人脸识别请求
      reqBody := map[string]interface{}{
          "image":  req.Img,
          "userId": userInfo.UserId,
      }
      // 将请求体转换为JSON格式
      reqBodyJSON, err := json.Marshal(reqBody)
      if err != nil {
          return nil, err
      }

      httpReq, err := http.NewRequest(http.MethodPost, url, bytes.NewBuffer(reqBodyJSON))
      if err != nil {
          return nil, err
      }
      // 获取三方token
      authorization := ExternalLogin(userInfo.UserId)
      // 设置HTTP请求的头部
      httpReq.Header.Set("Authorization", authorization)
      httpReq.Header.Set("Content-Type", "application/json") // 规定为JSON格式数据
      // 发送HTTP人脸识别请求
      httpClient := http.DefaultClient
      httpResp, err := httpClient.Do(httpReq)
      if err != nil {
          return nil, err
      }
      defer httpResp.Body.Close()
      // 解析HTTP响应
      respBody, err := ioutil.ReadAll(httpResp.Body)
      if err != nil {
          return nil, err
      }
      // 将响应的数据转换为字符串
      respStr := string(respBody)
      // 解析HTTP响应的数据
      var respData struct {
          Code int    `json:"code"`
          Msg  string `json:"msg"`
          Data struct {
              DetectFaceRet bool   `json:"detectFaceRet"`
              Reason        string `json:"reason"`
          } `json:"data"`
      }

      err = json.Unmarshal([]byte(respStr), &respData)
      if err != nil {
          return nil, err
      }
      // 构造RPC响应
      resp = &pb.FaceRecognitionResp{
          DetectFaceRet: respData.Data.DetectFaceRet,
          Reason:        respData.Data.Reason,
      }
      return resp, nil
}

常见问题及解决方案

接口返回400

这个问题奇怪的很,用postman可以请求成功,但用接口请求对方接口却返回400,然后自己又起了个go http服务器接收,也能收到。一时陷入困惑的我求助了一位热心老哥。

我按照老哥的方法比对了我的访问请求和postman的请求,看似也是完全一致,直到我看到了url里自己埋下的一个坑。在postman中url中出现的双引号是会被编码的,但是我在代码中是这样写的:

代码语言:javascript复制
url := BaseURL   `/xxxx?reqID="`   randomString   `"`

这里的反引号表示字符串字面量,不支持任何转义序列。字面量 raw literal string 的意思是,你定义时写的啥样,它就啥样。所以这里的双引号没有被转义导致路径有误,所以返回了400错误[/捂脸]。

接口返回404

404指接口未找到,有可能接口名搞错了或者他们把这个服务下掉了,也有可能三方的网关最新的配置未更新,这个问题需要和三方对接人员确认。

接口返回500

大概率是对方接口里或者数据上的bug,也是需要和三方对接人员确认。

接口时好时坏

多是对方网络问题,或者三方平台在重启服务,这个问题也是要反馈给他们处理的。

接口返回为空

三方网络问题导致接口不可用,注意要处理这种情况导致的空接口问题,应在接口中增加初始化及判空处理,不然定时任务会将报错塞满你的日志。

确定固定字段传值

要确认接口文档中所有必填参数都已经传递过去,而且要确定哪些字段是需要固定值的,固定的值对方是否有修改,比如厂商的唯一标识,用户来源固定值等等。

token失效及redis缓存问题

如果token过期而且未及时重新获取或者未续期的情况下会导致token失效,token失效会使得接口认证不通过,无法使用;也有可能是token的过期时间与redis中设置的过期时间不一致。所以要注意处理token失效的error,及时请求认证接口重新获取一次token,并将新的token更新到redis中,统一设置过期时间。

关注阿巩不迷路!

0 人点赞