接口鉴权
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×tamp=1590940800&nonce=CvJrba2F8V5Aq073&sign=0f2c65a9208ff8ff11a2fed281acb260633177662f951cd299ac6fc76b99af7f
...
参数说明
参数 | 说明 |
---|---|
user | 用户标识。在基塔后台的全局唯一标识 |
用户邮箱 | |
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值进行对比。
注意事项:
- sign 不参与计算,仅用于与计算值比较
- 参数名需要从小到大排序(ASCII正序)
- secret参数放在最后,不参与排序
- 参数名区分大小写
- 如果参数的值为空不参与签名
- 参数名和值都不需要再做编码
- 以后可能增加字段,验证签名时必须支持增加的扩展字段(不能按照固定顺序拼字符串)
// 对参数按照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×tamp=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