【Android】使用Twitter SDK实现OAuth认证和获取书签功能

从Twitter API默认更改为v2已经过去了大约一年时间。在v2版本中,可以获取书签、获取空间等等,因此我想尝试用API v2在Android上开发一个应用程序,并开始进行调查。结果我发现官方发布了Java版的SDK。因此,这次我决定使用我的账号进行认证,并尝试获取书签。希望阅读本文的人会对“我想用API v2做些什么!”感到有所启发。

这篇文章能让我们了解的/可以做的事情是什么?

Twitter公式SDK(Twitter API v2)の導入方法
OAuth認証~認証されたユーザーのブックマーク取得までの実装
トークンの保管方法、無効化(ログアウト処理)
トークンの自動更新

根据这个内容,将制作一个简易应用程序的视频作为基础。

qiita-02

只需要一个选项:

这篇文章没有提及的事情/基本知识

    • Android、Kotlinの基本的な仕様

 

    • OAuth2.0認証の詳細な内容

 

    • Twitter API v2を使うまでのデベロッパーアカウント登録方法

 

    Twitter API v2の詳細な仕様

环境

Android Studio Flamingo | 2022.2.1 鸟儿版 4
材料3: 1.0.0-beta03
Kotlin: 1.7.0
JetpackCompose: 1.2.1

有关认证到API调用的流程。

这次要实施的内容包括以下四个要点。

    1. 登录

 

    1. API调用

 

    1. 登出

 

    令牌更新

为了实施上述的三个要点,需要按照图示中的六个内容进行实施。

cb6c618f18c1febd904b2021079238af8c3d5d4d58f1386001a7a3a659c54bc0

在制作图表时,我参考了这个网站。每个图表将执行以下处理。

    1. 获取认证和授权代码

通过浏览器访问认证页面并获取授权代码。

获取访问令牌

根据第1步获得的代码,获取访问令牌和刷新令牌。

保留访问令牌

一旦获取访问令牌,将令牌持久化以避免在应用重新启动时需要进行第1和第2步的认证。

调用API

使用第2步获取的访问令牌进行需要认证的API调用。

令牌更新

使用第1步获得的刷新令牌来更新令牌。如果令牌过期,将自动更新。

令牌无效化(登出)

在应用内丢弃第2步获取的令牌,并在服务器端进行无效化。

接下来,我们将对每个处理内容进行代码解释。

代码 (Mandarin Chinese: “daima”)

添加依存关系

这次,我们要添加的依赖库是在以下网址上公开的TwitterSDK。
https://github.com/twitterdev/twitter-api-java-sdk

首先,在app/gradle目录下需要进行以下实现才能添加SDK的依赖关系。

dependencies {

    

	// Twitter-API-Java-SDK
	implementation ("com.twitter:twitter-api-java-sdk:2.0.3"){
        exclude group:'org.apache.oltu.oauth2' , module: 'org.apache.oltu.oauth2.common'
	    exclude module: 'listenablefuture'
	    exclude module: 'guava'
	}
}

在这里需要注意的是排除了重复的模块。如果直接添加,会出现类似的错误(重复错误),因此实施了上述内容。

构建错误详细信息:
在模块guava-15.0(com.google.guava:guava:15.0)和listenablefuture-1.0(com.google.guava:listenablefuture:1.0)中发现重复的类com.google.common.util.concurrent.ListenableFuture。
在模块org.apache.oltu.oauth2.client-1.0.1(org.apache.oltu.oauth2:org.apache.oltu.oauth2.client:1.0.1)和org.apache.oltu.oauth2.common-1.0.1(org.apache.oltu.oauth2:org.apache.oltu.oauth2.common:1.0.1)中发现重复的类org.apache.oltu.oauth2.common.OAuth。
在模块org.apache.oltu.oauth2.client-1.0.1(org.apache.oltu.oauth2:org.apache.oltu.oauth2.client:1.0.1)和org.apache.oltu.oauth2.common-1.0.1(org.apache.oltu.oauth2:org.apache.oltu.oauth2.common:1.0.1)中发现重复的类org.apache.oltu.oauth2.common.OAuth$ContentType。
在模块org.apache.oltu.oauth2.client-1.0.1(org.apache.oltu.oauth2:org.apache.oltu.oauth2.client

1. 获取认证和授权代码

初始化认证处理

在onCreate()函数中,我们对每个对象进行初始化。
同时,在这段代码中,我们参考了example对OAuth对象的初始化参数进行设置。

class MainActivity : ComponentActivity() {

    companion object {
        const val CALLBACK_URL = "app://"
        const val SCOPE = "offline.access tweet.read users.read bookmark.read"
        const val SECRET_STATE = "state"
        const val TWITTER_OAUTH2_CLIENT_ID = "Developer Portalで取得したCLIENT ID"
        const val TWITTER_OAUTH2_CLIENT_SECRET = "Developer Portalで取得したCLIENT SECRET ID"
        // 1時間半でトークンを更新(トークンの有効期限は2時間だが余裕を見るため)
        const val EXPIRY_TIME = 5400000
    }

    private lateinit var prefs: SharedPreferences

    private lateinit var credentials: TwitterCredentialsOAuth2
    private lateinit var service: TwitterOAuth20Service
    private lateinit var pkce: PKCE

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val prefs = getSharedPreferences("TwitterOauth", MODE_PRIVATE)

        credentials = TwitterCredentialsOAuth2(
            TWITTER_OAUTH2_CLIENT_ID,
            TWITTER_OAUTH2_CLIENT_SECRET,
            prefs.getString("OauthToken", ""),
            prefs.getString("OauthRefreshToken", "")
        )

        service = TwitterOAuth20Service(
            credentials.twitterOauth2ClientId,
            credentials.twitterOAuth2ClientSecret,
            CALLBACK_URL,
            SCOPE
        )

        pkce = PKCE().apply {
            codeChallenge = "challenge"
            codeChallengeMethod = PKCECodeChallengeMethod.PLAIN
            codeVerifier = "challenge"
        }

        
    }
}

