要实现支付宝和微信支付功能的前提,首先需要一个商户账号。申请开通请往官方指南。
在获取有商户账号,就可以拿到重要也是必须的关键参数。
支付宝关键参数:
商户PID
商户收款账号
商户私钥,pkcs8格式
支付宝公钥
============
微信支付关键参数
微信支付appid
微信支付密钥AppSecret
API密钥(在商户平台设置)
注意的是,密钥的生成保存之类的,最好如官方所说在服务端处理,出于安全问题。
支付宝与微信支付,两个的区别粗略的说,更像是客户端发起支付与服务端发起支付两种方式。
支付宝支付主要是本身APP客户端发起支付,然后通过SDK调起支付宝客户端,发起支付请求,支付完成后支付宝服务端异步通知服务端,与返回支付结果给客户端。
微信支付更多的是服务端处理,APP客户端发起支付前,先往服务端递交支付信息,服务端使用支付信息向微信统一支付发起请求,并返回预订单信息给客户端,客户端再利用预订单信息生成带签名的支付信息,然后再通过SDK调起微信客户端进行支付,支付完成后微信服务端返回客户端支付结果,同时通知客户服务端。
不管使用哪种支付,在发起支付前,都要自己的服务器生成订单并获取到订单号和钱。
集成SDK,有两种集成方法,最为方便的就是cocopods,不过之前并没有cocopods的集成方法,都是手动集成,手动集成相对麻烦,需要先下载SDK包,拖入工程,添加依赖库。官方有较为详细的介绍手动集入
支付宝:https://docs.open.alipay.com/204/105295/
也可以通过cocopods,
pod 'AlipaySDK-iOS'
pod 'WechatOpenSDK'
集成支付宝支付
首先在AppDelegate中处理回调问题
- (BOOL)application:(UIApplication *)application
openURL:(NSURL *)url
sourceApplication:(NSString *)sourceApplication
annotation:(id)annotation {
if ([url.host isEqualToString:@"safepay"]) {
//跳转支付宝钱包进行支付,处理支付结果
[[AlipaySDK defaultService] processOrderWithPaymentResult:url standbyCallback:^(NSDictionary *resultDic) {
NSLog(@"result = %@",resultDic);
}];
}
return YES;
}
// NOTE: 9.0以后使用新API接口
- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary<NSString*, id> *)options
{
if ([url.host isEqualToString:@"safepay"]) {
//跳转支付宝钱包进行支付,处理支付结果
[[AlipaySDK defaultService] processOrderWithPaymentResult:url standbyCallback:^(NSDictionary *resultDic) {
NSLog(@"result = %@",resultDic);
}];
}
return YES;
}
注意这里的safepay。微信是不提供这个判断的。
然后点击项目名称,点击 Info 选项卡,在“URL Types”选项中,点击“+”,在“URL Schemes”中输入“xxxxx”(注意这里填入的要和待会调起支付宝的appScheme参数值一样。否则会无法返回应用。
然后根据官方文档要求,将商品信息拼接成字符串作为待签名字符串,官方提供
app_id=2015052600090779&biz_content={"timeout_express":"30m","seller_id":"","product_code":"QUICK_MSECURITY_PAY","total_amount":"0.02","subject":"1","body":"我是测试数据","out_trade_no":"ZQLM3O56MJD4SK3"}&charset=utf-8&method=alipay.trade.app.pay&sign_type=RSA2×tamp=2016-07-28 20:36:11&version=1.0
这里的做法是新建一个model,并创建响应的属性,比如这里创建了一个叫Order的model,并定义属性,然后使用MJExtension转为json字符串。参数可以看参数说明https://docs.open.alipay.com/204/105465/
最后拼接上biz_content与app_id。当然也可以用官方的方法,也是很推荐的。
Order* order = [Order new];
// NOTE: app_id设置
order.app_id = appID;
// NOTE: 支付接口名称
order.method = @"alipay.trade.app.pay";
// NOTE: 参数编码格式
order.charset = @"utf-8";
// NOTE: 当前时间点
NSDateFormatter* formatter = [NSDateFormatter new];
[formatter setDateFormat:@"yyyy-MM-dd HH:mm:ss"];
order.timestamp = [formatter stringFromDate:[NSDate date]];
// NOTE: 支付版本
order.version = @"1.0";
// NOTE: sign_type设置
order.sign_type = @"RSA";
// NOTE: 商品数据
order.biz_content = [BizContent new];
order.biz_content.body = @"我是测试数据";
order.biz_content.subject = @"1";
order.biz_content.out_trade_no = [self generateTradeNO]; //订单ID(由商家自行制定)
order.biz_content.timeout_express = @"30m"; //超时时间设置
order.biz_content.total_amount = [NSString stringWithFormat:@"%.2f", 0.01]; //商品价格
//将商品信息拼接成字符串
NSString *orderInfo = [order orderInfoEncoded:NO];
NSString *orderInfoEncoded = [order orderInfoEncoded:YES];
拼接完字符串,要记得使用UTF-8编码一下,官方要求value都应编码,如果这里不清楚orderInfoEncoded函数由来或者发现不存在,你可以往Order的.m文件写入以下代码。
- (NSString *)orderInfoEncoded:(BOOL)bEncoded {
if (_app_id.length <= 0) {
return nil;
}
// NOTE: 增加不变部分数据
NSMutableDictionary *tmpDict = [NSMutableDictionary new];
[tmpDict addEntriesFromDictionary:@{@"app_id":_app_id,
@"method":_method?:@"alipay.trade.app.pay",
@"charset":_charset?:@"utf-8",
@"timestamp":_timestamp?:@"",
@"version":_version?:@"1.0",
@"biz_content":_biz_content.description?:@"",
@"sign_type":_sign_type?:@"RSA"}];
// NOTE: 增加可变部分数据
if (_format.length > 0) {
[tmpDict setObject:_format forKey:@"format"];
}
if (_return_url.length > 0) {
[tmpDict setObject:_return_url forKey:@"return_url"];
}
if (_notify_url.length > 0) {
[tmpDict setObject:_notify_url forKey:@"notify_url"];
}
if (_app_auth_token.length > 0) {
[tmpDict setObject:_app_auth_token forKey:@"app_auth_token"];
}
// NOTE: 排序,得出最终请求字串
NSArray* sortedKeyArray = [[tmpDict allKeys] sortedArrayUsingComparator:^NSComparisonResult(id _Nonnull obj1, id _Nonnull obj2) {
return [obj1 compare:obj2];
}];
NSMutableArray *tmpArray = [NSMutableArray new];
for (NSString* key in sortedKeyArray) {
NSString* orderItem = [self orderItemWithKey:key andValue:[tmpDict objectForKey:key] encoded:bEncoded];
if (orderItem.length > 0) {
[tmpArray addObject:orderItem];
}
}
return [tmpArray componentsJoinedByString:@"&"];
}
- (NSString*)orderItemWithKey:(NSString*)key andValue:(NSString*)value encoded:(BOOL)bEncoded
{
if (key.length > 0 && value.length > 0) {
if (bEncoded) {
value = [self encodeValue:value];
}
return [NSString stringWithFormat:@"%@=%@", key, value];
}
return nil;
}
- (NSString*)encodeValue:(NSString*)value
{
NSString* encodedValue = value;
if (value.length > 0) {
encodedValue = (__bridge_transfer NSString*)CFURLCreateStringByAddingPercentEscapes(kCFAllocatorDefault, (__bridge CFStringRef)value, NULL, (__bridge CFStringRef)@"!*'();:@&=+$,/?%#[]", kCFStringEncodingUTF8 );
}
return encodedValue;
}
然后是签名
// NOTE: 获取私钥并将商户信息签名,外部商户的加签过程请务必放在服务端,防止公私钥数据泄露;
// 需要遵循RSA签名规范,并将签名字符串base64编码和UrlEncode
id<DataSigner> signer = CreateRSADataSigner(privateKey);
NSString *signedString = [signer signString:orderInfo];
最好再将,上面编码过的字符串和和签名再拼接,官方提供
NSString *orderString = [NSString stringWithFormat:@"%@&sign=%@",
orderInfoEncoded, signedString];
最后开始调用支付
// NOTE: 调用支付结果开始支付
NSString *appScheme = @"xxxxx";
[[AlipaySDK defaultService] payOrder:orderString fromScheme:appScheme callback:^(NSDictionary *resultDic) {
NSLog(@"reslut = %@",resultDic);
NSDictionary * dict;
if ([resultDic[@"resultStatus"] integerValue] == 9000) {
dict = @{@"code":@"0",@"result":@"支付成功"};
}
else if([resultDic[@"resultStatus"] integerValue] == 6001)
{
dict = @{@"code":@"-2",@"result":@"已取消支付"};
}
else
{
dict = @{@"code":@"-1",@"result":@"支付失败"};
}
//这个地方你可以做一个回调提示
}];
集成微信支付
微信支付和支付宝不同,微信需要注册,因为微信SDK还包含了其他功能,如登录。
在AppDelegate中注册微信SDK
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
[WXApi registerApp:WxAppid enableMTA:YES];
return YES;
}
处理回调
//iOS9.0前
- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(NSString *)sourceApplication annotation:(id)annotation {
[WXApi handleOpenURL:url delegate:[WXApiManager sharedManager]];
return YES;
}
//iOS9.0后
- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary<NSString*, id> *)options{
[WXApi handleOpenURL:url delegate:[WXApiManager sharedManager]];
return YES;
}
然后像添加支付宝的URL Scheme一样,添加appScheme,identifier写入weixin(也可以随意),URL scheme写成微信开放平台申请的appid。之后还需要在info.plist文件中加入白名单LSApplicationQueriesSchemes--》weixin与wechat。在这里说一下,如果在支付宝调起出现问题,请在此白名单中加入alipay与alipayshare。
接下来是调起支付
PayReq *request = [[[PayReq alloc] init] autorelease];
request.partnerId = @"10000100";
request.prepayId= @"1101000000140415649af9fc314aa427";
request.package = @"Sign=WXPay";
request.nonceStr= @"a462b76e7436e98e0ed6e13c64b4fd1c";
request.timeStamp= @"1397527777";
request.sign= @"582282D72DD2B03AD892830965F428CB16E7A256";
[WXApi sendReq:request];
这里看似简单,实际不是的,微信文档曾被不少人说成了一坨屎。先看看参数要求https://pay.weixin.qq.com/wiki/doc/api/app/app.php?chapter=9_12,可以看到签名算法问题还是很容易出现问题的。
参数的意义
/** 商家向财付通申请的商家id */
@property (nonatomic, retain) NSString *partnerId;
/** 预支付订单 */
@property (nonatomic, retain) NSString *prepayId;
/** 随机串,防重发 */
@property (nonatomic, retain) NSString *nonceStr;
/** 时间戳,防重发 */
@property (nonatomic, assign) UInt32 timeStamp;
/** 商家根据财付通文档填写的数据和签名 */
@property (nonatomic, retain) NSString *package;
/** 商家根据微信开放平台文档对数据做的签名 */
@property (nonatomic, retain) NSString *sign;
这里提供一个随机串生成方法
/**
------------------------------
产生随机字符串
------------------------------
1.生成随机数算法 ,随机字符串,不长于32位
2.微信支付API接口协议中包含字段nonce_str,主要保证签名不可预测。
3.我们推荐生成随机数算法如下:调用随机数函数生成,将得到的值转换为字符串。
*/
+ (NSString *)generateTradeNO
{
static int kNumber = 15;
NSString *sourceStr = @"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
NSMutableString *resultStr = [[NSMutableString alloc] init];
// srand函数是初始化随机数的种子,为接下来的rand函数调用做准备。
// time(0)函数返回某一特定时间的小数值。
// 这条语句的意思就是初始化随机数种子,time函数是为了提高随机的质量(也就是减少重复)而使用的。
// srand(time(0)) 就是给这个算法一个启动种子,也就是算法的随机种子数,有这个数以后才可以产生随机数,用1970.1.1至今的秒数,初始化随机数种子。
// Srand是种下随机种子数,你每回种下的种子不一样,用Rand得到的随机数就不一样。为了每回种下一个不一样的种子,所以就选用Time(0),Time(0)是得到当前时时间值(因为每时每刻时间是不一样的了)。
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wshorten-64-to-32"
srand(time(0)); // 此行代码有警告:
#pragma clang diagnostic pop
for (int i = 0; i < kNumber; i++) {
unsigned index = rand() % [sourceStr length];
NSString *oneStr = [sourceStr substringWithRange:NSMakeRange(index, 1)];
[resultStr appendString:oneStr];
}
return resultStr;
}
这里有个值得注意的是,官方所说的
如果服务器不做这个预订单处理,那么我们就得自己发起请求做预订单处理。详细看https://pay.weixin.qq.com/wiki/doc/api/app/app.php?chapter=9_1
这里说一下大概的做法,先定义主要参数,然后转义为XML格式。然后使用AFNetworking,发起统一支付请求,成功会返回一串XML,然后再把XML的主要信息,放入PayReq的对象中,调起客户端微信支付。
参考以下主要做法
// 客户端操作 (实际操作应由服务端操作)
//============================================================
NSString *tradeType = @"APP"; //交易类型
NSString *totalFee = money; //交易价格1表示0.01元,10表示0.1元
NSString *tradeNO = [self generateTradeNO]; //随机字符串变量 这里最好使用和安卓端一致的生成逻辑
NSString *addressIP = [Cyhkeychain getIPAddress:YES]; //设备IP地址
NSString *orderNo = orderSn;//[NSString stringWithFormat:@"%ld",time(0)]; //随机产生订单号用于测试,正式使用请换成你从自己服务器获取的订单号
NSString *notifyUrl = @"http://www.xxxxxxx/xxx";// 交易结果通知网站此处用于测试,随意填写,正式使用时填写正确网站
//获取SIGN签名
MXWechatSignAdaptor *adaptor = [[MXWechatSignAdaptor alloc] initWithWechatAppId:MXWechatAPPID
wechatMCHId:MXWechatMCHID
tradeNo:tradeNO
wechatPartnerKey:MXWechatPartnerKey
payTitle:@"订单"
orderNo:orderNo
totalFee:totalFee
deviceIp:addressIP
notifyUrl:notifyUrl
tradeType:tradeType];
//转换成XML字符串,实际并不是正确的XML格式,需要使用AF方法进行转义
NSString *string = [[adaptor dic] XMLString];
AFHTTPSessionManager *session = [AFHTTPSessionManager manager];
session.responseSerializer = [[AFHTTPResponseSerializer alloc] init];
[session.requestSerializer setValue:@"text/xml; charset=utf-8" forHTTPHeaderField:@"Content-Type"];
[session.requestSerializer setValue:kUrlWechatPay forHTTPHeaderField:@"SOAPAction"];
[session.requestSerializer setQueryStringSerializationWithBlock:^NSString *(NSURLRequest *request, NSDictionary *parameters, NSError *__autoreleasing *error) {
return string;
}];
[session POST:kUrlWechatPay
parameters:string
progress:nil
success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject)
{
// 输出XML数据
NSString *responseString = [[NSString alloc] initWithData:responseObject
encoding:NSUTF8StringEncoding] ;
// 将微信返回的xml数据解析转义成字典
NSDictionary *dic = [NSDictionary dictionaryWithXMLString:responseString];
// NSLog(@"下单返回状态码:%@",[dic objectForKey:@"result_code"]);
// 判断返回的许可
if ([[dic objectForKey:@"result_code"] isEqualToString:@"SUCCESS"]
&&[[dic objectForKey:@"return_code"] isEqualToString:@"SUCCESS"] ) {
// 发起微信支付,设置参数
PayReq *request = [[PayReq alloc] init];
request.openID = [dic objectForKey:WXAPPID];
request.partnerId = [dic objectForKey:WXMCHID];
request.prepayId= [dic objectForKey:WXPREPAYID];
request.package = @"Sign=WXPay";
request.nonceStr= [dic objectForKey:WXNONCESTR];
// 将当前时间转化成时间戳
NSDate *datenow = [NSDate date];
NSString *timeSp = [NSString stringWithFormat:@"%ld", (long)[datenow timeIntervalSince1970]];
UInt32 timeStamp =[timeSp intValue];
request.timeStamp= timeStamp;
// 签名加密
MXWechatSignAdaptor *md5 = [[MXWechatSignAdaptor alloc] init];
request.sign=[md5 createMD5SingForPay:request.openID
partnerid:request.partnerId
prepayid:request.prepayId
package:request.package
noncestr:request.nonceStr
timestamp:request.timeStamp];
// 调用微信
[WXApi sendReq:request];
}
} failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
}];
这里只是作为参考,因为有些方法是需要自己去写的,粘贴复制必错。
=============这上面是服务端没做预订单处理的====如果做了,那么客户端是相对简单的,要注意的就是签名问题,sign的拼接加密。官方要求的sign的拼接模板
stringA="appid=wxd930ea5d5a258f4f&body=test&device_info=1000&mch_id=10000100&nonce_str=ibuaiVcKdpRxkhJA";
stringSignTemp=stringA+"&key=192006250b4c09247ec02edce69f6a2d" //注:key为商户平台设置的密钥key
sign=MD5(stringSignTemp).toUpperCase()="9A0A8659F005D6984697E2CA0A9CF3B7" //注:MD5签名方式
sign=hash_hmac("sha256",stringSignTemp,key).toUpperCase()="6A9AE1657590FD6257D693A078E1C3E4BB6BA4DC30B23E0EE2496E54170DACD6" //注:HMAC-SHA256签名方式
用MD5就不需要HMAC-SHA256方式,否则反之。操作参考如下
// 发起微信支付,设置参数
PayReq *request = [[PayReq alloc] init];
request.openID = [dic objectForKey:WXAPPID];
request.partnerId = [dic objectForKey:WXMCHID];
request.prepayId= [dic objectForKey:WXPREPAYID];
request.package = @"Sign=WXPay";
request.nonceStr= [dic objectForKey:WXNONCESTR];
// 将当前时间转化成时间戳
NSDate *datenow = [NSDate date];
NSString *timeSp = [NSString stringWithFormat:@"%ld", (long)[datenow timeIntervalSince1970]];
UInt32 timeStamp =[timeSp intValue];
request.timeStamp= timeStamp;
request.sign=[md5 createMD5SingForPay:request.openID
partnerid:request.partnerId
prepayid:request.prepayId
package:request.package
noncestr:request.nonceStr
timestamp:request.timeStamp];//字段拼接与MD5加密
// 调用微信
[WXApi sendReq:request];
当然,如果服务端在处理预订单返回sign的MD5字符串的话,就更简单,直接拼接上去就OK了。
最后处理好支付结果的返回就行了。
#pragma mark - WXApiDelegate
- (void)onResp:(BaseResp *)resp
{
if([resp isKindOfClass:[PayResp class]]){
//支付返回结果,实际支付结果需要去微信服务器端查询
NSString *strMsg;
switch (resp.errCode) {
case WXSuccess:
strMsg = @"支付结果:成功!";
// NSLog(@"支付成功-PaySuccess,retcode = %d", resp.errCode);
break;
default:
strMsg = [NSString stringWithFormat:@"支付结果:失败!retcode = %d, retstr = %@", resp.errCode,resp.errStr];
// NSLog(@"错误,retcode = %d, retstr = %@", resp.errCode,resp.errStr);
break;
}
}
}