iOS 移动端接口加密流程(包含单点登录和失效时间)

iOS 移动端接口加密流程(包含单点登录和失效时间)

最近换了工作,新公司的一套面试题是关于移动端接口加密流程,当时根据面试题画出了大概的 UML 类图,但是还有很多考虑不周的情况。接触到代码后仔细研究看了一下数据请求逻辑,感觉还是有必要总结一下加深理解。

  • 为了数据安全,大多数没有使用 https 的公司都会选择 token 来提高数据请求的安全性,token 的生成方法也不尽相同,只要保证唯一性就行,最好是后台管理 token,由后台生成并返回前端保存。

上送参数

  • 所有接口都需要携带当前的的版本号和平台
中文说明 上送项 取值
平台 plat android 或者 iOS
版本号 ver 1.0
  • iOS端和安卓端分配不同的 app_id、private_key。每次通信是除了上述要上送的数据外,还要上送 timestamp、app_id、private_key、rand、sign。见下表:
字段名称 中文含义 说明
timestamp 用户端时间戳 前后台约定好时间格式就行
rand 随机数 16位随机数,包含数字和字母
app_id 应用 id 后台分配的给每个版本使用的,尽量每个版本更新时更换一下,后面说明用处
sign 数据签名 MD5(除sign 以外的所有文字上送项+ private_key+app_id+rand+timestamp+plat+ver 按照参数名的字母顺序排序,按参数名=值&拼接,结尾没有&)

如:iOS 端

app_id=170ib799dd511e7b66000163e033322   
  private_key = 170ib799dd511e7b66000163e033322
  • 业务数据为 phoneNo=123456&password=123456

  • 不包含 sign 的上送数据(data) 为:

app_id=170ib799dd511e7b66000163e033322&phoneNo=123456&password=123456&plat=iOS&rand=12dwfhdy487rSelsd&timestamp=2017-12-9-13:39:39:01&ver=1.0
  • sign =MD5(data+ private_key), 就是对一下字符串做MD5运算
app_id=170ib799dd511e7b66000163e033322&phoneNo=123456&password=123456&private_key=170ib799dd511e7b66000163e033322&plat=iOS&rand=12dwfhdy487rSelsd&timestamp=2017-12-9-13:39:39:01&ver=1.0
  • 计算出来的结果sign最终为32位数据和字母组合8357heukf384r83gh3fi3dwks42i993(示例)

最终上送data + sign 如:

app_id=170ib799dd511e7b66000163e033322&phoneNo=123456&password=123456&plat=iOS&rand=12dwfhdy487rSelsd&timestamp=2017-12-9-13:39:39:01&ver=1.0&sign= 8357heukf384r83gh3fi3dwks42i993

这是签名管理类.h代码

#import <Foundation/Foundation.h>

@interface TPBaseRequest : NSObject

@property (nonatomic, strong) NSString *plat;
@property (nonatomic, strong) NSString *ver;

- (NSDictionary *)baseParameters;
- (NSDictionary *)finalParametersFrom:(NSDictionary *)dic;

@end

这是签名管理类.m代码

#import "TPBaseRequest.h"
#import <CommonCrypto/CommonDigest.h>

@interface TPBaseRequest()

@property (nonatomic, strong)NSString *rand;

@property (nonatomic, copy) NSString *timeStr;

@end

@implementation TPBaseRequest