打开认证用的URL

接下来我们将实现打开认证页面的处理。

通过使用`onCreate()`初始化的`pkce`和`state`作为参数调用`getAuthorizationUrl()`方法,我们可以创建认证页面的URL,并使用`Intent`在默认浏览器中跳转到认证页面。本次实现是使用默认浏览器跳转到认证页面,以下是具体内容。

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />


    <!--  Andoroid11以降は以下が必要  -->
    <queries>
        <intent>
            <action android:name="android.intent.action.VIEW"/>

            <data
                android:scheme="https"
                android:host="twitter.com"
                />
        </intent>
    </queries>

    <queries>
        <intent>
            <action android:name="android.intent.action.VIEW"/>
            <category android:name="android.intent.category.BROWSABLE"/>
            <data android:scheme="http"/>
        </intent>
    </queries>

    <application
        
        >
        <activity
            
            >

            <!--     コールバック用のIntent Filter      -->
            <intent-filter>
                <action android:name="android.intent.action.VIEW"/>
                <category android:name="android.intent.category.DEFAULT"/>
                <category android:name="android.intent.category.BROWSABLE"/>
                <data android:scheme="app" />
            </intent-filter>

        </activity>
    </application>

</manifest>
private fun openOAuthURL() {

    val authUrl = service.getAuthorizationUrl(pkce, SECRET_STATE)

    val intent = Intent(
        Intent.ACTION_VIEW,
        Uri.parse(authUrl)
    )
    val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse("https://"))
    val twitterIntent = Intent(Intent.ACTION_VIEW, Uri.parse("https://twitter.com/"))

    val existsDefBrows =
        packageManager.resolveActivity(browserIntent, PackageManager.MATCH_DEFAULT_ONLY)

    // 端末にツイッターアプリが入っているか確認
    val existsTwitterApp =
        packageManager.resolveActivity(twitterIntent, PackageManager.MATCH_DEFAULT_ONLY)

    runCatching {
        if (existsTwitterApp != null) {
            // ブラウザアプリがインストールされていたら
            if (existsDefBrows != null) {

                intent.setPackage(existsDefBrows.activityInfo.packageName)
                intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK

                startActivity(intent)
            }
        } else {
            startActivity(intent)
        }
    }.getOrElse {
        Log.i("openOAuthURL_Error", "${it.localizedMessage}")
    }

}

首先,我会检查设备上是否存在默认浏览器和Twitter应用(与Twitter链接的应用)。如果发现安装了Twitter应用,则在打开授权页面时会自动打开Twitter应用,导致OAuth认证无法完成。

为了避免这种情况,我会检查设备上每个应用的存在情况,如果发现已安装Twitter应用,则会打开默认浏览器;如果未安装,则会使用startActivity()打开相应的URL。

关于Android 11及以上的实施事项,请注意我们使用resolveActivity()来检查已安装在设备上的应用程序。在Android 11之前的设备上,这样的实施没有问题,但如果要将应用程序发布给11及以上的设备,请在AndrodManifest.xml中添加以下描述:

 

 

请查看文档中有关软件包的公开设置的详细信息。

在写完整篇文章之后,我意识到,不仅仅局限于浏览器,在创建一个供WebView使用的活动,并通过onActivityResult方法获取其结果,也是一个不错的选择。

