记 Spring OAuth2 登录时创建多个相同 token

1
2
// 错误
org.springframework.security.oauth2.common.exceptions.OAuth2Exception: Incorrect result size: expected 1, actual 2

这个错误是由于使用的token持久化策略是JdbcTokenStore创建token时出现了并发

image-20210420230307743

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

image-20210420230758197

既然出现了并发,我们只需要控制不出现并发即可,这边有三个解决方案

1.1 唯一索引

添加唯一索引可以有效防止并发创建相同token

1
ALTER TABLE oauth_access_token ADD unique (authentication_id);

1.2 锁

我们可以自己写一个TokenServices去继承DefaultTokenServices即可,然后重写createAccessToken,使用悲观锁锁住代码块即可

1
2
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;

/**

* @author dream.
* @description: TODO 类描述
* @date 2021/4/19
**/
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]

image-20210420233948035

这时我们可以设置隔离级别为串行化(SERIALIZABLE),此时读操作也会加锁,保证不同事物互斥,达到了防止并发的目的

1
2
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;

/**
* @author dream.
* @description: TODO 类描述
* @date 2021/4/19
**/
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 参考


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!