//最终上送为data(base + dic)+sign
- (NSDictionary *)finalParametersFrom:(NSDictionary *)dic
{
    self.rand = [self gainRand];
    NSMutableDictionary *finalDic = [NSMutableDictionary dictionaryWithDictionary:[self baseParameters]];
    [finalDic removeObjectForKey:@"private_key"];
    [finalDic addEntriesFromDictionary:dic];
    [finalDic setObject:[self gainSign:dic] forKey:@"sign"];
    [finalDic setObject:_timeStr forKey:@"timestamp"];
    return finalDic;
}
//基础参数
- (NSDictionary *)baseParameters
{
    //基础参数赋值
    NSMutableDictionary *para = [NSMutableDictionary dictionary];
    [para setObject:kApp_id forKey:@"app_id"];
    [para setObject:kPrivate_key forKey:@"private_key"];
    [para setObject:@"ios" forKey:@"plat"];
    [para setObject:[[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleShortVersionString"] forKey:@"ver"];
    [para setObject:self.rand forKey:@"rand"];
    [para setObject:[self gainTimestamp] forKey:@"timestamp"];
    
    if([MDAccountStore isLogined]){
        if(![MDAccountManager sharedInstance].currentAccount){
            [MDAccountManager sharedInstance].currentAccount = [[MDAccountStore sharedInstance] readTheAccount];
        }
    }
    //判断是否有用户登录 如果有携带上 userId
    if ([MDAccountManager sharedInstance].currentAccount.userId) {
        long userId = [MDAccountManager sharedInstance].currentAccount.userId;
        [para setObject:[NSNumber numberWithLong:userId] forKey:@"userId"];
    }
    //判断本地存储是否有 token
    if ([MDAccountManager sharedInstance].currentAccount.token) {
        [para setObject:[MDAccountManager sharedInstance].currentAccount.token forKey:@"token"];
    }
    
    return [NSDictionary dictionaryWithDictionary:para];
}
//获取随机数
- (NSString *)gainRand
{
    NSString *str = [NSString string];
    //16位
    for (int i = 0; i < 16; i++)
    {
        //随机出现字母、数字
        switch(arc4random() % 3) {
            case 0:
                str = [str stringByAppendingString:[NSString stringWithFormat:@"%d", arc4random() % 10]];
                break;
            case 1:
                str = [str stringByAppendingString:[NSString stringWithFormat:@"%c",  (arc4random() % 26) + 97]];
                break;
            case 2:
                str = [str stringByAppendingString:[NSString stringWithFormat:@"%c", (arc4random() % 26) + 65]];
                break;
        }
    }
    return str;
}
//获取时间字符串
- (NSString *)gainTimestamp
{
    NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
    dateFormatter.dateFormat = @"yyyy-MM-dd-HH:mm:ss";
    NSString *timeStr = [dateFormatter stringFromDate:[NSDate date]];
    //    NSString *stamp = [NSString stringWithFormat:@"%ld", (long)[[NSDate date] timeIntervalSince1970]];
    return timeStr;
}
//获取sign sign=md5(data+private_key)
- (NSString *)gainSign:(NSDictionary *)dic
{
    NSString *sign = [self getMd5_32Bit_String:[self gainData:dic]];
    return sign;
}
//获取data+private_key (private_key包含在base中)
- (NSString *)gainData:(NSDictionary *)dic
{
    //data = base + dic
    NSMutableDictionary *basePara = [NSMutableDictionary dictionaryWithDictionary:[self baseParameters]];
    _timeStr = [self gainTimestamp];
    [basePara addEntriesFromDictionary:dic];
    
    NSArray *arr = [basePara allKeys];
    arr = [arr sortedArrayUsingComparator:^NSComparisonResult(id obj1, id obj2){
        NSComparisonResult result = [obj1 compare:obj2];
        return result == NSOrderedDescending;
    }];
    
    NSString *dataStr = [NSString string];
    for (int i = 0; i < arr.count; i++) {
        dataStr = [dataStr stringByAppendingString:arr[i]];
        dataStr = [dataStr stringByAppendingString:@"="];
        dataStr = [dataStr stringByAppendingString:[NSString stringWithFormat:@"%@",[basePara objectOrNilForKey:arr[i]]]];
        dataStr = [dataStr stringByAppendingString:@"&"];
    }
    dataStr = [dataStr substringToIndex:dataStr.length - 1];
    return dataStr;
}
//md5加密
- (NSString *)getMd5_32Bit_String:(NSString *)srcString
{
    const char *cStr = [srcString UTF8String];
    unsigned char digest[CC_MD5_DIGEST_LENGTH];
    CC_MD5( cStr, strlen(cStr), digest );
    NSMutableString *result = [NSMutableString stringWithCapacity:CC_MD5_DIGEST_LENGTH * 2];
    for(int i = 0; i < CC_MD5_DIGEST_LENGTH; i++)
        [result appendFormat:@"%02x", digest[i]];
    
    return result;
}
@end

后台在接收到数据后首先会做验签操作,验证签名是否正确和签名是否失效。

  • 单点登录:如果账号被别的客户端登录,上送的 token 会发生改变,之前的 token 就会过期,会在本客户端发出数据请求时弹出登录框。
  • 失效时间: 后台验证token时间,超过预设时间会返回-100
    单点登录和失效时间返回 json 数据如下:
{
  "err":{
      "code":-100,
      "msg": 登录已过期,请重新登录,
      "eventId":"xxx-xxx-xxx-xxx-xxx"
  }
}
  • 这是有一个建议,我们可以在根视图设置一个通知监听,当我们在处理数据时收到 code= -100 是发出登录的通知,模态出登陆页面
  • 数据请求方法
+ (void)postRequestWihtUrl:(NSString *)url params:(NSDictionary *)params success:(void (^)(id))successInfo failure:(TPErrorInfo)errorInfo {
    AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
    manager.requestSerializer = [AFHTTPRequestSerializer serializer];
    manager.responseSerializer.acceptableContentTypes = [NSSet setWithArray:@[@"text/html", @"application/json", @"application/x-www-form-urlencoded"]];
    
    /**
     数据请求
     获取基础上送参数
     [[[TPBaseRequest alloc] init] finalParametersFrom:params]
     */
    [manager POST:url parameters:[[[TPBaseRequest alloc] init] finalParametersFrom:params] progress:nil success:^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject) {
        TPErrorHandle *err = [[TPErrorHandle alloc] initWithDic:[responseObject objectForKey:@"err"]];
        if (err.isError) {
            errorInfo(err);
            return;
        }
        successInfo(responseObject);
    } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
        NSLog(@"请求失败信息:%@",error);
        NSDictionary *errDic=error.userInfo;
        NSHTTPURLResponse *response=[errDic objectForKey:@"com.alamofire.serialization.response.error.response"];
        NSInteger errCode=response.statusCode;
        MDErrorInfo *err = [[MDErrorInfo alloc] initWithDefault];
        err.code = errCode;
        if (error.code == -1009) {
            err.msg = @"网络超时";
        }
        else if (err.code == 404){
            err.msg = @"网络页面不存在";
        }
   //我发出通知处理错误的方法在TPErrorHandle 文件里
        errorInfo([[TPErrorHandle alloc] initWithMDErrorInfo:err]);
    }];
}

  • 这里有一个eventId字段,我之前说了app_id尽量每个版本更新时候换一个,这里就用了
    在 APP 更新一个星期后,我们会在后台把之前版本使用的 app_id 删除,用户如果在继续使用老版本进行数据请求是会出现以下报错:
{
  "err":{
      "code":-99,
      "msg": "有新版本,请及时更新",
      "eventId":"返回的更新链接"
  }
}

根视图上添加的更新通知监听就会pop 出更新页面,这样极大地提高了新版本的更新率。

  • 这一套流程使用时移动端几乎不需要控制是否需要去登陆,所有的判断是否登录都抛给后台去判断,这种做法会浪费一部分服务器资源,但是只要前后台封装好一整套流程,开发速度会有很大提高的。

  • 接口的加密大概就这么些东西了,没有任何加密是无法破解的,我们要做的只是去增加破解的难度。这种加密方式肯定会有弊端,现阶段的我可能技术太烂,只能这么去总结一下,如果写的不好请见谅。

  • 最后想感慨一下关于接手别人代码的事,最近换了工作,公司自己的一个产品需要我接手开发迭代,这份代码从15年到现在经手过5个人,除了代码没有留下任何文档,每个人写代码的风格迥异。熟悉别人代码的过程是痛苦的,但是你在熟悉别人代码的过程中进步也是特别快的,不管别人代码写的好还是坏,每个人的代码中你总能找到亮点,找到你没有用过的技能。最近说的最多的话就是:我靠,还可以这样搞!!!我靠,又在搞事情!!!

  • 如果你觉得我这个流程不错,或者是觉得有问题,欢迎沟通 QQ 929245885

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,012评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,589评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,819评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,652评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,954评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,381评论 1 210
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,687评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,404评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,082评论 1 238
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,355评论 2 241
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,880评论 1 255
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,249评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,864评论 3 232
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,007评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,760评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,394评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,281评论 2 259

推荐阅读更多精彩内容