年末の思い出をシェアしよう、Cognitoを使った写真共有サービスを作ろう – ClassmethodサーバーレスAdvent Calendar 2017 #serverless #adventcalendar

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

こんにちは、田中孝明です。

このエントリは Serverless Advent Calendar 2017 4日目の記事です。

いよいよ2017年も終わりが近づき、忘年会、クリスマス、年越しとイベントが目白押しではないでしょうか?

思い出のシェアとなるとInstagramやfacebook、Google Photoなど、様々な写真共有サービスが候補にあがるでしょう。
だた、中には限られた人たちにだけ公開したい、特別な思い出もあるかもしれません。

そんなあなたにとって、 Amazon CognitoAmazon S3 は、希望を叶えてくれる力強い存在となるでしょう。

やりたいこと

  • Amazon Cognito でユーザー認証
  • 認証したユーザーでiOS端末から写真を送信
  • 写真を Amazon S3 に保存

Amazon Cognitoを使った認証

Google認証

ユーザーの認証にGoogleアカウントを利用することにしました。

Google をセットアップするという項目を参考にGoogleアカウントで認証ができる状態にします。

Google 開発者コンソールに移動し、任意のGoogleアカウントでログインし、新しいプロジェクトを作成します。

「Social」 を検索し、「Google+ API」 を有効にします。

iOS 向け OAuth 2.0 Client ID を作成します。

[Credentials] > [Add Credentials] で、サービスアカウントを作成します。コンソールで、新しいパブリック/プライベートキーが作成されたことが警告されます。

Client ID の作成まで終了したら次はAmazon Cognitoにidentity poolの作成しましょう

identity poolの作成

AWSのコンソールにログインし、「Cognito」を選択します。

「Manage Federated Identities」をクリックします。

「Identity pool name」 に任意のアプリ名を入力します。

「Authentication providers」で「Google+」を選択し、Google 開発者コンソールから入手した「Client ID」を入力します。

「create pool」クリック時にAuthとUnAuthのIAMロールの作成も行なってくれます。
用意しているものがなければここで作成し、「Allow」をクリックしましょう。

S3 Bucketへの権限作成

IAM設定

AWS Management ConsoleからIAMを選択し、「Roles」をクリックします。 ここでCognitoの「create pool」時に作成されたAuthのRoleを選択します。

ポリシーにS3のPutの権限を付与しておきましょう。

こうすることで、Cognitoの認証がとったユーザーのみ、S3のPut権限を与えることができます。

iOSの実装

Googleサインインの設定

Google Sign-In for iOSに従って設定を行います。

GoogleServices-Info.plist を作成するところから始めましょう。

任意のAppNameとiOSのBundle IDを入力し、「Choose and configure service」を選択します。

「ENABLE GOOGLE SIGN-IN」でサインインを有効にします。

GoogleServices-Info.plist をダウンロードします。

サインイン処理の作成

サインイン画面の作成に従って作業を行います。
サインインを行うために CocoaPodsGoogle/SignIn SDKを組み込みます。

pod 'Google/SignIn'

GoogleServices-Info.plist をXcodeに組み込みます。

Xcodeの 「Target」 > 「Info」 > 「URL Types」 に GoogleServices-Info.plistREVERSED_CLIENT_ID の値を入力します。

AppDelegateの protocolGIDSignInDelegate を追加します。

