接口鉴权

API网关会在请求的HTTP HEAD中增加字段 X-Jeata-Api-Proxy-Meta 包含了用户身份、组织、项目和校验签名。为了安全您至少应该检查签名和时间戳(±30秒)

X-Jeata-Api-Proxy-Meta 的值使用URL键值对的格式(即key1=value1&key2=value2…)

GET /api-01 HTTP/1.1
Host: server.example.com
X-Jeata-Api-Proxy-Meta: user=c09247ec02edce69f6625a2d&email=zhangsan@example.com&org=g-0001&project=pr-1&page=p-1&api=5fdb3af7b2e9c1284ad5b0d0&issue=master&client_ip=116.66.88.9&timestamp=1590940800&nonce=CvJrba2F8V5Aq073&sign=0f2c65a9208ff8ff11a2fed281acb260633177662f951cd299ac6fc76b99af7f

...

参数说明

参数 说明
user 用户标识。在基塔后台的全局唯一标识
email 用户邮箱
org 组织别名。如:g-0001
project 项目别名。如:pr-1
page 页面别名。如:p-1
api API标识
issue 页面的版本。master: 已发布页面; draft: 草稿
client_ip 客户端IP
timestamp 当前时间戳,单位秒
nonce 随机字符串
sign 签名字符串

签名规则

除 sign 外的参数,按照参数名排序后生成URL键值对格式字符串key1=value1&key2=value2&……&secret=**** 经过sha256加密后与sign值进行对比。

注意事项:

  1. sign 不参与计算,仅用于与计算值比较
  2. 参数名需要从小到大排序(ASCII正序)
  3. secret参数放在最后,不参与排序
  4. 参数名区分大小写
  5. 如果参数的值为空不参与签名
  6. 参数名和值都不需要再做编码
  7. 以后可能增加字段,验证签名时必须支持增加的扩展字段(不能按照固定顺序拼字符串)
// 对参数按照key=value的格式,并按照参数名ASCII字典序排序如下:
stringA = "api=5fdb3af7b2e9c1284ad5b0d0&client_ip=116.66.88.9&email=zhangsan@example.com&issue=master&nonce=CvJrba2F8V5Aq073&org=g-0001&page=p-1&project=pr-1&timestamp=1590940800&user=c09247ec02edce69f6625a2d"

// 拼接 secret
stringSignTemp=stringA+"&secret=aB72I7NrLAys5AM7"

// 使用SHA-256算法签名
// 提示:使用上面的stringA计算的verifySign为:0f2c65a9208ff8ff11a2fed281acb260633177662f951cd299ac6fc76b99af7f
verifySign=SHA256(stringSignTemp).toLowerCase()

//  比较计算的verifySign 与 sign参数是否一致
ok = verifySign == sign

开发样例

我们提供了一些语言的样例用于参考 GoLang、Java、NodeJS、Python、PHP

GoLang

// 项目Secret
const ProjectSecret = "aB72I7NrLAys5AM7"

// 检查 jeata签名
func JeataCheckSign(req *http.Request) error {
    meta := req.Header.Get("X-Jeata-Api-Proxy-Meta")
    if meta == "" {
        return errors.New("非Jeata请求")
    }

    // 解析参数
    param, err := url.ParseQuery(meta)
    if err != nil {
        return err
    }

    // 检查时间戳,误差超过±30秒时拒绝请求
    timestamp, _ := strconv.ParseInt(param.Get("timestamp"), 10, 0)
    nowUnix := time.Now().Unix()
    if timestamp < nowUnix-30 || timestamp > nowUnix+30 {
        return errors.New("请求已过期或服务器时间误差太大")
    }

    // 检查签名
    sign := param.Get("sign")
    verifySign := jeataGenerateSign(param, ProjectSecret)
    if sign != verifySign {
        return errors.New("签名错误")
    }

    // 签名正确
    return nil
}