我希望大家可以根据自己的喜好随意进行适当的变动。

2. 获取访问令牌

在onNewIntent()方法中获取认可代码。

override fun onNewIntent(intent: Intent?) {
        super.onNewIntent(intent)

        Log.i("onNewIntent", "onNewIntent")

        val uri = intent?.data
        val code = uri?.getQueryParameter("code")

        // 認可コードが正常に取得できていたらトークンを取得
        lifecycleScope.launch(Dispatchers.IO) {
            if(code != null) {
                getAccessToken(code)
            }
        }

}

您可以通过将此授权代码传递给getAccessToken()函数来获取访问令牌和刷新令牌。

 private fun getAccessToken(code: String?) {

    val accessToken = service.getAccessToken(pkce, code)

    storeToken(accessToken)

}

3. 存储访问令牌

将通过2.获取的访问令牌和刷新令牌存储在TwitterCredentialsOAuth2() 中。此外,为了在下次启动时无需重新认证,使用SharedPreference将其存储在内部存储中。

private fun storeToken(accessToken: OAuth2AccessToken) {

    val prefs = getSharedPreferences("TwitterOauth", MODE_PRIVATE)
    prefs.edit().apply {
        putString("OauthToken", accessToken.accessToken)
        putString("OauthRefreshToken", accessToken.refreshToken)
        putLong("getTokenTime", getTokenTime)
    }.apply()

    credentials.apply {
        twitterOauth2AccessToken = accessToken.accessToken
        twitterOauth2RefreshToken = accessToken.refreshToken
    }
}

4. 调用API(获取书签)

使用 getUsersIdBookmarks() 方法来获取收藏夹。
在 getUsersIdBookmarks() 方法的参数中设置经过1、2认证的用户的UserId。
可以使用 Postman 等工具来确认UserId。

private fun getBookmark() {
    Log.i("getBookMark", credentials.twitterOauth2AccessToken)
    Log.i("getBookMark", credentials.twitterOauth2RefreshToken)

    val apiInstance = TwitterApi(credentials)

    runCatching {
        apiInstance.bookmarks().getUsersIdBookmarks("認証ユーザーのUserIdを入力").execute()
    }.onSuccess {
        Log.i("BookMark_Success", "$it")
    }.onFailure {
        Log.i("BookMark_Failure_local", it.localizedMessage)
        Log.i("BookMark_Failure_message", "${it.message}")
        Log.i("BookMark_Failure_cause", "${it.cause}")
    }
}
3bded91bfd781b2d64f7b1732a62e49b6e06e7b2c02c294a59a0b9afbb2faa1d

看起来一切都顺利进行,太棒了?

5. 更新令牌

在配布的README文件中,通过将TwitterCredentialsOAuth2.isOAuth2AutoRefreshToken设置为true,似乎可以自动更新令牌。但是,根据我的测试,它并没有正常运作…(可能是因为我的实施方法有误)

因此,本次我們將實施以下程式碼,以使令牌可以自動更新。

class MainActivity : ComponentActivity() {
    companion object {
        
        

        // 1時間半でトークンを更新(トークンの有効期限は2時間だが余裕を見るため)
        const val EXPIRY_TIME = 5400000
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        

    }

    

    override fun onResume() {
        super.onResume()

        val getTokenTime = prefs.getLong("getTokenTime", 0)
        val now = System.currentTimeMillis()
        val elapsedTime = now - getTokenTime

        if (elapsedTime > EXPIRY_TIME) {
            lifecycleScope.launch(Dispatchers.IO) {
                refreshToken()
            }
        }
    }

    

    private suspend fun refreshToken() {

        val apiInstance = TwitterApi(credentials)

        apiInstance.addCallback {
            credentials.apply {
                twitterOauth2AccessToken = it.accessToken
                twitterOauth2RefreshToken = it.refreshToken
            }

            val refreshTokenTime = System.currentTimeMillis()

            prefs.edit().apply {
                putString("OauthToken", it.accessToken)
                putString("OauthRefreshToken", it.refreshToken)
                putLong("getTokenTime", refreshTokenTime)
            }.apply()

        }

        runCatching {
            apiInstance.refreshToken()
            withContext(Dispatchers.Main) {
                val toast = Toast.makeText(applicationContext, "トークン更新成功!", Toast.LENGTH_SHORT)
                toast.show()
            }
        }.getOrElse {
            withContext(Dispatchers.Main) {
                val toast = Toast.makeText(applicationContext, "トークン更新失敗", Toast.LENGTH_SHORT)
                toast.show()
            }
            Log.i("refreshToken_Fail", "$it")
        }
    }
    
}

