一、JSPatch的使用流程
为了保证js文件在传输过程中的安全,防止被别人恶意篡改,在传输过程中需要对js文件进行RSA加密,流程如下:
服务端:(服务器端的代码实现可参考PHP、iOS 使用JSPatch基本与RSA,AES加密)
1.计算js文件MD5值
2.用RSA私钥对MD5值进行加密,与JS文件一起下发给客户端
客户端:
1.拿到加密数据,用RSA公钥解密出MD5值,
2.本地计算返回的JS文件MD5值
3.对比上述的两个MD5值,若相等则校验通过,取JS文件保存到本地
二、JSPatch接入项目进行热修复有两种方式(使用JSPatch平台保存脚本文件和使用公司的服务器保存脚本文件)
1、使用JSPatch平台保存脚本文件
1)使用JSPatch平台保存脚本只需要上传脚本文件和对应的rsa_private_key.pem文件即可,JSPatch会自动打包上传;下发量比较多时需要资费,点击查看资费详细情况
2)导入JSPatch SDK
若使用 cocoapods管理,使用
pod 'JSPatchPlatform'
然后执行pod install即可
如果手动导入,下载导入即可
3)公钥、私钥生成:
在 Mac 终端上执行 openssl,再执行以下三句命令,生成 PKCS8 格式的 RSA 公私钥,执行过程中提示输入密码,密码为空(直接回车)就行。
openssl >
genrsa -out rsa_private_key.pem 1024
pkcs8 -topk8 -inform PEM -in rsa_private_key.pem -outform PEM –nocrypt
rsa -in rsa_private_key.pem -pubout -out rsa_public_key.pem
这样在执行的目录下就有了 rsa_private_key.pem 和 rsa_public_key.pem 这两个文件。这里生成了长度为 1024 的私钥,长度可选 1024 / 2048 / 3072 / 4096 ...。
4)使用 Private Key 下发脚本
下发脚本时在发布脚本界面勾选 使用自定义RSA Key 选项,会出现文件上传框,选择本地的 rsa_private_key.pem 文件,与脚本一同上传,JSPatch 平台会使用这个上传的 Private Key 对脚本 MD5 值进行加密,再下发给客户端,如下图所示:
5)SDK用法:
添加依赖库
libz.dylib
JavaScriptCore.framework
导入头文件
#import <JSPatchPlatform/JSPatch.h>
在- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions里面写下面代码:
本地测试时,要求在项目中有main.js文件,文件的名字必须是main.js,调用如下代码:
[JSPatch testScriptInBundle];
正式下发时,调用如下代码:
[JSPatch startWithAppKey:@"appkey"];//appkey是在JSPatch平台新建应用时生成的
[JSPatch setupRSAPublicKey:@"公钥"];//公钥为rsa_public_key.pem的值
[JSPatch sync];
注意:
[JSPatch testScriptInBundle]和[JSPatch startWithAppKey:@"appkey"]不可以同时使用
此时就已经完成了JSPatch的接入,如果在JSPatch平台有对应版本号的脚本文件,就会自动下载脚本并执行用来修复bug
2、使用公司的服务器保存脚本文件
1)使用公司自己的服务器保存脚本需要每次更新脚本文件时,将脚本文件和packer.php文件打包上传到后台指定的文件夹下,后台提供一个接口用来返回是否有新的脚本文件下载以及对应的脚本版本号
2)导入JSPatch三方库,若使用cocoapods管理,使用
pod 'JSPatch'
然后pod install 即可
如果手动导入,下载导入即可
添加依赖库
libz.dylib
JavaScriptCore.framework
3)公钥、私钥生成
使用openSSL命令生成密钥
//-days后面的数字代表public_key.der的时效,天数
openssl req -x509 -days 365 -out public_key.der -outform der -new -newkey rsa:1024 -keyout private_key.pem
按照提示,填入私钥的密码(之后会使用),签名证书的组织名、邮件等信息之后,就会生成包含有公钥的证书文件public_key.der和私钥文件private_key.pem。
public_key.der文件用于分发到ios客户端进行公钥加解密,而private_key.pem文件留在服务器端供php使用
生成pem格式的公钥文件
openssl rsa -in private_key.pem -pubout -out public_key.pem
复制private_key.pem的内容,替换packer.php里面的私钥。packer.php脚本的使用 (packer.php可以在JSPactch的demo里面找到)
$ php packer.php main.js -o v2
执行这个命令之后会生成v2.zip文件,将v2.zip文件上传到后台指定的路径下,其中main.js是脚本文件,v2代表js版本号
4)接口设计以及在下载脚本时需要解决的问题
这里使用公司自己的服务器,用自己的服务器,就牵涉到几个问题?
1)什么时候去请求脚本:每次启动APP的时候请求和从后台进入的时候
2)有脚本了怎么办 :先查看本地是否有脚本存在,然后根据app版本号和和脚本的版本号请求接口看是否有新的脚本需要下载,若有则下载,若没有则加载本地脚本,如不存在本地脚本则不需要做任何操作
3)同一个版本存在多个修复怎么办 :在同一个版本下,用fix_num 来标志本版本修复数,一直在递增
4)不同的版本请求脚本的问题:在请求接口的时候传入版本号,请求这个版本下的脚本
所以设计接口是xxx/xxx?version=xxxx&js_version=xxx ,version表示版本号 ,js_version代表脚本版本号,返回的数据结构是
{
"ret_code": "xxx", //表示是否有新的脚本供下载
"js_version": "fix_1", //表示需要下载的脚本版本号
}
5)在iOS中的使用
下载服务器上的脚本文件并进行解压保存到本地
+ (void)updateToVersion:(NSInteger)version callback:(JPUpdateCallback)callback
{
NSString *appVersion = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleShortVersionString"];
if (JPLogger) JPLogger([NSString stringWithFormat:@"JSPatch: updateToVersion: %@", @(version)]);
// create url request
//在服务器上存放的路径及命名格式(app版本号/js版本号.zip)
NSString *downloadKey = [NSString stringWithFormat:@"/v%@/v%@.zip", appVersion, @(version)];
//下载路径
NSURL *downloadURL = [NSURL URLWithString:[JSPatchUrl stringByAppendingString:downloadKey]];
NSURLRequest *request = [NSURLRequest requestWithURL:downloadURL cachePolicy:NSURLRequestReloadIgnoringLocalCacheData timeoutInterval:20.0];
if (JPLogger) JPLogger([NSString stringWithFormat:@"JSPatch: request file %@", downloadURL]);
//下载脚本
// create task
NSURLSessionDataTask *task = [[NSURLSession sharedSession] dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
if (!error) {
if (JPLogger) JPLogger([NSString stringWithFormat:@"JSPatch: request file success, data length:%@", @(data.length)]);
// script directory
NSString *scriptDirectory = [self fetchScriptDirectory];
//下载的zip 暂时存放的路径,下载完成或失败之后会把这个文件夹删除
// temporary files and directories
NSString *downloadTmpPath = [NSString stringWithFormat:@"%@patch_%@_%@", NSTemporaryDirectory(), appVersion, @(version)];
NSString *unzipVerifyDirectory = [NSString stringWithFormat:@"%@patch_%@_%@_unzipTest/", NSTemporaryDirectory(), appVersion, @(version)];
NSString *unzipTmpDirectory = [NSString stringWithFormat:@"%@patch_%@_%@_unzip/", NSTemporaryDirectory(), appVersion, @(version)];
// save data
[data writeToFile:downloadTmpPath atomically:YES];
// is the processing flow failed
BOOL isFailed = NO;
// 1. unzip encrypted md5 file and script file
NSString *keyFilePath;
NSString *scriptZipFilePath;
ZipArchive *verifyZipArchive = [[ZipArchive alloc] init];
[verifyZipArchive UnzipOpenFile:downloadTmpPath];
BOOL verifyUnzipSucc = [verifyZipArchive UnzipFileTo:unzipVerifyDirectory overWrite:YES];
if (verifyUnzipSucc) {
for (NSString *filePath in verifyZipArchive.unzippedFiles) {
NSString *filename = [filePath lastPathComponent];
if ([filename isEqualToString:@"key"]) {
// encrypted md5 file
keyFilePath = filePath;
} else if ([[filename pathExtension] isEqualToString:@"zip"]) {
// script file
scriptZipFilePath = filePath;
}
}
} else {
if (JPLogger) JPLogger(@"JSPatch: fail to unzip file");
isFailed = YES;
if (callback) {
callback([NSError errorWithDomain:@"org.jspatch" code:JPUpdateErrorUnzipFailed userInfo:nil]);
}
}
// 2. decrypt and verify md5 file
if (!isFailed) {
//拿到加密数据,用RSA公钥解密出MD5值
NSData *md5Data = [RSA decryptData:[NSData dataWithContentsOfFile:keyFilePath] publicKey:publicKey];
NSString *decryptMD5 = [md5Data dataToUtf8String] ;
NSLog(@"解析的md5 = %@",decryptMD5);
//本地计算返回的JS文件MD5值
NSString *md5 = [self fileMD5:scriptZipFilePath];
//两个md5值进行匹配
if (![decryptMD5 isEqualToString:md5]) {
if (JPLogger) JPLogger([NSString stringWithFormat:@"JSPatch: decompress error, md5 didn't match, decrypt:%@ md5:%@", decryptMD5, md5]);
isFailed = YES;
if (callback) {
callback([NSError errorWithDomain:@"org.jspatch" code:JPUpdateErrorVerifyFailed userInfo:nil]);
}
}
}
// 3. unzip script file and save
if (!isFailed) {
ZipArchive *zipArchive = [[ZipArchive alloc] init];
[zipArchive UnzipOpenFile:scriptZipFilePath];
BOOL unzipSucc = [zipArchive UnzipFileTo:unzipTmpDirectory overWrite:YES];
if (unzipSucc) {
for (NSString *filePath in zipArchive.unzippedFiles) {
NSString *filename = [filePath lastPathComponent];
if ([[filename pathExtension] isEqualToString:@"js"]) {
[[NSFileManager defaultManager] createDirectoryAtPath:scriptDirectory withIntermediateDirectories:YES attributes:nil error:nil];
NSString *newFilePath = [scriptDirectory stringByAppendingPathComponent:filename];
//将脚本文件保存到本地
[[NSData dataWithContentsOfFile:filePath] writeToFile:newFilePath atomically:YES];
}
}
}
else
{
if (JPLogger) JPLogger(@"JSPatch: fail to unzip script file");
isFailed = YES;
if (callback) {
callback([NSError errorWithDomain:@"org.jspatch" code:JPUpdateErrorUnzipFailed userInfo:nil]);
}
}
}
// success
if (!isFailed) {
if (JPLogger) JPLogger([NSString stringWithFormat:@"JSPatch: updateToVersion: %@ success", @(version)]);
[[NSUserDefaults standardUserDefaults] setInteger:version forKey:kJSPatchVersion(appVersion)];
[[NSUserDefaults standardUserDefaults] synchronize];
if (callback) callback(nil);
}
//删除临时文件夹
// clear temporary files
[[NSFileManager defaultManager] removeItemAtPath:downloadTmpPath error:nil];
[[NSFileManager defaultManager] removeItemAtPath:unzipVerifyDirectory error:nil];
[[NSFileManager defaultManager] removeItemAtPath:unzipTmpDirectory error:nil];
} else {
if (JPLogger) JPLogger([NSString stringWithFormat:@"JSPatch: request error %@", error]);
if (callback) callback(error);
}
}];
[task resume];
}
下载成功之后,加载js文件
+ (BOOL)run
{
if (JPLogger) JPLogger(@"JSPatch: runScript");
NSString *scriptDirectory = [self fetchScriptDirectory];
NSString *scriptPath = [scriptDirectory stringByAppendingPathComponent:@"main.js"];
if ([[NSFileManager defaultManager] fileExistsAtPath:scriptPath]) {
[JPEngine startEngine];
[JPEngine addExtensions:@[@"JPLoaderInclude"]];
[JPEngine evaluateScriptWithPath:scriptPath];
if (JPLogger) JPLogger([NSString stringWithFormat:@"JSPatch: evaluated script %@", scriptPath]);
return YES;
} else {
return NO;
}
}
此时就已经完成了JSPatch的接入,在公司的服务器上含有修复脚本的话就可以下载脚本用来修复bug
网友评论