LoginSignup
2
3

More than 3 years have passed since last update.

AndroidのreCAPTCHA APIを使う【私はロボットではありません】

Last updated at Posted at 2020-08-30

はじめに

機械的に大量に行われるユーザ登録やログイン試行、コメント投稿等を防ぐために、機械ではなく人間がUIが操作していることを確認する方法としてCAPTCHAがあります。GoogleはreCAPTCHAというサービスを提供していて、Android向けにもAPIを提供しているので使ってみました。

参考文献

この記事はAndroid公式サイトの「SafetyNet reCAPTCHA API」の内容に沿っています。

Google reCAPTCHAのサイトで登録する

必要事項を送信する

Google reCAPTCHAのサイトにアクセスして登録します。

  1. ラベルに分かりやすい名前を付けます。
  2. reCAPTCHAタイプを reCAPTCHA v2 にして、 reCAPTCHA Android を選択します。
  3. パッケージはAndroidアプリのパッケージ名を入力します。
  4. reCAPTCHA 利用条件に同意する にチェックを入れます。
  5. 送信 ボタンを押します。

スクリーンショット 2020-08-29 16.59.28.png

サイトキーとシークレットキーを保持する。

サイトキーとシークレットキーが表示されます。それらをこの画面で保存します。以後表示することは出来ません。

image.png

AndroidアプリにreCHAPTCHA v2を追加する

ライブラリの追加

モジュールレベルのbuild.gradleファイルにライブラリを追加します。

app/build.gradle
dependencies {
    // 追加
    implementation 'com.google.android.gms:play-services-safetynet:17.0.0'
}

入力画面を作る

今回はサンプルプログラムとしてコメント入力欄と投稿ボタンだけの簡単な画面を作りました。

投稿するときに自動プログラムによる入力でないことを確認する

MainActivity.kt
// 投稿ボタンをクリックしたときの処理
submit.setOnClickListener {
    SafetyNet.getClient(this)
        .verifyWithRecaptcha(getString(R.string.api_site_key)/* サイトキーを入力 */)
        .addOnSuccessListener { tokenResponse ->
            // 自動プログラムによる入力でないことの確認成功
            tokenResponse.tokenResult?.let {
                Log.d(TAG, "Success: $it")
            }
        }
        .addOnFailureListener {
            // 確認失敗
            if (it is ApiException) {
                Log.d(TAG, "Error: ${CommonStatusCodes.getStatusCodeString(it.statusCode)}")
            } else {
                Log.d(TAG, "Error: ${it.message}")
            }
        }
}

アプリを実行して投稿ボタンを押すと、このようなプログレスダイアログが表示されて、その後トークンを得ることが出来ます。トークンはサーバーサイドで検証する必要があるので、それを次の節で説明します。

デバッグのために何度か行っていると適切な画像を選択することを求められることがあります。

サーバサイドで検証する

トークンをサーバーサイドで検証するために、サーバサイドアプリケーションを開発します。この記事ではサーバサイドKotlinのWebフレームワークであるKtorを使用して開発していますが言語やフレームワークを問いません。サーバサイドでHTTPSのAPIが呼べれば何でも良いです。Ktorについては先日投稿した記事「Androidエンジニアが慣れている技術で作る維持費0円のサーバサイドアプリケーション」に本番環境へのデプロイまで解説しています。

トークン検証APIクライアントを作成する

トークンの検証はトークンを持ってGoogleのAPIを呼ぶことで出来ます。APIの仕様はこちらにあります。
Verifying the user's response

まずはサーバサイドで動作させるためのAPIクライアントをRetrofitで作ります。

build.gradle
dependencies {
    // Retrofit
    def retrofit_version = '2.9.0'
    implementation "com.squareup.retrofit2:retrofit:$retrofit_version"
    implementation "com.squareup.retrofit2:converter-gson:$retrofit_version"
}
VerifyResponse.kt
data class VerifyResponse(
    val success: Boolean,
    val challenge_ts: Date,
    val apk_package_name: String,
    @SerializedName("error-codes")
    val error_codes: List<String>
)
VerifyService.kt
interface VerifyService {
    @FormUrlEncoded
    @POST("/recaptcha/api/siteverify")
    suspend fun verify(
        @Field("secret") secret: String,
        @Field("response") response: String
    ): VerifyResponse
}
Application.kt
val retrofit = Retrofit.Builder()
    .addConverterFactory(GsonConverterFactory.create())
    .baseUrl("https://www.google.com/")
    .build()
