質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.35%
排他制御

排他制御とは、特定のファイル・データへのアクセスや更新を制御することです。特にファイルやデータベースへ書き込みを行う際、データの整合性を保つため別のプログラムによる書き込みを一時的に制御することを指します。

Android

Androidは、Google社が開発したスマートフォンやタブレットなど携帯端末向けのプラットフォームです。 カーネル・ミドルウェア・ユーザーインターフェイス・ウェブブラウザ・電話帳などのアプリケーションやソフトウェアをひとつにまとめて構成。 カーネル・ライブラリ・ランタイムはほとんどがC言語/C++、アプリケーションなどはJavaSEのサブセットとAndroid環境で書かれています。

Kotlin

Kotlinは、ジェットブレインズ社のアンドリー・ブレスラフ、ドミトリー・ジェメロフが開発した、 静的型付けのオブジェクト指向プログラミング言語です。

Q&A

0回答

1512閲覧

kotlinでサーバーへのログイン処理に排他制御をかける方法について

bob_yama

総合スコア0

排他制御

排他制御とは、特定のファイル・データへのアクセスや更新を制御することです。特にファイルやデータベースへ書き込みを行う際、データの整合性を保つため別のプログラムによる書き込みを一時的に制御することを指します。

Android

Androidは、Google社が開発したスマートフォンやタブレットなど携帯端末向けのプラットフォームです。 カーネル・ミドルウェア・ユーザーインターフェイス・ウェブブラウザ・電話帳などのアプリケーションやソフトウェアをひとつにまとめて構成。 カーネル・ライブラリ・ランタイムはほとんどがC言語/C++、アプリケーションなどはJavaSEのサブセットとAndroid環境で書かれています。

Kotlin

Kotlinは、ジェットブレインズ社のアンドリー・ブレスラフ、ドミトリー・ジェメロフが開発した、 静的型付けのオブジェクト指向プログラミング言語です。

0グッド

1クリップ

投稿2020/06/29 00:21

前提・実現したいこと

androidアプリのログイン処理と排他制御につきましてお聞きしたいことがあります。
現在、サーバへのアクセスに用いているsession_idの有効期限が切れた場合(code == 401)に自動的に、
再ログインを行う処理を実装しようとしています。
session_idはAccaoutManagerをラップしたMyAccountManagerというクラスで管理しております。

少しややこしいのですが、androidアプリからはwww.example1.comをwww.example2.jpという二つのドメインにアクセスしており、www.example1.comではaccess_token、www.example2.jpではsession_idという有効期限の異なるtokenを用いてアクセスしています。またwww.example2.jpのログインはwww.example1.comのaccess_tokenを認証情報として利用しています。

ここからが本題なのですが、サーバーへのアクセスは複数スレッド上で非同期に発生しているため、session_idの有効期限が切れ401レスポンスが複数返ってくると、再ログインとsession_idの更新が複数回発生してしまっています。
再ログインとsession_idの更新の処理に排他制御をかけて、401レスポンスが複数回返ってきたときも再ログインが1回しか行われないようにしたいのですが、critical sectionを実行しているスレッドが複数存在してしまい、うまくいきません。

具体的には、401レスポンスが返ってきた後に、loginExampel2()メソッドを呼び出すと、getSessionId critical section enterのログが複数回出力されたのち、getSessionId critical section exitのログが同じ回数出力されます。
httpアクセスのログを見ても再ログインが複数回発生しているようです。

どなたか問題点を指摘していただけると幸いです。

該当のソースコード

kotlin

