PAYATHON 2026

如何在 iOS Dwolla payment SDK 中获取 access token

支付阿杰

结论

[self hasCode:urlValues] 始终返回 false,说明当前拦截到的 URL 查询参数里没有名为 code 的 OAuth 授权码。常见原因包括:

  • Dwolla 应用中配置的 redirect_uri 与授权请求中的地址不完全一致;
  • 当前拦截到的是登录过程中的中间页面,不是最终回调 URL;
  • Dwolla 返回的是 error,而不是 code;
  • 回调参数的解析方式不够可靠;
  • code 不在第一个参数中,但代码仍固定读取 urlValues[0][1]。

应为应用配置明确的回调地址,只在 URL 命中该地址时读取 code,并按参数名查找,不能假定它一定是第一个参数。使用授权码换取 access token 的操作应在服务端完成,不要把 Client secret 写进 iOS 应用。

为什么 hasCode: 一直返回 false

OAuth 登录成功后,Dwolla 应将浏览器重定向到类似下面的地址:

https://example.com/oauth/dwolla/callback?code=AUTHORIZATION_CODE

只有最终回调 URL 中包含 code 参数,hasCode: 才可能返回 YES。可以先打印实际访问的 URL:

NSLog(@"Dwolla navigation URL: %@", req.URL.absoluteString);

如果打印出来的是登录页、授权确认页或其他中间地址,那么没有 code 很正常,WebView 应继续加载。

如果回调地址类似:

https://example.com/oauth/dwolla/callback?error=access_denied

说明用户拒绝了授权,或者授权请求无效,此时同样不会出现 code。

原代码还有一个明显的问题。即使 hasCode: 在数组的其他位置找到了 code,后续代码仍会固定读取第一个参数:

[[urlValues objectAtIndex:0] objectAtIndex:1]

例如回调地址是:

https://example.com/callback?state=abc&code=xyz

第一个参数为 state=abc,这段代码最终会取得 abc,而不是授权码 xyz。

正确解析回调 URL

不要手动使用 & 和 = 拆分查询字符串。这种方法无法可靠处理参数顺序、百分号编码、空值,以及参数值本身包含 = 的情况。可以改用 NSURLComponents 和 NSURLQueryItem:

- (BOOL)webView:(UIWebView *)webView
        shouldStartLoadWithRequest:(NSURLRequest *)request
        navigationType:(UIWebViewNavigationType)navigationType
{
    NSURL *URL = request.URL;

    // 必须与创建授权请求时使用的 redirect_uri 对应
    BOOL isCallback =
        [URL.scheme isEqualToString:@"https"] &&
        [URL.host isEqualToString:@"example.com"] &&
        [URL.path isEqualToString:@"/oauth/dwolla/callback"];

    if (!isCallback) {
        return YES;
    }

    NSURLComponents *components =
        [NSURLComponents componentsWithURL:URL resolvingAgainstBaseURL:NO];

    NSString *authorizationCode = nil;
    NSString *oauthError = nil;

    for (NSURLQueryItem *item in components.queryItems) {
        if ([item.name isEqualToString:@"code"]) {
            authorizationCode = item.value;
        } else if ([item.name isEqualToString:@"error"]) {
            oauthError = item.value;
        }
    }

    if (oauthError.length > 0) {
        NSLog(@"Dwolla authorization failed: %@", oauthError);
        return NO;
    }

    if (authorizationCode.length > 0) {
        [self sendAuthorizationCodeToServer:authorizationCode];
        return NO;
    }

    NSLog(@"Dwolla callback did not contain code: %@", URL.absoluteString);
    return NO;
}

其中的 example.com 和 /oauth/dwolla/callback 必须替换为实际配置的回调地址。

如果必须继续使用旧 SDK 的 requestAccessToken:,至少要把解析得到的变量传给它:

if (authorizationCode.length > 0) {
    [self requestAccessToken:authorizationCode];
    return NO;
}

不过,这种方式通常要求客户端持有 Client secret,不适合正式应用。

推荐的授权流程

较为安全的授权流程如下:

  1. iOS 应用打开 Dwolla 授权页面。
  2. 用户登录并批准授权。
  3. Dwolla 跳转到预先注册的 redirect_uri,并附带 code。
  4. iOS 应用截获回调,将 code 发送到自己的服务端。
  5. 服务端使用 Client key、Client secret、code 和相同的 redirect_uri 请求 Dwolla token endpoint。
  6. 服务端保存 access token,再通过自己的 API 向 iOS 应用提供所需的业务数据。

示意代码如下:

- (void)sendAuthorizationCodeToServer:(NSString *)code
{
    NSURL *URL = [NSURL URLWithString:
        @"https://api.example.com/oauth/dwolla/exchange"];

    NSMutableURLRequest *request =
        [NSMutableURLRequest requestWithURL:URL];

    request.HTTPMethod = @"POST";
    [request setValue:@"application/json"
   forHTTPHeaderField:@"Content-Type"];

    NSDictionary *body = @{
        @"code": code
    };

    request.HTTPBody =
        [NSJSONSerialization dataWithJSONObject:body
                                        options:0
                                          error:nil];

    NSURLSessionDataTask *task =
        [[NSURLSession sharedSession]
            dataTaskWithRequest:request
              completionHandler:^(NSData *data,
                                  NSURLResponse *response,
                                  NSError *error) {
        if (error) {
            NSLog(@"Authorization code exchange failed: %@", error);
            return;
        }

        // 根据服务端 API 的响应格式继续处理
    }];

    [task resume];
}

服务端交换 token 时使用的 redirect_uri 必须与授权请求中的值完全一致,包括协议、域名、路径、端口和尾部斜杠。

使用自定义 URL Scheme 时

原生应用也可以注册自定义回调,例如:

myapp://oauth/dwolla

对应的拦截条件可以改为:

BOOL isCallback =
    [URL.scheme isEqualToString:@"myapp"] &&
    [URL.host isEqualToString:@"oauth"] &&
    [URL.path isEqualToString:@"/dwolla"];

这种回调形式是否可用,取决于当前 Dwolla OAuth 的配置规则。如果 Dwolla 只接受 HTTPS 回调,可以让 HTTPS 页面通过 Universal Link 或受控跳转返回应用。

注意事项

  • 不要把 Client secret 编译进 iOS 应用。安装包可以被分析,secret 无法真正保密。
  • 必须校验 OAuth 的 state 参数,防止登录 CSRF 和回调串用。
  • 不要记录完整的 authorization code、access token 或 refresh token。
  • authorization code 通常只能使用一次,而且有效期较短,应立即发送到服务端进行交换。
  • UIWebView 已被弃用,新项目应使用 WKWebView 或系统身份验证会话。
  • 如果 dwolla-ios 是旧式 SDK,其中的示例可能对应历史 OAuth 接口。实际授权地址、token endpoint、scope、PKCE 支持情况和移动端限制,应以当前 Dwolla API 文档及应用后台配置为准。
  • 如果回调中没有 code,先检查完整回调 URL 中的 error、error_description 和 state,再确认 redirect_uri 是否完全匹配。

备注:内容仅供参考。