【问题标题】:Spring OAuth2: DuplicateKeyException when using JdbcTokenStore and DefaultTokenServices under heavy loadSpring OAuth2:在重负载下使用 JdbcTokenStore 和 DefaultTokenServices 时出现 DuplicateKeyException
【发布时间】:2017-04-20 05:58:56
【问题描述】:

正如标题中提到的,当同一个客户端同时查询令牌端点时(两个进程同时为同一个客户端请求令牌),我遇到了这个问题。

身份验证服务器日志中的消息如下所示:

2016-12-05 19:08:03.313  INFO 31717 --- [nio-9999-exec-5] o.s.s.o.provider.endpoint.TokenEndpoint  : Handling error: DuplicateKeyException, PreparedStatementCallback; SQL [insert into oauth_access_token (token_id, token, authentication_id, user_name, client_id, authentication, refresh_token) values (?, ?, ?, ?, ?, ?, ?)]; ERROR: duplicate key value violates unique constraint "oauth_access_token_pkey"
      Detail: Key (authentication_id)=(4148f592d600ab61affc6fa90bcbf16f) already exists.; nested exception is org.postgresql.util.PSQLException: ERROR: duplicate key value violates unique constraint "oauth_access_token_pkey"
      Detail: Key (authentication_id)=(4148f592d600ab61affc6fa90bcbf16f) already exists.

我正在使用带有这样的表的 PostgreSQL:

CREATE TABLE oauth_access_token
(
  token_id character varying(256),
  token bytea,
  authentication_id character varying(256) NOT NULL,
  user_name character varying(256),
  client_id character varying(256),
  authentication bytea,
  refresh_token character varying(256),
  CONSTRAINT oauth_access_token_pkey PRIMARY KEY (authentication_id)
)

我的应用程序看起来像这样:

@SpringBootApplication
public class OAuthServTest {
   public static void main (String[] args) {
      SpringApplication.run (OAuthServTest.class, args);
   }

   @Configuration
   @EnableAuthorizationServer
   @EnableTransactionManagement
   protected static class OAuth2Config extends AuthorizationServerConfigurerAdapter {

      @Autowired
      private AuthenticationManager authenticationManager;

      @Autowired
      private DataSource            dataSource;

      @Bean
      public PasswordEncoder passwordEncoder ( ) {
         return new BCryptPasswordEncoder ( );
      }

      @Override
      public void configure (AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
         endpoints.authenticationManager (authenticationManager);
         endpoints.tokenServices (tokenServices ( ));
      }

      @Override
      public void configure (ClientDetailsServiceConfigurer clients) throws Exception {
         clients.jdbc (this.dataSource).passwordEncoder (passwordEncoder ( ));
      }

      @Override
      public void configure (AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
         oauthServer.passwordEncoder (passwordEncoder ( ));
      }

      @Bean
      public TokenStore tokenStore ( ) {
         return new JdbcTokenStore (this.dataSource);
      }

      @Bean
      @Primary
      public AuthorizationServerTokenServices tokenServices ( ) {
         final DefaultTokenServices defaultTokenServices = new DefaultTokenServices ( );
         defaultTokenServices.setTokenStore (tokenStore ( ));
         return defaultTokenServices;
      }

   }
}

我的研究总是把我引向this problem。但是这个错误很久以前就修复了,我使用的是最新版本的 Spring Boot (v1.4.2)。

我的猜测是我做错了什么并且在事务中没有检索到DefaultTokenServices 中的令牌?

【问题讨论】:

    标签: spring spring-security spring-boot oauth spring-security-oauth2


    【解决方案1】:

    问题在于DefaultTokenServices 上的竞争条件。

    我找到了解决方法。不是说它很华丽,但它确实有效。这个想法是使用围绕TokenEndpoint 的AOP Advice 添加重试逻辑:

    @Aspect
    @Component
    public class TokenEndpointRetryInterceptor {
    
      private static final int MAX_RETRIES = 4;
    
      @Around("execution(* org.springframework.security.oauth2.provider.endpoint.TokenEndpoint.*(..))")
      public Object execute (ProceedingJoinPoint aJoinPoint) throws Throwable {
        int tts = 1000;
        for (int i=0; i<MAX_RETRIES; i++) {
          try {
            return aJoinPoint.proceed();
          } catch (DuplicateKeyException e) {
            Thread.sleep(tts);
            tts=tts*2;
          }
        }
        throw new IllegalStateException("Could not execute: " + aJoinPoint.getSignature().getName());
      }
    
    }
    

    【讨论】:

    • 您能详细说明一下吗?我有同样的错误,为什么我需要这个?我的 Spring Boot 应用程序中没有:@Aspect@AroundProceedingJoinPoint。我该如何解决同样的问题?
    【解决方案2】:

    如果您没有 Spring 并且使用带有 EJB 的普通 JEE,您可以考虑为此使用数据库表级别的解决方法。我创建了以下触发器,通过附加时间戳和随机字符串来修改重复的 authentication_id(和令牌),因此它不会失败并引发异常。不是理想的解决方案,但可以完成工作。

    DELIMITER $$
    
    CREATE TRIGGER handle_duplicate_authentication_id
    
    BEFORE INSERT ON oauth_access_token
    FOR EACH ROW
    BEGIN
    
      declare suffix varchar(20);
      SET suffix = CONCAT(UNIX_TIMESTAMP(),SUBSTRING(MD5(RAND()) FROM 1 FOR 8));
    
      IF (EXISTS(SELECT 1 FROM oauth_access_token WHERE authentication_id = NEW.authentication_id)) THEN
            SET NEW.authentication_id = CONCAT(NEW.authentication_id, suffix);
      END IF;
      IF (EXISTS(SELECT 1 FROM oauth_access_token WHERE token_id = NEW.token_id)) THEN
            SET NEW.token_id = CONCAT(NEW.token_id, suffix);
      END IF;
    END$$
    DELIMITER ;
    

    【讨论】:

      【解决方案3】:

      https://github.com/spring-projects/spring-security-oauth/issues/1033

       @Bean
      public TokenStore tokenStore(final DataSource dataSource) {
          final JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
          final AuthenticationKeyGenerator authenticationKeyGenerator = new DefaultAuthenticationKeyGenerator();
          return new JdbcTokenStore(dataSource) {
      
              @Override
              public void storeAccessToken(final OAuth2AccessToken token, final OAuth2Authentication authentication) {
                  final String key = authenticationKeyGenerator.extractKey(authentication);
                  jdbcTemplate.update("delete from oauth_access_token where authentication_id = ?", key);
                  super.storeAccessToken(token, authentication);
              }
      
          };
      }
      

      【讨论】:

        猜你喜欢
        • 2014-05-11
        • 2018-11-21
        • 2018-01-20
        • 2015-07-02
        • 2015-09-20
        • 2023-03-26
        • 1970-01-01
        • 2021-07-17
        • 1970-01-01
        相关资源
        最近更新 更多