PIYO - Tech & Life -

RailsアプリにiOSクライアントをサッと作る

この記事はソニックガーデン Advent Calendar 2015 、6日目の記事です。

どうも大野(@pi_cha_n)です。3日目に続いて登場させてもらいます。今日は5日目までとは少し変わって技術的なHow Toの話で、自分用の覚書でもあります。

ちなみに3日目のはこれ。

https://blog.piyo.tech/posts/2015-12-03-083000

ソニックガーデンではお客さまが実現したいサービスを開発するのにRuby on Railsを使っています。PCやスマートフォンのブラウザから利用されるようなサービスを作ってきました。

ところが最近のスマートフォン普及率などから、スマートフォンのネイティブアプリケーションを用意してユーザーに使ってもらいたいというお客さまのご要望も増えてきています。社内でも開発や運用についてどうしていくかの議論が盛んになってきています。

そういう流れから今日はすでに運用しているRailsアプリケーションにiOSクライアントを作るには、という話をサンプルコードで簡単に紹介できればと思います。

今回の事例

「Facebookログインを利用した既存のWebサイト(Rails)用のiOSクライアントを作りたい」というケースを考えてみます。

やらなければならないことは

  • Railsにスマートフォン用の認証口を用意すること
  • Railsと連携するiOSクライアントを作ること

この2つです。

またサンプルコードをGitHubに用意したので、RubyやXcodeがあれば実際に動かすことができます。

https://github.com/pi-chan/integration-sample-rails

https://github.com/pi-chan/integration-sample-ios

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.rbforce_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を許すドメインを指定してあげたほうが便利です。

http://mushikago.com/i/?p=6150

※p2.OAuth2ライブラリ内のコードに、開こうとしているURLがhttpのときはassertで止まるような記述があります。あまりよろしくないですが開発中はコメントアウトしています。

URLスキームを設定する

p2.OAuth2を使った認証の流れですが、

  1. doorkeeperで取得したIDやトークンを設定する
  2. SafariまたはSafariViewを開いてログインを求める
  3. URLスキームを使ってアプリに戻る

といった感じになっています。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の連携ができるようになりました。ライブラリに頼りっきりではありますが、その分連携するまでをサッと作ることができます。アプリケーションそのものに時間を使ったほうがいいですからね。

何回か書いていますが、サンプルコードを見てもらったほうがわかりやすいと思います。