0%

在SpringBoot1.5.x下如何使RedisTokenStore集群化

在SpringBoot1.5.x下如何使RedisTokenStore集群化

在 spring boot 1.5.x 下 spring-boot-starter-data-redis 默认使用 jedis 作为客户端。

因为 JedisCluster 不支持集群的管道操作(pipleline),但是项目中又要用到 Redis 集群,这时候该怎么办呢?

现在,提供两种解决办法:

  1. 重写 RedisTokenStore, 用 RedisTemplateTokenStore
  2. 将 jedis 换掉,使用 spring boot 2.x 中默认的 redis 客户端 lettuce 来支持 Redis 集群(推荐)

解决办法 1:重写 RedisTokenStore

因为 JedisCluster 不支持管道操作:(源码)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class JedisClusterConnection implements RedisClusterConnection {
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.RedisConnection#openPipeline()
*/
@Override
public void openPipeline() {
throw new UnsupportedOperationException("Pipeline is currently not supported for JedisClusterConnection.");
}

/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.RedisConnection#closePipeline()
*/
@Override
public List<Object> closePipeline() throws RedisPipelineException {
throw new UnsupportedOperationException("Pipeline is currently not supported for JedisClusterConnection.");
}
}

因此可以使用 RedisTemplate 重写 RedisTokenStore,虽然会导致性能的损失,但至少能用不是吗

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
package com.fengxuechao.examples.auth.provider.token.store;

import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.common.OAuth2RefreshToken;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.token.AuthenticationKeyGenerator;
import org.springframework.security.oauth2.provider.token.DefaultAuthenticationKeyGenerator;
import org.springframework.security.oauth2.provider.token.TokenStore;

import java.util.*;
import java.util.concurrent.TimeUnit;

