RailsアプリにiOSクライアントをサッと作る #sgadvent
この記事はソニックガーデン Advent Calendar 2015 、6日目の記事です。
どうも大野(@xoyip)です。3日目に続いて登場させてもらいます。今日は5日目までとは少し変わって技術的なHow Toの話で、自分用の覚書でもあります。
ちなみに3日目のはこれ。
ソニックガーデンではお客さまが実現したいサービスを開発するのにRuby on Railsを使っています。PCやスマートフォンのブラウザから利用されるようなサービスを作ってきました。
ところが最近のスマートフォン普及率などから、スマートフォンのネイティブアプリケーションを用意してユーザーに使ってもらいたいというお客さまのご要望も増えてきています。社内でも開発や運用についてどうしていくかの議論が盛んになってきています。
そういう流れから今日はすでに運用しているRailsアプリケーションにiOSクライアントを作るには、という話をサンプルコードで簡単に紹介できればと思います。
今回の事例
「Facebookログインを利用した既存のWebサイト(Rails)用のiOSクライアントを作りたい」というケースを考えてみます。
やらなければならないことは
この2つです。
またサンプルコードをGitHubに用意したので、RubyやXcodeがあれば実際に動かすことができます。
Railsにスマートフォン用の認証口を用意する
doorkeeperの導入
まずは外部のクライアントが認証できる仕組みをRails側に用意します。doorkeeperというgemを使うと、超簡単にOAuthプロバイダーの機能をRailsアプリケーションに追加することができます。
https://github.com/doorkeeper-gem/doorkeeper
既存のRailsアプリケーションに対して次のステップを実施すれば簡単に導入可能です。
$ gem 'doorkeeper' $ bundle $ rails generate doorkeeper:install $ rails generate doorkeeper:migration $ rake db:migrate
あとはroutes.rb
に追記すれば大体おわり。
# config/routes.rb Rails.application.routes.draw do # 他のroutes use_doorkeeper end
※設定は上のステップで生成されたconfig/initializers/doorkeeper.rb
で。色々あるので説明は省略!
OAuth Applicationの作成
先ほどのセットアップがうまくいっていれば/oauth/applications
にアクセスして次のような画面を開くことができるようになります。この画面から新しいアプリケーションを作成していきます。
必要なパラメータを入れます。名前は識別できればなんでもよくて、リダイレクトURI
にはiOS側で設定するものと同じものを入れます。今回はintegration-sample-ios://oauth-callback/ios
を使うことにしました(スキーマ以外が合っていれば、path
などは必要に応じて変えればよいです)。
追記
development環境以外ではURLスキームがhttpsでないとエラーとなります。それを回避するためにはconfig/initializers/doorkeeper.rb
でforce_ssl_in_redirect_uri false
としておけば良いです。
追記おわり
作成するとアプリケーションIDやトークンが発行されます。iOS側で使うので控えておきます。
APIを用意する
Railsアプリケーション側にJSON APIがなければ作っておきます。今回はiOSからログインできていることを確認するために、ログインしているユーザーの名前やメールアドレスを返すAPIを用意します。
まずAPI用のApplicationController
的なものを用意します。
API用のコントローラの全てのアクションでdoorkeeper_authorize!
で認証をかけて、ヘルパーメソッドcurrent_user
で認証したユーザーを得られるようにしておきます。
# app/controllers/api/api_controller.rb class Api::ApiController < ApplicationController before_action :doorkeeper_authorize! helper_method :current_user def current_user User.find(doorkeeper_token.resource_owner_id) if doorkeeper_token end end
最後に、名前とメールアドレスを返すAPIを定義します。
# app/controllers/api/users_controller.rb class Api::UsersController < Api::ApiController def show @user = current_user end end
# app/views/api/users/show.json.jbuilder json.extract!(@user, :id, :name, :email)
これをiOSから呼び出せれば連携成功!となるわけです。
iOSクライアントを作る
iOSアプリでは起動時にログイン済みかどうかを判定し、ログイン済みであればAPIを叩いて自分の情報を表示、そうでなければログイン画面を表示という流れになるようなものを作ります。
pod install
ライブラリをいくつか使うのでそれらをインストールします。
# Podfile source 'https://github.com/CocoaPods/Specs.git' platform :ios, '9.0' use_frameworks! pod 'Alamofire', '>= 2.0' # APIを呼ぶときに pod 'SwiftyJSON', '>= 2.3' # APIのレスポンスの処理に pod 'p2.OAuth2', '~> 2.0.0' # OAuth2の認証に pod 'KeychainAccess' # トークンの保存に
※iOS9からhttpの通信で叱られるようになります。それだと開発中に困るのでApp Transport Securityの設定でhttpを許すドメインを指定してあげたほうが便利です。
※p2.OAuth2ライブラリ内のコードに、開こうとしているURLがhttpのときはassertで止まるような記述があります。あまりよろしくないですが開発中はコメントアウトしています。
URLスキームを設定する
p2.OAuth2を使った認証の流れですが、
といった感じになっています。doorkeeperで設定したリダイレクトURIは認証後にアプリに戻るために使われます。
URLスキームの設定方法はこんな感じ。
認証する
認証のところのコード(全体像はサンプルコードを見てもらったほうがよいと思います)。
var oauth2 : OAuth2CodeGrant? let settings = [ "client_id": "YOUR_APP_ID", "client_secret": "YOUR_APP_SECRET", "authorize_uri": "YOUR_SERVER_URL" + "/oauth/authorize", "token_uri": "YOUR_SERVER_URL" + "/oauth/token", "scope": "", "redirect_uris": ["integration-sample-ios://oauth-callback/ios"], "keychain": false, ] as OAuth2JSON // OAuthアプリケーションのトークンなどを使って認証用のオブジェクトを生成 oauth2 = OAuth2CodeGrant(settings: settings) // アプリ内に埋め込みでSafariViewを出す oauth2?.authConfig.authorizeEmbedded = true // 成功時 oauth2?.onAuthorize = { parameters in let json = JSON(parameters) // トークンを保存 let keychain = KeychainAccess.Keychain(service: "YOUR_BUNDLE_ID") try! keychain.set(json["access_token"].stringValue, key: "access_token") } // 失敗時 oauth2?.onFailure = { error in print(error) }
そして最初のビューが表示されたタイミングで、ここで生成したoauth2
オブジェクトを使ってログイン画面を表示します。
override func viewDidAppear(animated: Bool) { super.viewDidAppear(animated) let keychain = KeychainAccess.Keychain(service: "YOUR_BUNDLE_ID") // トークンが保存されているかどうかでログイン済みかどうかを判定する if let accessToken = try! keychain.get("access_token") { // APIを呼ぶ } else { // contextとして自身(UIViewController)を指定する Auth.sharedInstance.oauth2?.authConfig.authorizeContext = self // ログイン画面を出す Auth.sharedInstance.oauth2?.authorize() } }
最後に、認証完了後、URLスキームでアプリに戻ってきたことをoauth2
オブジェクトに教えてあげることで成功時のコールバックonAuthorize
が呼ばれます。
// AppDelegate.swift func application(application: UIApplication, openURL url: NSURL, sourceApplication: String?, annotation: AnyObject) -> Bool { if let path = url.path { if path.hasPrefix("/ios") { Auth.sharedInstance.oauth2?.handleRedirectURL(url) return true } } return false }
なお今回はoauth2
オブジェクトを各所から参照するためにシングルトンを用いています。
APIを使う
ここまでくれば、あとは取得したトークンでAPI呼び出しをするだけです。
ApiClient.sharedInstance.get("/user", parameters: [:], onSuccess: { json in let alertController = UIAlertController(title: "ログイン成功", message: "\(json["name"])\nとしてログインしました", preferredStyle: .Alert) let defaultAction = UIAlertAction(title: "OK", style: .Default, handler: nil) alertController.addAction(defaultAction) self.presentViewController(alertController, animated: true, completion: nil) }, onFailure: { error in print(error) })
jsonにはRailsが返したid、名前、メールアドレスが入っています。アラートで表示(手抜き)してきちんと取得できるのが確認できるはずです。
ちなみにAPIクライアントはこんな感じのコードです。本当のアプリケーションではもっと汎用的に使えるような書き方をしたほうが良いですが、サンプルなのでサッと書いてしまいました。
// ApiClient.swift class ApiClient: NSObject { static let sharedInstance = ApiClient() let endpoint : String = "YOUR_SERVER_URL/api" func get(path: String, parameters: Dictionary<String, String>, onSuccess: (JSON)->Void, onFailure: (NSError)->Void) { guard let url = NSURL(string: endpoint + path) else { return } let headers = [ "Authorization": "Bearer \(Auth.sharedInstance.accessToken()!)" ] Alamofire.request(.GET, url, headers: headers, parameters: parameters).responseJSON { response in if response.result.isSuccess { if let value = response.result.value { onSuccess(JSON(value)) } } else { if let error = response.result.error { onFailure(error) } } } } }
これでiOSアプリケーションとRailsの連携ができるようになりました。ライブラリに頼りっきりではありますが、その分連携するまでをサッと作ることができます。アプリケーションそのものに時間を使ったほうがいいですからね。
何回か書いていますが、サンプルコードを見てもらったほうがわかりやすいと思います。