この記事はソニックガーデン 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.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を許すドメインを指定してあげたほうが便利です。
http://mushikago.com/i/?p=6150
※p2.OAuth2ライブラリ内のコードに、開こうとしているURLがhttpのときはassertで止まるような記述があります。あまりよろしくないですが開発中はコメントアウトしています。
URLスキームを設定する
p2.OAuth2を使った認証の流れですが、
- doorkeeperで取得したIDやトークンを設定する
- SafariまたはSafariViewを開いてログインを求める
- 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の連携ができるようになりました。ライブラリに頼りっきりではありますが、その分連携するまでをサッと作ることができます。アプリケーションそのものに時間を使ったほうがいいですからね。
何回か書いていますが、サンプルコードを見てもらったほうがわかりやすいと思います。