// 生成签名
func jeataGenerateSign(param url.Values, secret string) (sign string) {
    pList := make([]string, 0, len(param)+1)
    for key := range param {
        var val = param.Get(key)
        // 空值和sign不参与计算
        if val != "" && key != "sign" {
            pList = append(pList, key+"="+val)
        }
    }

    // 排序
    sort.Strings(pList)
    // 在最后拼接 secret
    pList = append(pList, "secret="+secret)

    // SHA-256 签名
    src := strings.Join(pList, "&")
    signBytes := sha256.Sum256([]byte(src))
    return fmt.Sprintf("%x", signBytes)
}

Java

public class SignToolExample {
    // 项目Secret
    public final static String ProjectSecret = "aB72I7NrLAys5AM7";

    // 检查 jeata签名(错误则抛出异常)
    public static void JeataCheckSign(HttpServletRequest req) throws Exception {
        String meta = req.getHeader("X-Jeata-Api-Proxy-Meta");
        if (meta == null || meta.length() == 0) {
            throw new Exception("非Jeata请求");
        }

        // 解析参数
        Map<String, String> param = new HashMap<>();
        for (String kv : meta.split("&")) {
            String[] kvPair = kv.split("=", 2);
            String k = URLDecoder.decode(kvPair[0], "UTF-8");
            String v = kvPair.length > 1 ? URLDecoder.decode(kvPair[1], "UTF-8") : "";
            param.put(k, v);
        }

        // 检查时间戳,误差超过±30秒时拒绝请求
        long timestamp = Long.parseLong(param.get("timestamp"));
        long nowUnix = System.currentTimeMillis() / 1000;
        if (timestamp < nowUnix - 30 || timestamp > nowUnix + 30) {
            throw new Exception("请求已过期或服务器时间误差太大");
        }

        // 检查签名
        String sign = param.get("sign");
        String verifySign = jeataGenerateSign(param, ProjectSecret);
        if (sign == null || !sign.equals(verifySign)) {
            throw new Exception("签名错误");
        }

        // 签名正确
    }

    // 生成签名
    private static String jeataGenerateSign(Map<String, String> param, String secret) throws Exception {
        List<String> pList = new ArrayList<>();
        for (Map.Entry<String, String> kv: param.entrySet()) {
            String k = kv.getKey(), v = kv.getValue();
            // 空值和sign不参与计算
            if (v != null && v.length() != 0 && !k.equals("sign")) {
                pList.add(k+"="+v);
            }
        }

        // 排序
        pList.sort(null);
        // 在最后拼接 secret
        pList.add("secret="+secret);

        // SHA-256 签名
        String src = String.join("&", pList);
        MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");
        messageDigest.update(src.getBytes(StandardCharsets.UTF_8));

        byte[] hash = messageDigest.digest();
        return bytes2Hex(hash);
    }

    private static String bytes2Hex(byte[] bytes) {
        StringBuilder sb = new StringBuilder();
        for (byte x: bytes) {
            int xInt = ((int) x) & 0xff;
            sb.append(String.format("%02x", xInt));
        }

        return sb.toString();
    }
}

NodeJS

const querystring = require("querystring");
const crypto = require('crypto');

// 项目Secret
const ProjectSecret = "aB72I7NrLAys5AM7"

/**
 * 检查jeata签名
 */
function JeataCheckSign(req, res, next) {
  let meta = req.header('X-Jeata-Api-Proxy-Meta');
  if(!meta) {
    res.json({status:1, msg: "非Jeata请求"});
    return;
  }

  // 解析参数
  let param = querystring.parse(meta);

  // 检查时间戳,误差超过±30秒时拒绝请求
  let timestamp = parseInt(param.timestamp);
  let nowUnix = Math.floor(Date.now().valueOf() / 1000);
  if (!timestamp || timestamp < nowUnix-30 || timestamp > nowUnix+30) {
    res.json({status:1, msg: "请求已过期或服务器时间误差太大"});
    return;
  }

  // 检查签名
  let verifySign = jeataGenerateSign(param, ProjectSecret)
  if (!param.sign || param.sign != verifySign) {
    res.json({status:1, msg: "签名校验失败"});
    return;
  }

  // 签名正确
  // 正常业务流程
  // ...
  res.json({status:0, msg: "", data:{username: "zhangsan"}});
}