1/** 2 * singleton class for managing access token and session id. 3 */ 4 5class MyAccountManager private constructor(private val context: Context) { 6 7 companion object { 8 // for singleton 9 @Volatile 10 private var instance: MyAccountManager? = null 11 12 fun get(context: Context): MyAccountManager { 13 return instance 14 ?: synchronized(this) { 15 instance 16 ?: MyAccountManager(context) 17 .also { instance = it } 18 } 19 } 20 21 const val TAG = "MyAccountManager" 22 const val ACCOUNT_TYPE = "com.example.android" 23 const val REFRESH_TOKEN = "refresh_token" // www.example1.com 24 const val ACCESS_TOKEN = "access_token" // www.example1.com 25 const val SESSION_ID = "session_id" // www.example2.jp 26 27 val ACCESS_TOKEN_LOCK = Object() 28 val SESSION_ID_LOCK = Object() 29 var accessTokenCache = "" 30 var sessionIdCache = "" 31 } 32 33 34 class TokenResult(val code: Int, val token: String? = null) { 35 companion object { 36 const val CODE_OK = 0 37 const val CODE_NETWORK_ERROR = 1 38 const val CODE_AUTHENTICATION_ERROR = 2 39 } 40 } 41 42 43 private val accountManager: AccountManager by lazy { AccountManager.get(context) } 44 val userId: String? 45 get() = accountManager.accounts.getOrNull(0)?.name 46 47 @Synchronized 48 suspend fun addAccount(activity: Activity?): String? = withContext(Dispatchers.Default) { 49 userId?.let { 50 return@withContext it 51 } 52 var userId: String? = null 53 try { 54 val accountManagerFeature = accountManager.addAccount( 55 ACCOUNT_TYPE, 56 REFRESH_TOKEN, 57 null, 58 null, 59 activity, 60 null, 61 null 62 ) 63 val bundle = accountManagerFeature.result 64 userId = bundle.getString(AccountManager.KEY_ACCOUNT_NAME) 65 } catch (e: Exception) { 66 Log.e(TAG, "failed to add account", e) 67 } 68 return@withContext userId 69 } 70 71 fun removeAccount() { 72 accountManager.getAccountsByType(ACCOUNT_TYPE).forEach { account -> 73 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) { 74 accountManager.removeAccount(account, null, null, null) 75 } else { 76 accountManager.removeAccount(account, null, null) 77 } 78 } 79 } 80 81 /** @return access token if login to www.exampel1.com succeeded */ 82 suspend fun loginExample1(activity: Activity?): String? = withContext(Dispatchers.Default) { 83 invalidateAccessToken() 84 val tokenResult = getAccessToken() 85 when (tokenResult?.code) { 86 TokenResult.CODE_OK -> { 87 return@withContext tokenResult.token 88 } 89 TokenResult.CODE_AUTHENTICATION_ERROR -> { 90 removeAccount() 91 addAccount(activity) 92 val tokenResult2 = getAccessToken() 93 return@withContext tokenResult2?.token 94 } 95 else -> { 96 return@withContext null 97 } 98 } 99 } 100 101 /** @return session id if login to example2 succeeded */ 102 suspend fun loginExampel2(activity: Activity): String? = withContext(Dispatchers.Default) { 103 invalidateSessionId() 104 val tokenResult = getSessionId() 105 when (tokenResult?.code) { 106 TokenResult.CODE_OK -> { 107 return@withContext tokenResult.token 108 } 109 TokenResult.CODE_AUTHENTICATION_ERROR -> { 110 if (loginMyrova(activity) != null) { 111 val tokenResult2 = getSessionId() 112 return@withContext tokenResult2?.token 113 } else { 114 return@withContext null 115 } 116 } 117 else -> { 118 return@withContext null 119 } 120 } 121 } 122 123 // 無駄なログインをしないように、排他制御をかけたかったが。 124 // synchronized、ReenterLock、Mutexのどのやり方でもなぜか出来なかった。 125 private fun getAccessToken(): TokenResult? { 126 // 現在のaccess_tokenを保存 127 // accessTokenCacheはstatic変数 128 val prevAccessToken = accessTokenCache 129 var tokenResult: TokenResult? 130 // ACCESS_TOKEN_LOCKはstatic変数 131 // val ACCESS_TOKEN_LOCK = Object()で初期化している。 132 synchronized(ACCESS_TOKEN_LOCK) { 133 Log.d(TAG, "getAccessToken critical section enter") 134 // アクセストークンが更新されていた場合はログインせずにそれを返す。 135 if (prevAccessToken != accessTokenCache) { 136 return TokenResult(TokenResult.CODE_OK, accessTokenCache) 137 } 138 // refreshTokenを取得する。 139 // デバイスにAccountが登録されていなかった場合nullが返ってくる。 140 val refreshToken = getToken(REFRESH_TOKEN)?.token 141 ?: return TokenResult(TokenResult.CODE_AUTHENTICATION_ERROR) 142 val options = Bundle().apply { 143 putString(REFRESH_TOKEN, refreshToken) 144 } 145 // refreshTokenを用いてwww.example1.comのaccess_tokenを取得。 146 tokenResult = getToken(ACCESS_TOKEN, options) 147 // 取得に成功した場合は、accessTokenCacheを新しいaccess_tokenで更新。 148 tokenResult?.token?.let { 149 accessTokenCache = it 150 } 151 Log.d(TAG, "getAccessToken critical section exit") 152 } 153 return tokenResult 154 } 155 156 157 // 無駄なログインをしないように、排他制御をかけたかったが。 158 // synchronized、ReenterLock、Mutexのどのやり方でもなぜか出来なかった。 159 private fun getSessionId(): TokenResult? { 160 // 現在のsession_idを保存 161 // sessoinIdCacheはstatic変数 162 val prevSessionId = sessionIdCache 163 var tokenResult: TokenResult? 164 // SESSION_ID_LOCKはstatic変数 165 // val SESSION_ID_LOCK = Object()で初期化している。 166 synchronized(SESSION_ID_LOCK) { 167 Log.d(TAG, "getSessionId critical section enter") 168 if (prevSessionId != sessionIdCache) { 169 return TokenResult(TokenResult.CODE_OK, sessionIdCache) 170 } 171 // access_tokenを取得する。 172 // access_tokenが取得できなかった場合はnullが返ってくる。 173 val accessToken = getAccessToken()?.token 174 ?: return TokenResult(TokenResult.CODE_AUTHENTICATION_ERROR) 175 val options = Bundle().apply { 176 putString(ACCESS_TOKEN, accessToken) 177 } 178 // access_tokenを用いてwww.example2.jpのsession_idを取得。 179 // 取得に成功した場合は、sessionIdCaheを新しいsession_idで更新。 180 tokenResult = getToken(SESSION_ID, options) 181 tokenResult?.token?.let { 182 sessionIdCache = it 183 } 184 Log.d(TAG, "getSessionId critical section exit") 185 } 186 return tokenResult 187 } 188 189 private suspend fun invalidateAccessToken() = withContext(Dispatchers.Default) { 190 val token = getAccessToken()?.token 191 accountManager.invalidateAuthToken(ACCOUNT_TYPE, token) 192 } 193 194 private suspend fun invalidateSessionId() = withContext(Dispatchers.Default) { 195 val token = getSessionId()?.token 196 accountManager.invalidateAuthToken(ACCOUNT_TYPE, token) 197 } 198 199 private fun getToken(tokenType: String, options: Bundle? = null): TokenResult? { 200 val account = accountManager.getAccountsByType(ACCOUNT_TYPE).getOrNull(0) 201 ?: return null 202 203 var tokenResult: TokenResult? = null 204 try { 205 val accountManagerFuture = 206 accountManager.getAuthToken( 207 account, 208 tokenType, options, true, null, null 209 ) 210 val bundle = accountManagerFuture.result 211 val token = bundle.getString(AccountManager.KEY_AUTHTOKEN) 212 token?.let { 213 tokenResult = TokenResult(TokenResult.CODE_OK, it) 214 } 215 } catch (e: AuthenticatorException) { 216 Log.e(TAG, "failed to get token", e) 217 tokenResult = TokenResult(TokenResult.CODE_AUTHENTICATION_ERROR) 218 } catch (e: IOException) { 219 Log.e(TAG, "failed to access network", e) 220 tokenResult = TokenResult(TokenResult.CODE_NETWORK_ERROR) 221 } catch (e: Exception) { 222 Log.e(TAG, "unknown error has occurred", e) 223 } 224 225 return tokenResult 226 } 227} 228

補足情報(FW/ツールのバージョンなど)

テスト環境
android os 7
sdkversion 29
gradle version 4.0.0
kotlin version 1.3.72

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

hoshi-takanori

2020/07/02 18:34

もしかして、accessTokenCache と sessionIdCache に @Volatile が必要かも?
bob_yama

2020/07/07 01:58

返信遅れてしまい申し訳ありません。 ご指摘ありがとうございます。 synchronizedのキーとして用いている、以下の val ACCESS_TOKEN_LOCK = Object() val SESSION_ID_LOCK = Object() の部分を、 @Volatile var ACCESS_TOKEN_LOCK = Object() @Volatile var SESSION_ID_LOCK = Object() に変更してみましたが、解決しませんでした。
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

まだ回答がついていません

会員登録して回答してみよう

アカウントをお持ちの方は

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.35%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問