移动接口安全规范

2,627 阅读5分钟
原文链接: blog.loujiwei.cn

接口结构

接口一律采用https , 如:

https://api.xxx.cn/version/controller/action

请求参数

字段名 变量名 类型 描述
业务动作 action String(32) 操作, 如: add
业务内容 content String(32) 业务数据, json对象
控制模块 controller String(32) 业务模块, 如: user
随机字符串 nonceStr String(32) 随机字符串, 不长于32位。推荐随机数生成算法
签名 sign String(32) 签名,详见签名生成算法
签名类型 sign_type String(32) 签名类型, 如HMAC-SHA256或MD5, 默认为MD5
服务Id serviceId String(32) 用于服务器快速定位用户的临时数据, 在app第一次注册的时候获取

返回结果

字段名 变量名 类型 描述
业务动作 action String(32) 操作, 如: add
业务内容 content String(32) 业务数据, json对象
控制模块 controller String(32) 业务模块, 如: user
返回状态码 returnCode String(16) SUCCESS/FAIL 此字段是通信标识,非交易标识,交易是否成功需要查看result_code来判断
返回信息 returnMsg String(128) 返回信息,如非空,为错误原因签名失败参数格式校验错误

签名算法

第一步
将所有的数据(剔除签名字段), 按照key值的ASCII码从小到大排序, 直接输出成json字符串stringA.

  • 参数名ASCII码从小到大排序(字典序);
  • 参数名区分大小写;

第二步
在stringA的后面拼接和服务器约定的密钥, 使用密码. 得到stringATemp, 再对stringATemp进行MD5运算, 将得到的字符串全部转换成大写, 得到sign的值

假设待提交的数据:

{
    "action": "add",
    "nonceStr": "ibuaiVcKdpRxkhJA",
    "content": {
        "userName": "111",
        "passWord":"123" 
    },
    "controller": "user",
}

第一步之后得到字符串

stringA = {"action": "add","content": {"userName": "111","passWord":"123" },"controller": "user","nonceStr":"ibuaiVcKdpRxkhJA"}

第二步拼接key

stringATemp = stringA + password
sign=MD5(stringSignTemp).toUpperCase()="9A0A8659F005D6984697E2CA0A9CF3B7"

最终得到的发出去的数据为:

{
    "action": "add",
    "nonceStr": "ibuaiVcKdpRxkhJA",
    "content": {
        "userName": "111",
        "passWord":"123" 
    },
    "controller": "user",
    "sign": "9A0A8659F005D6984697E2CA0A9CF3B7"
}

key值的约定

key值的约定
由于服务器的密码存储一般是md5(password + salt) 记为A值, 但是在login的时候, 这个A值会在网络层中传输一次, 如果攻击者劫持了login接口, 那么就获取了key值,
所以建议在服务器表中除了A, 再多存储一个md5(password + salt2) 记为B,
这个B值作为客户的与服务器约定密码key值, 即传输报文的salt.

随机数

主要保证签名不可预测

java

public static String random() {
        return UUID.randomUUID().toString().replace("-", "");
}

oc

+ (NSString *)random {
    return [[[NSUUID UUID] UUIDString] stringByReplacingOccurrencesOfString:@"-" withString:@""];
}

实现

Android

实现算法的第一步和第二步 (使用fastjson, 可自动实现key值排序)

/**
 * 计算jsonObject的json值, 并添加key
 *
 * @param jsonObject
 * @param uuid, 和服务器约定的密钥, 可用password代替
 * @return
 */
public static String encode(JSONObject jsonObject, String uuid) {
    StringBuilder sb = new StringBuilder();
    sb.append(jsonObject.toJSONString());
    sb.append(uuid);
    String md5 = MD5.stringMD5(sb.toString());
    return md5;
}

调用

/**
    * 对发送的json加密
    *
    * @param jsonObject
    * @param uuid
    * @param serviceId
    * @return
    */
   public static JSONObject encryptJSON(JSONObject jsonObject, String uuid, String serviceId) {
       jsonObject.put("sign", encode(jsonObject, uuid));
       jsonObject.put("serviceId", serviceId);
       return jsonObject;
   }

校验服务器返回数据

/**
     * 检查服务器返回json是否正确
     *
     * @param jsonObject
     * @param uuid, 和服务器约定的密钥, 一般可用用户密码
     * @return
     */
    public static boolean checkEncrypt(JSONObject jsonObject, String uuid) {
        String checkNumService = jsonObject.getString("sign");
        jsonObject.remove("sign");
        return checkNumService != null && checkNumService.equals(encode(jsonObject, uuid));
    }

