记 Spring OAuth2 登录时创建多个相同 token
      
        
          
          2022-03-06 11:24:21
        
        
              
                
                
                
                  
                    #Java
                  
                
                
                
                  
                    #Spring
                  
                
                
                
                  
                    #OAuth2
                  
                
                
              
          
       
      
        | 12
 
 | org.springframework.security.oauth2.common.exceptions.OAuth2Exception: Incorrect result size: expected 1, actual 2
 
 | 
这个错误是由于使用的token持久化策略是JdbcTokenStore创建token时出现了并发

我们可以看到DefaultTokenServices这边是没有做任何并发处理的

既然出现了并发,我们只需要控制不出现并发即可,这边有三个解决方案
1.1 唯一索引
添加唯一索引可以有效防止并发创建相同token
| 1
 | ALTER TABLE oauth_access_token ADD unique (authentication_id);
 | 
1.2 锁
我们可以自己写一个TokenServices去继承DefaultTokenServices即可,然后重写createAccessToken,使用悲观锁锁住代码块即可
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 
 | package com.doway.cloud.center.common;
 import org.springframework.security.core.AuthenticationException;
 import org.springframework.security.oauth2.common.OAuth2AccessToken;
 import org.springframework.security.oauth2.provider.OAuth2Authentication;
 import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
 
 
 
 
 
 
 
 public class CustomTokenServices extends DefaultTokenServices {
 @Override
 public synchronized OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) throws AuthenticationException {
 return super.createAccessToken(authentication);
 }
 }
 
 | 
需要注意的是我们这里使用的是synchronized(使用lock锁也可),如果项目是集群部署,此时synchronized是只能锁住当前进程的资源,还是无法防止并发,这时我们可以采用redis来实现分布式锁
1.3 设置事务隔离级别为串行化
我们知道数据库隔离级别有四种(MYSQL),数据库默认为可重复读(REPEATABLE READ),该级别默认查询是没有读锁的,不能防止并发,Spring使用的就是数据库默认隔离级别^1

这时我们可以设置隔离级别为串行化(SERIALIZABLE),此时读操作也会加锁,保证不同事物互斥,达到了防止并发的目的
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 
 | package com.doway.cloud.center.common;
 import org.springframework.security.core.AuthenticationException;
 import org.springframework.security.oauth2.common.OAuth2AccessToken;
 import org.springframework.security.oauth2.provider.OAuth2Authentication;
 import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
 import org.springframework.transaction.annotation.Isolation;
 import org.springframework.transaction.annotation.Transactional;
 
 
 
 
 
 
 public class CustomTokenServices extends DefaultTokenServices {
 @Transactional(isolation = Isolation.SERIALIZABLE)
 @Override
 public OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) throws AuthenticationException {
 return super.createAccessToken(authentication);
 }
 }
 
 | 
此时去调用父类的 createAccessToken ,父类的事务会加入到当前事务中来(Spring事务传播行为)
1.4 参考