AndroidからGoogle Sign In経由、AWS Cognitoでuser管理してDynamo DBを操作する ~AWS Java SDKを添えて~
ふと、「そういえばNo SQLのDBって業務で関わったことないな」と思いついたのでAWS DynamoDBをさわってみることにした。
Androidから直接さわるためAWS Java SDKを利用したが、ググって引っかかる情報は結構古くてもう存在しないAPIを使っていたり、公式documentのsampleコードも使えなかったりと苦労したのでメモを残しておく。
とはいえSDKのコードはgithubで公開されているし、Java docも用意されているので、これからさわる人はこっちからコードを辿ったりした方が早いかもしれない。
1.やったこと概要
- Android appからGoogle Sign In
- 1で取得したtokenを用いてAWS CognitoにOpen ID Connectでlog in
- 2で取得したcredentialを用いてAWS DynamoDBのtableをread/write
2. 手順詳細
Google Sign In を実装する
まずは画面ぽちぽちしてFirebase側の設定を終わらす。このあたりを参考にしたらできるので手順は省略。
設定が終わったらコードを書いていく。まずapp moduleのbuild.gradleにDependenciesを追加する。
implementation 'com.google.firebase:firebase-auth:19.3.0' implementation 'com.google.android.gms:play-services-auth:17.0.0'
認証画面のonCreate()
でGoogleSignInClient
を初期化。
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val gso = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN) .requestIdToken(getString(R.string.default_web_client_id)) .requestEmail() .build() googleSignInClient = GoogleSignIn.getClient(this, gso) }
onStart()
ですでにGoogle Log in 済み状態かどうか取得。前回のLog in情報が残っていればGoogle Sign Inはスキップ。
override fun onStart() { super.onStart() val account = GoogleSignIn.getLastSignedInAccount(this) if (account == null) { authorizeWithGoogle() } else { logInToCognito(account) } }
未Log in状態であればGoogleが用意しているLog inボタンを表示してclick listenerを設定する。
private fun authorizeWithGoogle() { setContentView(R.layout.activity_launch) findViewById<SignInButton>(R.id.sign_in_button).apply { setSize(SignInButton.SIZE_WIDE) setOnClickListener { // 第二引数のrequest code(RC_SIGN_IN)は適宜定義してください startActivityForResult(googleSignInClient.signInIntent, RC_SIGN_IN) } } }
<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".LaunchActivity"> <com.google.android.gms.common.SignInButton android:id="@+id/sign_in_button" android:layout_width="wrap_content" android:layout_height="wrap_content" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> </androidx.constraintlayout.widget.ConstraintLayout>
上記のボタンが押下されるとGoogleSignIn側でいい感じにLog in処理が走ってくれるので結果を受け取るコードを書く。
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) if (requestCode == RC_SIGN_IN) { handleSignInResult(GoogleSignIn.getSignedInAccountFromIntent(data)) } } private fun handleSignInResult(completedTask: Task<GoogleSignInAccount>) { try { val account = completedTask.getResult(ApiException::class.java) if (account != null) { logInToCognito(account) } else { // handle error case } } catch (ex: ApiException) { // handle error case } }
Open ID Connectを使用してGoogleのuser account情報を元にAWS Cognitoにsing in する
この辺りを参考にしてAWSのmanegement画面をぽちぽちしてIdentity Poolを作成する。
Cognitoの設定が終わったらコードを書く。
まずはbuild.gradleにaws sdkのdependenciesを追加
def aws_android_sdk_version = '2.15.0' implementation "com.amazonaws:aws-android-sdk-core:$aws_android_sdk_version" implementation "com.amazonaws:aws-android-sdk-ddb:$aws_android_sdk_version"
このサンプルコードでは画面間でCognitoCachingCredentialsProvider
を共有したいので、雑にApplicationクラスに持たせるようなコードを書いている。
private fun logInToCognito(account: GoogleSignInAccount) { val application = application as MyApplication application.initCredentialProvider(account.idToken) }
Applicationクラスはこんな感じ。
class MyApplication : Application() { var credentialsProvider: CognitoCachingCredentialsProvider? = null fun initCredentialProvider(token: String?) { credentialsProvider = CognitoCachingCredentialsProvider( applicationContext, YOUR_IDENTITY_POOL_ID, YOUR_REGION ).apply { logins = mapOf("accounts.google.com" to token) } } }
これでDynamo DBにアクセスするためのcredential providerの準備は完了。
Dynamo DBにアクセスする
再びAWSのmanagement画面側をぽちぽちしてDynamo DBにTableを定義する。Tableが定義されるとAmazon Resource Name (ARN)が発行されるので、CogniteでIdentity Poolを作った時に自動で作成されたIAM RoleにこのARNへのpermissionをつける。Resourceへのpermissionだけでなく、Read/WriteといったActionsへのpermissionも一緒につける。
ぽちぽちが終わったらAndroid側のコードに戻る。
Credential providerからAmazonDynamoDBClient
を生成してDynamo DBにアクセスする。ここでsetRegion()
を設定する必要があることに気がつかずしばらくはまってしまった…
val dynamoDBClient = AmazonDynamoDBClient(credentialsProvider).apply { setRegion(Region.getRegion(Regions.US_EAST_1)) // Regionは適宜置き換えてください }
ここから先実際にtableにアクセスするにはnetwork通信が走るので別threadで処理を実行する必要がある。サンプルコードでは雑にKotlin coroutine(しかも一部)で書いているので適宜使いたい非同期処理に置き換えてほしい。
まずはread系。table全体のitemsを取得したい時はこんな感じ。
withContext(Dispatchers.IO) { try { val result = dynamoDBClient.scan(ScanRequest(YOUR_TABLE_NAME)) // Do whatever you want } catch (e: NotAuthorizedException) { // We need to refresh token } catch (e: Exception) { // error handling } }
このresult
はScanResult
型になっていて、getItems()
経由で各itemのListを取得することができる。さらにこのListの型が<String, AttributeValue>
のMapとなっている。このAttributeValue
が若干曲者で、例えばNumber型が欲しい場合はgetN()
を呼ぶ必要があるが、戻り値自体はStringなのでtoInt()
を呼んだりしてキャストしてやる必要がある。詳細は下記のdoc参照。
また、これはscanに限らずtableにアクセスする場合全般に言える話だが、tokenが切れていたらNotAuthorizedException
がthrowされるので、この場合は手動でtokenをrefreshする必要がある。
条件を指定するquery()
の使い方はこんな感じ。
withContext(Dispatchers.IO) { try { val result = dynamoDBClient.query( QueryRequest(YOUR_TABLE_NAME).apply { keyConditionExpression = "id = :id" expressionAttributeValues = mapOf( ":id" to AttributeValue(credentialsProvider.identityId) ) } ) // Do whatever you want } catch (e: NotAuthorizedException) { // We need to refresh token } catch (e: Exception) { // error handling } }
上記の例ではid
というpartition keyが存在するtableに対し、ログイン中のユーザのIDに一致するitemsを取得している。
key condition expressionの記述方法はJava doc を参照。また、このdocに書いてある通り、
Use the ExpressionAttributeValues parameter to replace tokens such as :partitionval and :sortval with actual values at runtime.
とのことなので、AttributeValueでwrapしたvalueをsetExpressionAttributeValues()
で設定する必要がある。
次にWrite系。
まずはtableにitemを追加するputItem()
のメモ。
withContext(Dispatchers.IO) { try { dynamoDBClient.putItem( PutItemRequest( YOUR_TABLE_NAME, mapOf( "id" to AttributeValue(credentialsProvider.identityId), "name" to AttributeValue(account.displayName), "image_url" to AttributeValue(account.photoUrl.toString()) ) ) ) } catch (e: NotAuthorizedException) { // We need to refresh token } catch (e: Exception) { // error handling } }
追加したいItemのattributeをmapで渡すだけ。ここでもvalueはAttributeValue
でwrapする必要がある。
最後にAtomic Counterを使ってItemを更新する場合の例。
withContext(Dispatchers.IO) { try { dynamoDBClient.updateItem( UpdateItemRequest() .withTableName(YOUR_TABLE_NAME) .withKey( mapOf( "partition_key" to AttributeValue(partitionKey), "sort_key" to AttributeValue(sortKey) ) ) .apply { updateExpression = "add #count :i" expressionAttributeNames = mapOf( "#count" to "count" ) expressionAttributeValues = mapOf( ":i" to AttributeValue().apply { n = "1" } ) } ) } catch (e: NotAuthorizedException) { // We need to refresh token } catch (e: Exception) { // error handling } }
上記の例では、withKey()
でpartition keyとsort keyを渡して更新したいitemを指定し、更新の条件はapply{}
節の中に記述している。サンプルではcount
というNumber型のattributeを現在のものから1インクリメントしている。ちなみにAttributeValue
のpublic constructorはStringを取れるのだが、ここにStringである"1"を渡してしまうとString型のvalueとして処理されてしまうので、この場合はparameterなしconstructorを使ってinstanceを生成してからsetN()
でString "1"を渡す必要がある。
言いたいことは以上です。