返回

iOS 应用通过 RESTful 调用 Google APIs 实现授权与数据操作

IOS

iOS 应用与 Google APIs REST 通信:授权与数据操作

在 iOS 应用中与 Google APIs 进行 RESTful 通信,尤其是创建诸如 Google 日历这样的资源时,开发者经常会遇到挑战。一个常见的问题是在尝试访问 API 时被重定向到 Google 的登录页面,即使 API 密钥已经正确设置。本文将分析这一问题,并提供几种解决方案。

问题分析

通过 NSURLConnection 发起请求,并附带授权参数,得到一个 Google HTML 登录页面,意味着你尝试使用的 URL 本质上是在发起一个 OAuth 2.0 的授权流程。直接将用户信息 (如用户名密码) 发送到 Google API 是不安全且不被推荐的。OAuth 2.0 要求你首先获得授权代码,然后用代码交换访问令牌。这个流程不能简化成简单的用户名密码直接请求。

问题的根本在于,https://accounts.google.com/o/oauth2/auth? 这个地址是 Google 授权服务器的端点,你需要完成完整的 OAuth 2.0 授权流程。直接发送请求到该地址,只能得到一个登录页面,这是预期内的行为。你需要按 OAuth 2.0 协议走,否则不能直接获取到访问令牌。

解决方案一:使用 Google 授权服务获取访问令牌