/**
 * 生成签名
 * @param param 参数
 * @param secret 项目秘钥
 */
function jeataGenerateSign(param, secret) {
  // 空值和sign不参与计算
  let src = Object.keys(param).filter(key => key !== 'sign' && param[key] !== void 0 && param[key] !== '')
      .sort()
      .map(key => key + '=' + param[key])
      .join('&');

  // 在最后拼接 secret
  src += "&secret=" + secret;

  // SHA-256 签名
  return crypto.createHash('sha256')
      .update(src)
      .digest('hex');
}

Python

# -*- coding:utf-8 -*-
import hashlib
import time
from urllib.parse import parse_qsl

# 项目Secret
ProjectSecret = "aB72I7NrLAys5AM7"


# 检查jeata签名
# 成功返回 True
# 错误时返回 异常
def JeataCheckSign(req):
    # 从HTTP Head中读取,请根据所用web框架修改
    meta = req.headers['X-Jeata-Api-Proxy-Meta']
    if not meta:
        raise Exception("非Jeata请求")

    param = dict(parse_qsl(meta))

    # 检查时间戳,误差超过±30秒时拒绝请求
    if 'timestamp' not in param or 'sign' not in param:
        raise Exception("格式错误")
    try:
        timestamp = int(param['timestamp'])
    except:
        timestamp = 0
    nowUnix = int(time.time())
    if timestamp < nowUnix - 30 or timestamp > nowUnix + 30:
        raise Exception("请求已过期或服务器时间误差太大")

    # 检查签名
    verifySign = jeataGenerateSign(param, ProjectSecret)
    if verifySign != param['sign']:
        raise Exception("签名校验失败")

    # 签名正确
    return True


# 生成签名
def jeataGenerateSign(param, secret):
    # 排序、过滤空值、sign不参与计算
    raw = [(k, str(param[k])) for k in sorted(param.keys()) if k != 'sign' and param[k]]
    s = "&".join(["=".join(kv) for kv in raw])
    # 在最后拼 secret
    s += "&secret={0}".format(secret)
    # SHA-256 签名
    return hashlib.sha256(s.encode("utf-8")).hexdigest()

PHP

$ProjectSecret = "aB72I7NrLAys5AM7";

/**
 * 检查jeata签名
 * 成功返回 True
 * 错误时返回 异常
 */
function JeataCheckSign(){
    global $ProjectSecret;

    $meta = $_SERVER['X-Jeata-Api-Proxy-Meta'];
    if(!$meta) {
        throw new Exception("非Jeata请求");
    }

    // 解析参数
    parse_str($meta, $param);

    // 检查时间戳
    $timestamp = intval($param["timestamp"]);
    $nowUnix = time();
    if ($timestamp < $nowUnix - 30 || $timestamp > $nowUnix + 30) {
        throw new Exception("请求已过期或服务器时间误差太大");
    }

    # 检查签名
    $verifySign = jeataGenerateSign($param, $ProjectSecret);
    if ($verifySign != $param['sign']) {
        throw new Exception("签名校验失败");
    }

    echo "校验通过";
    return True;
}

/**
 * 生成签名
 */
function jeataGenerateSign($param, $secret) {
    // 按字典序排序参数
    ksort($param);

    // 生成名值对字符串,并去掉空值和sign参数
    $string = toUrlParams($param);

    //在最后拼 secret
    $string = $string . "&secret=".$secret;

    // SHA-256 签名
    $result = hash("sha256", $string);
    return $result;
}

/**
 * 格式化参数格式化成url参数
 * 空置和sign不参与
 */
function toUrlParams($param){
    $buff = "";
    foreach ($param as $k => $v)
    {
        if($k != "sign" && $v != "" && !is_array($v)){
            $buff .= $k . "=" . $v . "&";
        }
    }

    $buff = trim($buff, "&");
    return $buff;
}
更新时间: 2021-10-20 08:19