import GoogleSignIn

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate, GIDSignInDelegate {

didFinishLaunchingWithOptions にsign-inの初期化処理を記載します。
YOUR_CLIENT_ID には発行されたClient IDを入力します。

func application(application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
    // sign-inの初期化処理
    GIDSignIn.sharedInstance().clientID = "YOUR_CLIENT_ID"
    GIDSignIn.sharedInstance().delegate = self

    return true
}

URLスキーマでアプリに遷移した際の処理をopenURLで行うようにします。

public func application(_ app: UIApplication, open url: URL, options: [UIApplicationOpenURLOptionsKey : Any] = [:]) -> Bool {
    return GIDSignIn.sharedInstance().handle(url as URL!,
        sourceApplication: options[UIApplicationOpenURLOptionsKey.sourceApplication] as? String,
        annotation: options[UIApplicationOpenURLOptionsKey.annotation])
}

AppDelegateに signIn を実装します。

func sign(_ signIn: GIDSignIn!, didSignInFor user: GIDGoogleUser!, withError error: Error!) {
    if (error == nil) {
        // Perform any operations on signed in user here.
        let userId = user.userID                  // For client-side use only!
        let idToken = user.authentication.idToken // Safe to send to the server
        let fullName = user.profile.name
        let givenName = user.profile.givenName
        let familyName = user.profile.familyName
        let email = user.profile.email
        // ...
    } else {
        print("\(error.localizedDescription)")
    }
}

サインイン画面の作成

ログイン画面に相当するUIViewControllerに GIDSignInUIDelegate を適用します。

import GoogleSignIn

class LoginViewController: UIViewController, GIDSignInUIDelegate {
    ...
}

viewDidLoad で初期化処理とサインインボタンの配置を行います。
GIDSignInButton でボタンを配置するとテーマがGoogleのログインボタンになります。

override func viewDidLoad() {
    super.viewDidLoad()

    GIDSignIn.sharedInstance().uiDelegate = self

    ...

    // Signin Buttonの配置
    let signInButton = GIDSignInButton()
    signInButton.center = view.center
    view.addSubview(_signInButton)
}

GIDSignInUIDelegate のメソッドを実装します。

// Stop the UIActivityIndicatorView animation that was started when the user
// pressed the Sign In button
func signInWillDispatch(signIn: GIDSignIn!, error: NSError!) {
    // stop indicator
}

// Present a view that prompts the user to sign in with Google
func signIn(signIn: GIDSignIn!,
    presentViewController viewController: UIViewController!) {
    // signin
    self.present(viewController, animated: true, completion: nil)
}

// Dismiss the "Sign in with Google" view
func signIn(signIn: GIDSignIn!,
    dismissViewController viewController: UIViewController!) {
    self.dismiss(animated: true, completion: nil)
}

ここまでの作業で、ログイン画面を作ることができました。

Googleログイン後の認証情報の登録

「identity poolの作成」の項目で作成したidentity poolから「Identity pool ID」を確認します。

iOSで必要なAWS SDKを取得します。

pod 'AWSS3'
pod 'AWSCognito'
pod 'AWSCognitoIdentityProvider'

AWSIdentityProviderManagerのサブクラスを作成し、 logins 処理を仲介させるようにします。

// AWSIdentityProviderManagerのサブクラス
class GoogleProvider: NSObject, AWSIdentityProviderManager {
    var tokens : [NSString : NSString]?

    init(tokens: [NSString : NSString]) {
        self.tokens = tokens
    }

    public func logins() -> AWSTask<NSDictionary> {
        let token: NSDictionary = NSDictionary(dictionary: tokens!)
        return AWSTask(result: token)
    }
}

先ほどログイン画面を作る際に編集した AppDelegate に認証成功後の処理を追加します。
YOUR_IDENTITY_POOL_ID には先ほど確認した「Identity pool ID」を入力します。

import AWSCognito
import AWSCognitoIdentityProvider
import AWSCore
import GoogleSignIn

class AppDelegate: UIResponder, UIApplicationDelegate, GIDSignInDelegate {
    func sign(_ signIn: GIDSignIn!, didSignInFor user: GIDGoogleUser!, withError error: Error!) {
        if (error == nil) {
            // authentication token取得
            let idToken = user.authentication.idToken

            // Initialize the Amazon Cognito credentials provider
            let provider = GoogleProvider(tokens: [AWSIdentityProviderGoogle as NSString: idToken as! NSString])
            let credentialsProvider = AWSCognitoCredentialsProvider(regionType: .APNortheast1,
                identityPoolId:"ap-northeast-1:32e6c955-1859-4222-aa09-2f15a7ddbedc",
                identityProviderManager: provider)
            let configuration = AWSServiceConfiguration(region: .APNortheast1,
                credentialsProvider: credentialsProvider)

            AWSServiceManager.default().defaultServiceConfiguration = configuration
        } else {
            print("\(error.localizedDescription)")
        }
    }

    ...

AWSCognitoCredentialsProvidergetIdentityId をコールします。

credentialsProvider.getIdentityId().continueOnSuccessWith(block: { [weak self] task -> Any? in
    if credentialsProvider.identityId != nil {
        // login!
    }
    return nil
})

AWS Management ConsoleからAWS CognitoのIdentity browserを確認すると、認証済みの端末が登録されていることが確認できます。

写真の送信

カメラの画像取得処理については割愛します。
画像を保存したURLから AWSS3 を使い、指定したS3のBucketへPutする処理を作成します。

func upload(imageURL: URL, createAt: Date) -> AWSTask<AnyObject> {
    let request = AWSS3TransferManagerUploadRequest()

    request?.bucket = "report-camera"
    request?.key = "images/original/\(createAt.timeIntervalSince1970)-\(imageURL.lastPathComponent)"
    request?.body = imageURL

    return AWSS3TransferManager.default().upload(request!)
}

これをカメラの画像取得時に呼べば、S3に写真がPutされるようになります。

let data = UIImagePNGRepresentation(image)
let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
let documentsDirectory = paths[0]

let filename = documentsDirectory.appendingPathComponent("copy.png")
try? data?.write(to: filename)

upload(imageURL: filename, createAt: Date()).continueWith(block: { task -> Any? in
    print(task)
})

まとめ

「Firebase SDK」を使った方がいいなど、色々ツッコミどころはあると思いますが、 Amazon CognitoはTwitter、facebookなどの認証にも対応しているため、S3へのPut以外にも応用はききます。
面倒な認証の実装を代理してもらうことで、アプリの実装により集中できるのではないでしょうか。