侧边栏壁纸
博主头像
翻斗

开始一件事最好是昨天,其次是现在

  • 累计撰写 44 篇文章
  • 累计创建 42 个标签
  • 累计收到 2 条评论

跨平台API签名实战

翻斗
2023-04-23 / 0 评论 / 0 点赞 / 625 阅读 / 6,743 字

前言

这篇内容解释了一些API签名相关的知识,以及要做一个跨平台的API签名的技术设计,对于程序员来说,光有设计图/架构图可能还不够,说一千道一万还不如 “Show you the code” ,因此本篇文章开始来细致的做各种Coding,尽量用代码让大家更加理解跨平台API签名的用法。

回顾架构设计

这里有一个核心的库apisign-core,然后其他四个平台(Web/iOS/Android/Linux)下的对应代码都是使用了此代码的核心逻辑,本篇文章我们先来编写这个apisign-core。

关于Rust

从2015开始,在每年的Stack Overflow站点的年度开发者调查中,Rust一直都是程序员最喜爱的编程语言,因其设计重点在于安全性、速度和并发性而受到开发者社区的高度评价。它有着太多的优点了:

  1. 内存安全:Rust 通过其所有权模型来确保内存安全,无需垃圾收集器。这意味着在编译时就可以防止诸如空指针解引用和数据竞争等常见的内存错误。

  2. 类型系统:Rust 的类型系统旨在提供额外的编译时安全性,但不牺牲性能。它有助于在编译时捕获错误,如类型不匹配、生命周期问题等。

  3. 并发性:Rust 旨在使并发编程更安全和更容易。它的所有权和借用规则可以在编译时阻止数据竞争,这是并发程序中的一个常见问题。

  4. 性能:Rust 旨在与 C 和 C++ 等语言相媲美的性能。它没有运行时或垃圾收集器,因此可以用于需要最大化性能的系统级编程。

  5. 现代工具链:Rust 的包管理器和构建工具 Cargo,为依赖管理、项目构建和发布提供了极大的便利。

  6. 跨平台开发:Rust 支持跨平台开发,可以在多种操作系统上编译,包括 Linux、macOS、Windows 以及许多其他系统。

  7. 零成本抽象:Rust 的抽象不会引入运行时性能开销。这意味着你可以写高级的抽象,而不牺牲性能。

  8. 错误消息:Rust 的编译器错误信息非常有名地友好和有帮助,它提供了详细的错误描述和可能的解决方案,以帮助开发者理解和修复代码问题。

  9. 社区:Rust 拥有一个活跃和支持性的社区。它被认为是一个非常友好的社区,对新手友好。

  10. 持续发展:Rust 不断发展,每六周就会有一个稳定的新版本发布。社区和开发团队非常活跃,不断改进语言。

  11. 多范式编程:Rust 支持面向过程、函数式和面向对象的编程范式,使开发者可以选择最适合问题的工具。

讲道理,这么多优点的一个编程语言,任何人一看,这难道不应该每个人都赶紧学赶紧用吗?当然不是,Rust最让人头疼的就是它及其陡峭的学习曲线,他的一套新的所有权、引用、生命周期机制对新人而言简直劝退,相同的代码在其他语言上没问题,到Rust上就各种被编译器教做人
下面是一个国外比较有名的图:

另外,它的编译时间也很长,有这个时间,可能其他语言早就完成下一个功能了。

不过,在安全领域,在高性能领域,现在建议能用Rust还是就用Rust好了,安全高效有保障,而API签名正是这类领域。

思路设计

下面的均已图片中的请求为例子:

一般的签名流程:

1、准备各种数据

我们这里需要:

  • 请求方法method,例子中是POST
  • 请求路径url,例子中http://www.fandou.wang/getItem
  • 请求参数url params,例子中是id=123&region=456
  • 请求体body,例子中是一个json格式的字符串
  • 时间戳timestamp,需要提供方法get_timestamp
  • 随机数nonce,需要提供方法get_nonce
  • 秘钥idsecret_id,比如是123123123123

注意,因为我们是统一使用一个core的逻辑来做签名/验签,所以不用单独的知道一个secret_key,因为都是调用core中方法,由core来保管这个secret_key

2、构造待签名字符串

下面是伪代码

String timestamp = get_timestamp()
String nonce = get_nonce()
//给body先签名(一般是简易hash)
String signed_payload = get_signed_payload(body)
var data_map = {
    id:123,
    region:456,
    y_timestamp:timestamp,
    y_nonce:nonce,
    y_payload:signed_payload,
    y_secretid:"123123123123"
}
var data = json_str(data_map)

这里的data_map是用于生成签名的对象,其中包含了url params、时间戳、随机数、单独签名后的body(这里一般是简易的hash),然后转为json字符串,供后续步骤使用。

3、生成签名

需要提供方法get_sign来做签名
伪代码如下:

String method = "POST";
String url = "http://www.fandou.wang/getItem"
String signature = get_signature(method, url, data)

这里使用前面准备的method和url,加上一个签名算法get_signature来得到最终的签名signature

4、回填数据

生成的数据需要放到请求头部中,给后台做校验使用,包括:

  • 时间戳y_timestamp
  • 随机数y_nonce
  • 分配的签名秘钥id, 即y_secretid,比如为123123123123
  • y_signature: 上一步生成的signature

注意:中间数据signed_payload并没有放入头部中哦!

这里都统一加上了一个y_,方便与其他header区别开来,你也可以改为其他前缀,但是最好有一个,否则可能会有可能和某个已有的header冲突

