【Android】使用Twitter SDK实现OAuth认证和获取书签功能
从Twitter API默认更改为v2已经过去了大约一年时间。在v2版本中,可以获取书签、获取空间等等,因此我想尝试用API v2在Android上开发一个应用程序,并开始进行调查。结果我发现官方发布了Java版的SDK。因此,这次我决定使用我的账号进行认证,并尝试获取书签。希望阅读本文的人会对“我想用API v2做些什么!”感到有所启发。
这篇文章能让我们了解的/可以做的事情是什么?
Twitter公式SDK(Twitter API v2)の導入方法
OAuth認証~認証されたユーザーのブックマーク取得までの実装
トークンの保管方法、無効化(ログアウト処理)
トークンの自動更新
根据这个内容,将制作一个简易应用程序的视频作为基础。
只需要一个选项:
这篇文章没有提及的事情/基本知识
-
- 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调用的流程。
这次要实施的内容包括以下四个要点。
-
- 登录
-
- API调用
-
- 登出
- 令牌更新
为了实施上述的三个要点,需要按照图示中的六个内容进行实施。
在制作图表时,我参考了这个网站。每个图表将执行以下处理。
-
- 获取认证和授权代码
通过浏览器访问认证页面并获取授权代码。
获取访问令牌
根据第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。
…
…
…
请查看文档中有关软件包的公开设置的详细信息。
在写完整篇文章之后,我意识到,不仅仅局限于浏览器,在创建一个供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}")
}
}
看起来一切都顺利进行,太棒了?
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 でネイティブアプリの実装サンプルを作ってみた