Duplicate Oauth2 AccessTokens Created


Background


ソリューションのOAuth 2機能では、次の間欠的なエラーが発生したことを示す通知が表示されます.
省略...
Incorrect result size: expected 1, actual 2
...
...JdbcTemplate.queryForObject(JdbcTemplate.java:797)
...JdbcTokenStore.getAccessToken(JdbcTokenStore.java:105)
...JadeTokenService.createAccessToken(JadeTokenService.java:112)
省略...

CheckPoint


OAuth 2はSpringのOAuth 2機能を利用しているので,以下に示すように,まず検索を行い,関連する問題があるかどうかを決定した.
https://github.com/spring-projects/spring-security-oauth/issues/276
https://github.com/jhipster/generator-jhipster/issues/1759
https://github.com/jhipster/generator-jhipster/pull/1782
前述したように、1番目のSpring Outh 2 Oldバージョンにはトランザクションがない場合があり、2番目の同じclient idが複数のtokenを同時に要求すると、2つ以上の同じtokenがDBに格納される場合があります.
ただし、Tokenをアプリケーション側から取り出す場合は、1つの値を待機して取り出すだけなので、2つ以上のクエリーがある場合は、すぐにエラーが発生する可能性があります.

Replayed


エラーまたはエラー操作で最も重要なのは、プロジェクトチームで発生した同じ現象を再プレゼンテーションすることです.
PostManとFilderは、同じリクエストをコピーし、複数のリクエストを同時に呼び出すことで、同じ状況を再現します.

  • PostManを使用したトークン要求の発行


  • Fiddlerを利用した複数のリクエスト.500エラーを確認します.


  • 500 Error詳細
  • 14:23:35.760 [http-bio-8080-exec-8] ERROR        s.e.SpringWebExceptionHandler [EID:] [SS-ID:] [USER-ID:] - Incorrect result size: expected 1, actual 3[END]
    org.springframework.dao.IncorrectResultSizeDataAccessException: Incorrect result size: expected 1, actual 3
    	at org.springframework.dao.support.DataAccessUtils.requiredSingleResult(DataAccessUtils.java:74)
    	at org.springframework.jdbc.core.JdbcTemplate.queryForObject(JdbcTemplate.java:737)
    	at org.springframework.security.oauth2.provider.token.JdbcTokenStore.getAccessToken(JdbcTokenStore.java:113)
    	at 
    	...
  • 表に同じコインが3つ追加されていることを確認します.
  • Task


    前述したように、問題の原因が確認され、特定された場合は、次の操作を続行します.

    取引


    まず、JNDIを使用しているときのWASのautoCommit属性がfalseの場合、トランザクションが行われていない場合はコミットできない場合があります.これらの状況を考慮して、トランザクション終了後にコミットするためにトランザクションノイズ(@Transactional)を追加することもできます.
    public class JadeTokenService ... {
    	...
    	@Transacrional
    	public OAuth2AccessToken createAccessToken(...) throws AuthenticationException { ... }
    
    	@Transacrional
    	public OAuth2AccessToken refreshAccessToken(...) { ... }
    	...
    }

    PrimaryKeyの追加とレプリケーションExceptionの処理

  • PrimaryKeyを追加
    UTHENTICATION IDはPKとして定義される.
    /* 발급된 접근 토큰 */
    CREATE TABLE OAUTH_ACCESS_TOKEN (
    	TOKEN_ID VARCHAR2(256), /* 토큰 아이디 */
    	TOKEN BLOB, /* 토큰 */
    	AUTHENTICATION_ID VARCHAR2(256) NOT NULL, /* 인증 아이디 */
    	USER_NAME VARCHAR2(256), /* 사용자 명 */
    	CLIENT_ID VARCHAR2(256), /* 클라이언트 아이디 */
    	AUTHENTICATION BLOB, /* 인증 정보 */
    	REFRESH_TOKEN VARCHAR2(256) /* 리프레시 토큰 */
    );
    CREATE UNIQUE INDEX PK_OAUTH_ACCESS_TOKEN
    	ON OAUTH_ACCESS_TOKEN (
    		AUTHENTICATION_ID ASC
    );
    ALTER TABLE OAUTH_ACCESS_TOKEN
    	ADD
    		CONSTRAINT PK_OAUTH_ACCESS_TOKEN
    		PRIMARY KEY (
    			AUTHENTICATION_ID
    		);
  • 処理
  • コピーException
    DuplicateExceptionが発生した場合、Tokenクエリが処理されます.
    // https://github.com/spring-projects/spring-security-oauth/issues/502
    public class EnsureTokenService extends JadeTokenService {
    	
    	private static final Log LOG = LogFactory.getLog(EnsureTokenService.class);
    	
    	@Override
    	@Transactional
    	public OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) throws AuthenticationException {
    		try {
              return super.createAccessToken(authentication);
          }catch (DuplicateKeyException dke) {
          	LOG.info(String.format("Duplicate Primary Key(Authenticate ID) found for %s",authentication.getUserAuthentication().getPrincipal()));
              return super.getAccessToken(authentication);
          }catch (Exception ex) {
          	LOG.info(String.format("Exception while creating access token %s",ex));
          }
          return null;
    	}
    }