iOS

工具方法

// 获取随机数
+ (NSString *)random {
    return [[[NSUUID UUID] UUIDString] stringByReplacingOccurrencesOfString:@"-" withString:@""];
}
// MD5计算
+ (NSString *)md5:(NSString *)str {
    const char *cStr = [str UTF8String];//转换成utf-8
    unsigned char result[16];//开辟一个16字节(128位:md5加密出来就是128位/bit)的空间(一个字节=8字位=8个二进制数)
    CC_MD5(cStr, strlen(cStr), result);
    /*
        extern unsigned char *CC_MD5(const void *data, CC_LONG len, unsigned char *md)官方封装好的加密方法
        把cStr字符串转换成了32位的16进制数列(这个过程不可逆转) 存储到了result这个空间中
     */
    return [NSString stringWithFormat:@"%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X",
                                      result[0], result[1], result[2], result[3],
                                      result[4], result[5], result[6], result[7],
                                      result[8], result[9], result[10], result[11],
                                      result[12], result[13], result[14], result[15]
    ];
    /*
        x表示十六进制,%02X  意思是不足两位将用0补齐,如果多余两位则不影响
        NSLog("%02X", 0x888);  //888
        NSLog("%02X", 0x4); //04
     */
}
//字典转json格式字符串:
+ (NSString *)dictionaryToJson:(NSDictionary *)dic {
    NSError *parseError = nil;
    NSData *jsonData = [NSJSONSerialization dataWithJSONObject:dic options:NSJSONWritingPrettyPrinted error:&parseError];
    return [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
}

实现算法第一步和第二步

// 加密
+ (NSString *)encode:(NSMutableDictionary *)jsonObject uuid:(NSString *)uuid {
    jsonObject[NONCE_STR] = [self random];    //在原来的json里面添加随机字符串
    // 对字典输出json string 精选排序
    OrderedDictionary *orderedDictionary = [[OrderedDictionary alloc] initWithDictionary:jsonObject];
    NSString *json = [orderedDictionary toJSONString];
    NSString *jsonString = [json stringByAppendingString:uuid]; // 将json -> string
    NSLog(@"jsonString %@", jsonString);
    return [[self md5:jsonString] uppercaseString];   // -> md5 -> upper case
}

NSDictionary按属性排序导出json string
OrderedDictionary 代码地址

调用

// 对发送数据加密
+ (NSMutableDictionary *)encryptJSON:(NSMutableDictionary *)jsonObject uuid:(NSString *)uuid serviceId:(NSString *)serviceId {
    [jsonObject setObject:[self encode:jsonObject uuid:uuid] forKey:SIGN];
    [jsonObject setObject:serviceId forKey:SERVICE_ID];
    return jsonObject;
}

校验

//校验收到的数据是否被篡改
+ (BOOL)checkEncrypt:(NSMutableDictionary *)jsonObject uuid:(NSString *)uuid {
    NSString *sign = [jsonObject objectForKey:SIGN];
    [jsonObject removeObjectForKey:SIGN];
    return sign != nil && [sign isEqualToString:[self encode:jsonObject uuid:uuid]];
}

服务器

java 和Android一致, 只是不需要传serviceId

实现算法的第一步和第二步 (使用fastjson, 可自动实现key值排序)

/**
 * 计算jsonObject的json值, 并添加key
 *
 * @param jsonObject
 * @param uuid, 和服务器约定的密钥, 可用password代替
 * @return
 */
public static String encode(JSONObject jsonObject, String uuid) {
    StringBuilder sb = new StringBuilder();
    sb.append(jsonObject.toJSONString());
    sb.append(uuid);
    String md5 = MD5.stringMD5(sb.toString());
    return md5;
}

调用

/**
    * 对发送的json加密
    *
    * @param jsonObject
    * @param uuid
    * @param serviceId
    * @return
    */
   public static JSONObject encryptJSON(JSONObject jsonObject, String uuid) {
       jsonObject.put("sign", encode(jsonObject, uuid));
       return jsonObject;
   }

校验服务器返回数据

/**
     * 检查客户的提交json是否正确
     *
     * @param jsonObject
     * @param uuid, 和客户端约定的密钥, 一般可用用户密码
     * @return
     */
    public static boolean checkEncrypt(JSONObject jsonObject, String uuid) {
        String checkNumService = jsonObject.getString("sign");
        jsonObject.remove("sign");
        return checkNumService != null && checkNumService.equals(encode(jsonObject, uuid));
    }

说明

由于iOS的Dictionary的无序性, 这里提供一个自己实现的OrderedDictionary, 源码献上.