val service = retrofit.create(VerifyService::class.java)

注意点としてレスポンス本文の形式が application/json であるのに対して、リクエスト本文の形式はapplication/x-www-form-urlencoded なので、間違えないようにしてください。

コメント投稿APIで検証を行う

コメントの投稿を受け付けるAPIのサーバサイドを作ります。

今回はこのようなAPIを作ります。
エンドポイント POST /comments
リクエスト本文

{
    "comment": "ユーザが記入したコメントです。",
    "token": "reCAPTCHAのトークンです。サーバサイドで検証します"
}

まず各アプリ固有の情報であるGoogle reCAPTCHAのサイトで登録したAndroidアプリのパッケージ名と、取得したシークレットキーを準備します。

Application.kt
val packageName = System.getenv("APK_PACKAGE_NAME")
val secret = System.getenv("RECAPTCHA_SECRET_KEY")

そしてAPIのエンドポイントを検証付きで作ります。

CommentRequest.kt
data class CommentRequest(val comment: String, val token: String)
Application.kt
routing {
    post("/comments") {
        // APIリクエストの取得
        val request = call.receive<CommentRequest>()
        // 検証API呼び出し
        val response = service.verify(secret, request.token)
        // 成功フラグとAndroidアプリのパッケージ名を確認する
        if (response.success &&
            response.apk_package_name == packageName
        ) {
            // 検証成功
            // request.comment をデータベースに書き込む等する
            call.respond(mapOf("result" to "ok", "comment" to request.comment))
        } else {
            // 検証失敗
            call.respond(HttpStatusCode.BadRequest)
        }
    }
}

AndroidアプリからAPIを呼び出す

あとはトークンと投稿内容を合わせてサーバに送信すれば良いです。

app/build.gradle
dependencies {
    def lifecycle_version = '2.2.0'
    // ViewModelとLiveData
    implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
    implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
    // by viewModels() 委譲プロパティを使えるようにする
    implementation "androidx.activity:activity-ktx:1.1.0"
    // Retrofit
    def retrofit_version = '2.9.0'
    implementation "com.squareup.retrofit2:retrofit:$retrofit_version"
    implementation "com.squareup.retrofit2:converter-gson:$retrofit_version"
}
CommentRequest.kt
data class CommentRequest(val comment: String, val token: String)
ApiService.kt
interface ApiService {
    @POST("/comments")
    suspend fun postComment(@Body request: CommentRequest)
}
MainViewModel.kt
class MainViewModel : ViewModel() {

    /**
     * URLはデプロイ先。今回はGoogle App Engineにデプロイした。
     */
    private val retrofit = Retrofit.Builder()
        .addConverterFactory(GsonConverterFactory.create())
        .baseUrl("https://recaptcha-dot-tfandkusu.appspot.com/")
        .build()

    private val service = retrofit.create(ApiService::class.java)

    /**
     * CAPTCHAの検証が成功
     */
    val success = MutableLiveData<Boolean>()

    /**
     * CAPTCHAの検証が失敗
     */
    val fail = MutableLiveData<String>()

    /**
     * プログレス表示
     */
    val progress = MutableLiveData(false)

    fun postComment(comment: String, token: String) = viewModelScope.launch {
        try {
            progress.value = true
            service.postComment(CommentRequest(comment, token))
            // 投稿成功
            success.value = true
        } catch (e: Throwable) {
            // 投稿失敗
            fail.value = e.toString()
        } finally {
            progress.value = false
        }
    }
}
MainActivity.kt
class MainActivity : AppCompatActivity() {
    private val viewModel: MainViewModel by viewModels()
    // 略
}
MainActivity.kt
SafetyNet.getClient(this)
    .verifyWithRecaptcha(getString(R.string.api_site_key)/* サイトキーを入力 */)
    .addOnSuccessListener { tokenResponse ->
        tokenResponse.tokenResult?.let {
            viewModel.postComment(comment, it)
        }
    }

全体ソースコード

Githubで公開しています。

サーバサイド
Androidアプリ

2
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
3