5、服务器校验

服务器拿到请求信息后,按照1/2/3步骤重新计算一遍签名得到new_signature,和请求头中的y_signature进行比对,相同则通过,不同则认定签名失败,直接返回。

当然,我们还有时间戳和随机数,需要先进性这两个校验,具体内容我们到了网关/服务器校验的地方再细说,这里给个建议伪代码:

```java
if(!isValid(url, timestamp ,nonce)){
    //可能是重复请求或者其他非法请求,拦截
}

if(!new_signature.equalsTo(y_signature)){
    //非法请求,拦截
}

apisign-core需要提供的一些方法

根据前面的内容,我们需要提供以下方法:

  • get_timestamp
  • get_nonce
  • get_signed_payload
  • get_signature
use std::collections::HashMap;

mod sign;

pub fn get_timestamp() -> String {
    return sign::make_timestamp();
}

pub fn get_nonce() -> String {
    return sign::make_nonce();
}

pub fn get_signed_payload(body: &str) -> String {
    return sign::make_signed_payload(body);
}

pub fn get_signature(method: &str, url: &str, data: &str) -> String {
    return sign::sign(method, url, data);
}

pub fn get_signature_with_map(method: &str, url: &str, data: HashMap<String, String>) -> String {
    return sign::sign_with_map(method, url, data);
}

上面多提供了一个方法get_signature_with_map,因为如果接入方(比如我们的安全网关使用Rust)是Rust语言的话,直接传入map即可,其他语言需要传入String,使用get_signature方法。

下面是实现方法:

use base64::{engine::general_purpose, Engine};
use hmac::{Hmac, Mac};
use rand::prelude::*;
use serde_json::Value;
use sha2::Sha256;
use std::collections::{BTreeMap, HashMap};
use std::time::{SystemTime, UNIX_EPOCH};
use urlencoding;
use lazy_static::lazy_static;


lazy_static! {
    static ref KEY_MAP:HashMap<String, String> = {
        let mut map = HashMap::new();
        //这里是secret id/secret key
        map.insert("123123123123".to_string(),"4567845678".to_string());
        map.insert("abcdeabcde".to_string(),"ggghhhfffhll".to_string());
        map
    };
}

pub fn sign(method: &str, url: &str, data: &str) -> String {
    let mut params = BTreeMap::new();

    let v: Value = serde_json::from_str(data).unwrap();
    if let Some(obj) = v.as_object() {
        for (key , value) in obj.iter() {
            let new_value = serde_json::to_string(value).unwrap().trim_matches('"').to_string();

            let trimed_key = key.trim().to_string();

            params.insert(trimed_key, new_value);
        }
    }
    return sign_internal(method, url, params);
}

pub fn sign_with_map(method: &str, url: &str, data: HashMap<String, String>) -> String {
    let mut params = BTreeMap::new();
    for (k, v) in data.into_iter() {
        params.insert(k.trim().to_string(), v.trim().to_string());
    }
    return sign_internal(method, url, params);
}

fn sign_internal(method: &str, url: &str, params: BTreeMap<String, String>) -> String {
    let trimmed_url = url
        .replace("https://", "")
        .replace("http://", "")
        .replace("?", "");

    let secret_id = params.get("y_secretid");
    if secret_id == None {
        return "".to_string();
    }
    let secret_key = KEY_MAP.get(secret_id.unwrap());
    if secret_key == None {
        return "".to_string();
    }
    let sorted_params = sort_params(&params);
    // println!("apisign:sorted_params is {}", sorted_params);

    let content = method.to_uppercase() + trimmed_url.trim_end_matches("?") + "?" + &sorted_params;
    // println!("content is {}", &content);

    let signature = make_signature(secret_key.unwrap(), &content);
    // println!("signature is {}", signature);
    return signature;
}

pub fn make_timestamp() -> String {
    SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .unwrap()
        .as_millis()
        .to_string()
}

pub fn make_nonce() -> String {
    let mut rng = rand::thread_rng();
    // 随机5位数字
    let nonce = rng.gen_range(10000..=100000);
    nonce.to_string()
}

pub fn make_signed_payload(body: &str) -> String {
    let digest = md5::compute(body);
    let md5_result = format!("{:x}", digest);
    return base64_and_urlencode(md5_result.as_bytes());
}

fn base64_and_urlencode(content: &[u8]) -> String {
    let base64_body = general_purpose::STANDARD_NO_PAD.encode(content);
    let encoded_body = urlencoding::encode(&base64_body);
    return encoded_body.into_owned();
}

fn make_signature(secret_key: &str, content: &str) -> String {
    type HmacSha256 = Hmac<Sha256>;

    let mut mac = HmacSha256::new_from_slice(secret_key.as_bytes()).expect("key error");
    mac.update(content.as_bytes());
    let result = mac.finalize();
    let hex_result = format!("{:x}", result.into_bytes());
    return hex_result;
}

fn sort_params(params: &BTreeMap<String, String>) -> String {
    let mut sorted_params = BTreeMap::new();
    for (key, value) in params.iter() {
        sorted_params.insert(key.to_lowercase(), value.to_owned());
    }
    let mut query_string = String::new();
    for (key, value) in sorted_params.iter() {
        query_string.push_str(&format!("{}={}&", key, value));
    }
    query_string.trim_end_matches('&').to_owned()
}

总结

思路和核心代码都罗列在上面了,这里只是讲解了核心core的代码,后续如何在不同的平台上(WASM/iOS/Android)接入,我会补上对应的文章。

0

评论区