首先,您需要在EXPIRY_TIME中设置令牌的有效截止时间。

根据文件显示,在Twitter的情况下,访问令牌的有效期限为2小时。不过,我们这次设置为1小时30分钟来更新令牌以确保充足的时间。

我的凭证会有效多长时间?
通常情况下,通过授权码流程(Authorization Code Flow)和PKCE创建的访问令牌(access token)在未使用offline.access范围的情况下,只会保持有效两个小时。
https://developer.twitter.com/en/docs/authentication/oauth-2-0/authorization-code

class MainActivity : ComponentActivity() {
    companion object {
        
        

        // 1時間半でトークンを更新(トークンの有効期限は2時間だが余裕を見るため)
        const val EXPIRY_TIME = 5400000
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        

    }
}

以下是使用刷新令牌来更新令牌的实现方法。这段代码是参考了示例代码。同时,为了将更新令牌的时间记录下来,它会将其存储在SharedPreferences中。

    private suspend fun refreshToken() {

        val apiInstance = TwitterApi(credentials)

        apiInstance.addCallback {
            credentials.apply {
                twitterOauth2AccessToken = it.accessToken
                twitterOauth2RefreshToken = it.refreshToken
            }

            val refreshTokenTime = System.currentTimeMillis()

            prefs.edit().apply {
                putString("OauthToken", it.accessToken)
                putString("OauthRefreshToken", it.refreshToken)
                putLong("getTokenTime", refreshTokenTime)
            }.apply()

        }

        runCatching {
            apiInstance.refreshToken()
            withContext(Dispatchers.Main) {
                val toast = Toast.makeText(applicationContext, "トークン更新成功!", Toast.LENGTH_SHORT)
                toast.show()
            }
        }.getOrElse {
            withContext(Dispatchers.Main) {
                val toast = Toast.makeText(applicationContext, "トークン更新失敗", Toast.LENGTH_SHORT)
                toast.show()
            }
            Log.i("refreshToken_Fail", "$it")
        }
    }

在 onResume() 中实现更新处理,以便在令牌过期时更新令牌。

    override fun onResume() {
        super.onResume()

        val getTokenTime = prefs.getLong("getTokenTime", 0)
        val now = System.currentTimeMillis()
        val elapsedTime = now - getTokenTime

        if (elapsedTime > EXPIRY_TIME) {
            lifecycleScope.launch(Dispatchers.IO) {
                refreshToken()
            }
        }
    }

6. 令牌的失效化(登出)

通过revokeToken在服务器端使令牌失效。
此外,在上述实现中,由于应用程序中仍然存在令牌,还实施了将SharedPreference和TwitterCredentialsOAuth2对象清空的处理。

private fun revokeToken() {

        service.revokeToken(credentials.twitterOauth2AccessToken, TokenTypeHint.ACCESS_TOKEN)

        val prefs = getSharedPreferences("TwitterOauth", MODE_PRIVATE)
        prefs.edit().apply {
            putString("OauthToken", "")
            putString("OauthRefreshToken", "")
        }.apply()

        credentials.apply {
            twitterOauth2AccessToken = ""
            twitterOauth2RefreshToken = ""
        }

    }

总结

    • 公式で公開しているライブラリを使ってOAuth認証~ブックマークの取得まで実装

 

    • 依存関係の追加に一部注意が必要

 

    • デフォルトブラウザを使って認証しアクセストークンの取得

 

    トークンの自動更新の実装

我们已经在下面公开了整个源代码,希望能对计划使用TwitterAPI开发应用程序的人有所帮助。(由于这段代码是由一个初学者编写的,所以可能有些难以理解的地方,请提出并指出,我们会很感激… ?)
CIDRA4023/TwitterAppTes

闲话不多

有许多不同的方法来实现OAuth认证,但选择中也包括了AppAuth for Android库。然而,使用这个库时,实现注销功能需要一些额外的工作,并且在API通信方面,有可能需要引入Retrofit等其他组件。因此,我个人认为本次选择使用包含认证和各API处理的官方SDK是最佳实践。

另外,转到认证页面时,可以创建适用于WebView的活动来接收结果,而不是使用默认浏览器…?

或许还有其他更好的实施方法,因此本次只是以使用官方SDK的方式作为参考,若能采纳将不胜感激。

请参考

安卓文档

    パッケージの公開設定を管理する

Twitter API 文档

    • OAuth 2.0 Authorization Code Flow with PKCE | Docs | Twitter Developer Platform

 

    GET /2/users/:id/bookmarks

我在OAuth认证方面参考了一篇文章。

    Kotlin と AppAuth for Android でネイティブアプリの実装サンプルを作ってみた
广告
将在 10 秒后关闭
bannerAds