最正统的方式是执行完整的 OAuth 2.0 授权流程。具体步骤如下:

  1. 生成授权链接 : 使用正确的授权参数(包括 response_typecode, client_idredirect_uri 以及所需 scope)。授权链接将你的应用导向 Google 授权页面,用户在此页面同意授权。
    NSString *baseURL = @"https://accounts.google.com/o/oauth2/auth?";
    NSDictionary *authParameters = @{
       @"response_type" : @"code",
       @"redirect_uri"  : @"YOUR_REDIRECT_URI",  //需要事先在 Google Cloud Console 中设置
       @"client_id"     : @"YOUR_CLIENT_ID",
       @"scope"         : @"https://www.googleapis.com/auth/calendar"  //或其它需要的权限
     };
    
    NSURL *authURL = [NSURL URLWithString:[NSString stringWithFormat:@"%@%@", baseURL, [self dictionaryToURLStringVariables:authParameters]]];
    
    
     [[UIApplication sharedApplication] openURL:authURL options:@{} completionHandler:nil];
    
    
     // 辅助方法 将字典转换为URL查询字符串
    - (NSString *)dictionaryToURLStringVariables:(NSDictionary *)dictionary
    {
        NSMutableArray *pairs = [NSMutableArray array];
        for (NSString *key in dictionary) {
            NSString *value = [dictionary objectForKey:key];
            NSString *pair = [NSString stringWithFormat:@"%@=%@",
                    key, [value stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]]
                  ];
            [pairs addObject:pair];
        }
        return [pairs componentsJoinedByString:@"&"];
    }
    
    
    
  2. 监听重定向 : 当用户完成授权,浏览器会被重定向回你预先注册的 redirect_uri。URL 中会携带授权 code 参数。
  3. 交换授权代码为访问令牌 : 将获得的授权 code 以及你的客户端凭据 (client_idclient_secret,需要在 Google API 控制台中找到) 发送到 Google 的令牌端点 https://oauth2.googleapis.com/token ,换取访问令牌 access_token。 注意,这里使用 POST 方法,并确保发送内容类型设置为 application/x-www-form-urlencoded
      //  当你的应用被redirect回来
     - (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options {
    
          NSString *code = nil;
    
    
          NSURLComponents *components = [NSURLComponents componentsWithURL:url resolvingAgainstBaseURL:YES];
    
              NSArray *queryItems = components.queryItems;
    
              for (NSURLQueryItem *item in queryItems) {
                 if([item.name isEqualToString:@"code"]){
                   code= item.value;
                 }
    
           }
    
    
            [self exchangeCodeForToken:code];
    
         return true;
    
        }
    
      -(void)exchangeCodeForToken:(NSString *)authCode{
    
    
        NSString *tokenURL = @"https://oauth2.googleapis.com/token";
    
          NSURL *url = [NSURL URLWithString:tokenURL];
    
            NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
    
            request.HTTPMethod = @"POST";
    
    
        NSString *requestBody =[NSString stringWithFormat: @"code=%@&client_id=%@&client_secret=%@&redirect_uri=%@&grant_type=authorization_code",
        authCode, @"YOUR_CLIENT_ID", @"YOUR_CLIENT_SECRET",@"YOUR_REDIRECT_URI"];
    
    
    
         [request setHTTPBody:[requestBody dataUsingEncoding:NSUTF8StringEncoding]];
    
    
          [request setValue:@"application/x-www-form-urlencoded" forHTTPHeaderField:@"Content-Type"];
    
    
        NSURLSessionDataTask *task = [[NSURLSession sharedSession] dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
    
    
                if(error){
    
                        NSLog(@"Error Exchange: %@", error.localizedDescription);
                } else{
                    NSError *jsonError;
                     NSDictionary *json =[NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:&jsonError];
    
                   if(jsonError){
                    NSLog(@"jsonError %@", jsonError.localizedDescription);
                     }
    
                    if(json != nil){
                          NSString *accessToken = json[@"access_token"];
    
                        NSLog(@"access_token:%@",accessToken);
                      // now store it for other requests!
                       }
    
                }
    
        }];
    
          [task resume];
    
    
     }
    
  4. 使用访问令牌进行 API 调用 : 获得 access_token 之后,将它放入 Authorization 请求头,然后就可以发送请求进行增删改查操作了。
    
       -(void)createCalendar:(NSString *)accessToken
      {
       NSString *apiUrl =@"https://www.googleapis.com/calendar/v3/calendars";
    
             NSURL *url = [NSURL URLWithString:apiUrl];
    
                NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
    
             [request setHTTPMethod:@"POST"];
    
             NSString *title=@"New REST Calendar";
    
              NSDictionary *requestBodyJson =@{
                @"summary":title
            };
    
            NSData *body =[NSJSONSerialization dataWithJSONObject:requestBodyJson options:kNilOptions error:nil];
    
    
             [request setHTTPBody:body];
    
            [request setValue:@"application/json" forHTTPHeaderField:@"Content-Type"];
    
         NSString *authHeaderValue=[NSString stringWithFormat:@"Bearer %@",accessToken];
    
    
          [request setValue:authHeaderValue forHTTPHeaderField:@"Authorization"];
    
    
             NSURLSessionDataTask *task = [[NSURLSession sharedSession] dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
    
    
                    if(error){
    
                            NSLog(@"Error  Request: %@", error.localizedDescription);
    
                     }
                        else{
    
    
                        NSError *jsonError;
                            NSDictionary *json =[NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:&jsonError];
    
                                  if(jsonError){
                                            NSLog(@"jsonError  Request: %@", jsonError.localizedDescription);
                                  }
    
                        NSLog(@"Create request %@", json);
    
    
                        }
    
    
    
         }];
    
    
                [task resume];
       }
    

安全提示

  • 不要将 client_secret 直接硬编码到客户端,应尽量使用安全的方式管理和传递秘钥,例如通过服务端请求或者 KeyChain。
  • redirect_uri 的校验,应该确保和注册在 Google API 控制台的一致。
  • access_token 也应该小心管理,避免泄露。考虑将其存储在 KeyChain 中,并在合适的时间进行刷新(refreshToken) 。

总结

直接用用户名和密码来访问 Google API 是不可行的。必须使用 OAuth 2.0 标准协议完成授权。使用官方的客户端库,尽管提供了更高层次的抽象,简化了认证和授权过程,但理解底层流程是有益的。遵循上面给出的流程可以允许您构建更精细化和可控的应用行为。