/**
* @author fengxuechao
* @version 0.1
* @date 2019/6/21
*/
public class RedisTemplateTokenStore implements TokenStore {
private static final String ACCESS = "access:";
private static final String AUTH_TO_ACCESS = "auth_to_access:";
private static final String AUTH = "auth:";
private static final String REFRESH_AUTH = "refresh_auth:";
private static final String ACCESS_TO_REFRESH = "access_to_refresh:";
private static final String REFRESH = "refresh:";
private static final String REFRESH_TO_ACCESS = "refresh_to_access:";
private static final String CLIENT_ID_TO_ACCESS = "client_id_to_access:";
private static final String UNAME_TO_ACCESS = "uname_to_access:";

private RedisTemplate<String,Object> redisTemplate ;

public RedisTemplate<String,Object> getRedisTemplate() {
return redisTemplate;
}

public void setRedisTemplate(RedisTemplate<String,Object> redisTemplate) {
this.redisTemplate = redisTemplate;
}

private AuthenticationKeyGenerator authenticationKeyGenerator = new DefaultAuthenticationKeyGenerator();

public void setAuthenticationKeyGenerator(AuthenticationKeyGenerator authenticationKeyGenerator) {
this.authenticationKeyGenerator = authenticationKeyGenerator;
}

@Override
public OAuth2AccessToken getAccessToken(OAuth2Authentication authentication) {
String key = authenticationKeyGenerator.extractKey(authentication);
OAuth2AccessToken accessToken = (OAuth2AccessToken) redisTemplate.opsForValue().get(AUTH_TO_ACCESS+key);
if (accessToken != null
&& !key.equals(authenticationKeyGenerator.extractKey(readAuthentication(accessToken.getValue())))) {
// Keep the stores consistent (maybe the same user is represented by this authentication but the details
// have changed)
storeAccessToken(accessToken, authentication);
}
return accessToken;
}

@Override
public OAuth2Authentication readAuthentication(OAuth2AccessToken token) {
return readAuthentication(token.getValue());
}

@Override
public OAuth2Authentication readAuthentication(String token) {
return (OAuth2Authentication) this.redisTemplate.opsForValue().get(AUTH + token);
}

@Override
public OAuth2Authentication readAuthenticationForRefreshToken(OAuth2RefreshToken token) {
return readAuthenticationForRefreshToken(token.getValue());
}

public OAuth2Authentication readAuthenticationForRefreshToken(String token) {
return (OAuth2Authentication) this.redisTemplate.opsForValue().get( REFRESH_AUTH+token);
}

@Override
public void storeAccessToken(OAuth2AccessToken token, OAuth2Authentication authentication) {

this.redisTemplate.opsForValue().set(ACCESS+ token.getValue(), token);
this.redisTemplate.opsForValue().set(AUTH +token.getValue(), authentication);
this.redisTemplate.opsForValue().set(AUTH_TO_ACCESS+authenticationKeyGenerator.extractKey(authentication), token);
if (!authentication.isClientOnly()) {
redisTemplate.opsForList().rightPush(UNAME_TO_ACCESS+getApprovalKey(authentication), token) ;
}

redisTemplate.opsForList().rightPush(CLIENT_ID_TO_ACCESS+authentication.getOAuth2Request().getClientId(), token) ;

if (token.getExpiration() != null) {

int seconds = token.getExpiresIn();
redisTemplate.expire(ACCESS+ token.getValue(), seconds, TimeUnit.SECONDS) ;
redisTemplate.expire(AUTH+ token.getValue(), seconds, TimeUnit.SECONDS) ;

redisTemplate.expire(AUTH_TO_ACCESS+ authenticationKeyGenerator.extractKey(authentication), seconds, TimeUnit.SECONDS) ;
redisTemplate.expire(CLIENT_ID_TO_ACCESS+authentication.getOAuth2Request().getClientId(), seconds, TimeUnit.SECONDS) ;
redisTemplate.expire(UNAME_TO_ACCESS+ getApprovalKey(authentication), seconds, TimeUnit.SECONDS) ;
}
if (token.getRefreshToken() != null && token.getRefreshToken().getValue() != null) {
this.redisTemplate.opsForValue().set( REFRESH_TO_ACCESS+ token.getRefreshToken().getValue(), token.getValue());
this.redisTemplate.opsForValue().set(ACCESS_TO_REFRESH+token.getValue(), token.getRefreshToken().getValue());
}
}

private String getApprovalKey(OAuth2Authentication authentication) {
String userName = authentication.getUserAuthentication() == null ? "" : authentication.getUserAuthentication()
.getName();
return getApprovalKey(authentication.getOAuth2Request().getClientId(), userName);
}

private String getApprovalKey(String clientId, String userName) {
return clientId + (userName==null ? "" : ":" + userName);
}

@Override
public void removeAccessToken(OAuth2AccessToken accessToken) {
removeAccessToken(accessToken.getValue());
}

@Override
public OAuth2AccessToken readAccessToken(String tokenValue) {
return (OAuth2AccessToken) this.redisTemplate.opsForValue().get(ACCESS+tokenValue);
}

public void removeAccessToken(String tokenValue) {
OAuth2AccessToken removed = (OAuth2AccessToken) redisTemplate.opsForValue().get(ACCESS+tokenValue);
// Don't remove the refresh token - it's up to the caller to do that
OAuth2Authentication authentication = (OAuth2Authentication) this.redisTemplate.opsForValue().get(AUTH+tokenValue);

this.redisTemplate.delete(AUTH+tokenValue);
redisTemplate.delete(ACCESS+tokenValue);
this.redisTemplate.delete(ACCESS_TO_REFRESH +tokenValue);

if (authentication != null) {
this.redisTemplate.delete(AUTH_TO_ACCESS+authenticationKeyGenerator.extractKey(authentication));

String clientId = authentication.getOAuth2Request().getClientId();
// redisTemplate.opsForList().rightPush("UNAME_TO_ACCESS:"+getApprovalKey(authentication), token) ;
redisTemplate.opsForList().leftPop(UNAME_TO_ACCESS+getApprovalKey(clientId, authentication.getName()));
redisTemplate.opsForList().leftPop(CLIENT_ID_TO_ACCESS+clientId);

this.redisTemplate.delete(AUTH_TO_ACCESS+authenticationKeyGenerator.extractKey(authentication));
}
}

@Override
public void storeRefreshToken(OAuth2RefreshToken refreshToken, OAuth2Authentication authentication) {
this.redisTemplate.opsForValue().set(REFRESH+refreshToken.getValue(), refreshToken);
this.redisTemplate.opsForValue().set( REFRESH_AUTH + refreshToken.getValue(), authentication);
}

@Override
public OAuth2RefreshToken readRefreshToken(String tokenValue) {
return (OAuth2RefreshToken) this.redisTemplate.opsForValue().get(REFRESH+tokenValue);
}

@Override
public void removeRefreshToken(OAuth2RefreshToken refreshToken) {
removeRefreshToken(refreshToken.getValue());
}

public void removeRefreshToken(String tokenValue) {
this.redisTemplate.delete( REFRESH + tokenValue);
this.redisTemplate.delete( REFRESH_AUTH + tokenValue);
this.redisTemplate.delete(REFRESH_TO_ACCESS +tokenValue);
}

@Override
public void removeAccessTokenUsingRefreshToken(OAuth2RefreshToken refreshToken) {
removeAccessTokenUsingRefreshToken(refreshToken.getValue());
}

private void removeAccessTokenUsingRefreshToken(String refreshToken) {

String token = (String) this.redisTemplate.opsForValue().get( REFRESH_TO_ACCESS +refreshToken) ;

if (token != null) {
redisTemplate.delete(ACCESS+ token);
}
}

@Override
public Collection<OAuth2AccessToken> findTokensByClientIdAndUserName(String clientId, String userName) {
List<Object> result = redisTemplate.opsForList().range(UNAME_TO_ACCESS+ getApprovalKey(clientId, userName), 0, -1);

if (result == null || result.size() == 0) {
return Collections.<OAuth2AccessToken> emptySet();
}
List<OAuth2AccessToken> accessTokens = new ArrayList<OAuth2AccessToken>(result.size());

for(Iterator<Object> it = result.iterator(); it.hasNext();){
OAuth2AccessToken accessToken = (OAuth2AccessToken) it.next();
accessTokens.add(accessToken);
}

return Collections.<OAuth2AccessToken> unmodifiableCollection(accessTokens);
}

@Override
public Collection<OAuth2AccessToken> findTokensByClientId(String clientId) {
List<Object> result = redisTemplate.opsForList().range((CLIENT_ID_TO_ACCESS+clientId), 0, -1);

if (result == null || result.size() == 0) {
return Collections.<OAuth2AccessToken> emptySet();
}
List<OAuth2AccessToken> accessTokens = new ArrayList<OAuth2AccessToken>(result.size());

for(Iterator<Object> it = result.iterator();it.hasNext();){
OAuth2AccessToken accessToken = (OAuth2AccessToken) it.next();
accessTokens.add(accessToken);
}

return Collections.<OAuth2AccessToken> unmodifiableCollection(accessTokens);
}
}

解决办法 2:使用 lettuce 替换 jedis

我们可以使用 Lettuce 来替代 jedis,况且 lettuce 也是 spring boot 2.x 中默认的 redis 客户端。

POM

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<exclusions>
<exclusion>
<artifactId>jedis</artifactId>
<groupId>redis.clients</groupId>
</exclusion>
</exclusions>
</dependency>
<!-- lettuce 客户端 -->
<dependency>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
<version>5.0.5.RELEASE</version>
<scope>compile</scope>
</dependency>
<!-- lettuce 客户端 -->
<dependency>
<groupId>biz.paluch.redis</groupId>
<artifactId>lettuce</artifactId>
<version>4.5.0.Final</version>
</dependency>
<!-- lettuce 连接池 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>

配置文件 application.yml

1
2
3
4
5
6
7
8
9
spring:
redis:
cluster:
nodes: 192.168.213.13:7001,192.168.213.14:7003,192.168.213.21:7006
max-redirects: 5
logging:
level:
root: info
com.fengxuechao.examples.auth: debug

配置 LettuceConnectionFactory 和 RedisTokenStore

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
package com.fengxuechao.examples.auth.config;

import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisClusterConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;

/**
* Redis 配置
*
* @author fengxuechao
* @version 0.1
* @date 2019/6/24
*/
@EnableConfigurationProperties(RedisProperties.class)
@Configuration
public class RedisConfig {

/**
* 使用 lettuce 作为 redis 的连接池
*
* @param configuration
* @return
*/
@Bean
public LettuceConnectionFactory lettuceConnectionFactory(RedisClusterConfiguration configuration) {
return new LettuceConnectionFactory(configuration);
}

/**
* lettuce 集群配置
*/
@Bean
public RedisClusterConfiguration getClusterConfiguration(RedisProperties redisProperties) {
RedisProperties.Cluster clusterProperties = redisProperties.getCluster();
RedisClusterConfiguration config = new RedisClusterConfiguration(clusterProperties.getNodes());

if (clusterProperties.getMaxRedirects() != null) {
config.setMaxRedirects(clusterProperties.getMaxRedirects());
}
return config;
}

@Bean
public TokenStore tokenStore(LettuceConnectionFactory lettuceConnectionFactory) {
return new RedisTokenStore(lettuceConnectionFactory);
}
}