# spring security oauth2使用redis存储token
spring security oauth2使用redis来存储token的配置及在redis中的存储结构
# maven
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
# 配置
@Configuration
@EnableAuthorizationServer
public class OAuth2ServerConfig extends
AuthorizationServerConfigurerAdapter {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private RedisConnectionFactory connectionFactory;
@Override
public void configure(
AuthorizationServerEndpointsConfigurer endpoints)
throws Exception {
endpoints
.authenticationManager(authenticationManager)
.tokenStore(tokenStore());
}
@Bean
public TokenStore tokenStore() {
return new RedisTokenStore(connectionFactory);
}
@Override
public void configure(ClientDetailsServiceConfigurer clients)
throws Exception {
clients.inMemory()
.withClient("demoApp")
.secret("123456")
.authorizedGrantTypes("password", "authorization_code");
}
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.allowFormAuthenticationForClients();
}
}
这里配置了redis token store
# 配置文件需要指定redis地址
spring.redis.url=redis://localhost:6379
# redis中存储结构
127.0.0.1:6379> keys *
1) "client_id_to_access:client"
2) "auth:2950a1d8-b49f-4cef-91c3-7fd14add1afe"
3) "uname_to_access:client:user"
4) "access:2950a1d8-b49f-4cef-91c3-7fd14add1afe"
5) "auth_to_access:287b1b4095d75bc94942ea499ad78a0c"
可以看到这里存了5个key
# auth_to_access:287b1b4095d75bc94942ea499ad78a0c
127.0.0.1:6379> type auth_to_access:287b1b4095d75bc94942ea499ad78a0c
string
127.0.0.1:6379> get auth_to_access:287b1b4095d75bc94942ea499ad78a0c
"\xac\xed\x00\x05sr\x00Corg.springframework.security.oauth2.common.DefaultOAuth2AccessToken\x0c\xb2\x9e6\x1b$\xfa\xce\x02\x00\x06L\x00\x15additionalInformationt\x00\x0fLjava/util/Map;L\x00\nexpirationt\x00\x10Ljava/util/Date;L\x00\x0crefreshTokent\x00?Lorg/springframework/security/oauth2/common/OAuth2RefreshToken;L\x00\x05scopet\x00\x0fLjava/util/Set;L\x00\ttokenTypet\x00\x12Ljava/lang/String;L\x00\x05valueq\x00~\x00\x05xpsr\x00\x1ejava.util.Collections$EmptyMapY6\x14\x85Z\xdc\xe7\xd0\x02\x00\x00xpsr\x00\x0ejava.util.Datehj\x81\x01KYt\x19\x03\x00\x00xpw\b\x00\x00\x01jE\xceB\xc1xpsr\x00%java.util.Collections$UnmodifiableSet\x80\x1d\x92\xd1\x8f\x9b\x80U\x02\x00\x00xr\x00,java.util.Collections$UnmodifiableCollection\x19B\x00\x80\xcb^\xf7\x1e\x02\x00\x01L\x00\x01ct\x00\x16Ljava/util/Collection;xpsr\x00\x17java.util.LinkedHashSet\xd8l\xd7Z\x95\xdd*\x1e\x02\x00\x00xr\x00\x11java.util.HashSet\xbaD\x85\x95\x96\xb8\xb74\x03\x00\x00xpw\x0c\x00\x00\x00\x10?@\x00\x00\x00\x00\x00\x01t\x00\x03appxt\x00\x06bearert\x00$2950a1d8-b49f-4cef-91c3-7fd14add1afe"
这个key的命名结构是auth_to_access:OAuth2Authentication相关信息的加密值,默认是md5加密 具体存储的是OAuth2AccessToken的序列化的值
# uname_to_access:client:user
127.0.0.1:6379> type uname_to_access:client:user
list
127.0.0.1:6379> lrange uname_to_access:client:user 0 1
1) "\xac\xed\x00\x05sr\x00Corg.springframework.security.oauth2.common.DefaultOAuth2AccessToken\x0c\xb2\x9e6\x1b$\xfa\xce\x02\x00\x06L\x00\x15additionalInformationt\x00\x0fLjava/util/Map;L\x00\nexpirationt\x00\x10Ljava/util/Date;L\x00\x0crefreshTokent\x00?Lorg/springframework/security/oauth2/common/OAuth2RefreshToken;L\x00\x05scopet\x00\x0fLjava/util/Set;L\x00\ttokenTypet\x00\x12Ljava/lang/String;L\x00\x05valueq\x00~\x00\x05xpsr\x00\x1ejava.util.Collections$EmptyMapY6\x14\x85Z\xdc\xe7\xd0\x02\x00\x00xpsr\x00\x0ejava.util.Datehj\x81\x01KYt\x19\x03\x00\x00xpw\b\x00\x00\x01jE\xceB\xc1xpsr\x00%java.util.Collections$UnmodifiableSet\x80\x1d\x92\xd1\x8f\x9b\x80U\x02\x00\x00xr\x00,java.util.Collections$UnmodifiableCollection\x19B\x00\x80\xcb^\xf7\x1e\x02\x00\x01L\x00\x01ct\x00\x16Ljava/util/Collection;xpsr\x00\x17java.util.LinkedHashSet\xd8l\xd7Z\x95\xdd*\x1e\x02\x00\x00xr\x00\x11java.util.HashSet\xbaD\x85\x95\x96\xb8\xb74\x03\x00\x00xpw\x0c\x00\x00\x00\x10?@\x00\x00\x00\x00\x00\x01t\x00\x03appxt\x00\x06bearert\x00$2950a1d8-b49f-4cef-91c3-7fd14add1afe"
这个key的命名是uname_to_access:clientId:userId
value的结构是list,存储token的序列化值
# auth:2950a1d8-b49f-4cef-91c3-7fd14add1afe
127.0.0.1:6379> type auth:2950a1d8-b49f-4cef-91c3-7fd14add1afe
string
127.0.0.1:6379> get auth:2950a1d8-b49f-4cef-91c3-7fd14add1afe
"\xac\xed\x00\x05sr\x00Aorg.springframework.security.oauth2.provider.OAuth2Authentication\xbd@\x0b\x02\x16bR\x13\x02\x00\x02L\x00\rstoredRequestt\x00<Lorg/springframework/security/oauth2/provider/OAuth2Request;L\x00\x12userAuthenticationt\x002Lorg/springframework/security/core/Authentication;xr\x00Gorg.springframework.security.authentication.AbstractAuthenticationToken\xd3\xaa(~nGd\x0e\x02\x00\x03Z\x00\rauthenticatedL\x00\x0bauthoritiest\x00\x16Ljava/util/Collection;L\x00\adetailst\x00\x12Ljava/lang/Object;xp\x00sr\x00&java.util.Collections$UnmodifiableList\xfc\x0f%1\xb5\xec\x8e\x10\x02\x00\x01L\x00\x04listt\x00\x10Ljava/util/List;xr\x00,java.util.Collections$UnmodifiableCollection\x19B\x00\x80\xcb^\xf7\x1e\x02\x00\x01L\x00\x01cq\x00~\x00\x04xpsr\x00\x13java.util.ArrayListx\x81\xd2\x1d\x99\xc7a\x9d\x03\x00\x01I\x00\x04sizexp\x00\x00\x00\x00w\x04\x00\x00\x00\x00xq\x00~\x00\x0cpsr\x00:org.springframework.security.oauth2.provider.OAuth2Request\x00\x00\x00\x00\x00\x00\x00\x01\x02\x00\aZ\x00\bapprovedL\x00\x0bauthoritiesq\x00~\x00\x04L\x00\nextensionst\x00\x0fLjava/util/Map;L\x00\x0bredirectUrit\x00\x12Ljava/lang/String;L\x00\arefresht\x00;Lorg/springframework/security/oauth2/provider/TokenRequest;L\x00\x0bresourceIdst\x00\x0fLjava/util/Set;L\x00\rresponseTypesq\x00~\x00\x11xr\x008org.springframework.security.oauth2.provider.BaseRequest6(z>\xa3qi\xbd\x02\x00\x03L\x00\bclientIdq\x00~\x00\x0fL\x00\x11requestParametersq\x00~\x00\x0eL\x00\x05scopeq\x00~\x00\x11xpt\x00\x06clientsr\x00%java.util.Collections$UnmodifiableMap\xf1\xa5\xa8\xfet\xf5\aB\x02\x00\x01L\x00\x01mq\x00~\x00\x0expsr\x00\x11java.util.HashMap\x05\a\xda\xc1\xc3\x16`\xd1\x03\x00\x02F\x00\nloadFactorI\x00\tthresholdxp?@\x00\x00\x00\x00\x00\x06w\b\x00\x00\x00\b\x00\x00\x00\x04t\x00\rresponse_typet\x00\x04codet\x00\x04codet\x00\x0678NisIt\x00\ngrant_typet\x00\x12authorization_codet\x00\tclient_idq\x00~\x00\x14xsr\x00%java.util.Collections$UnmodifiableSet\x80\x1d\x92\xd1\x8f\x9b\x80U\x02\x00\x00xq\x00~\x00\tsr\x00\x17java.util.LinkedHashSet\xd8l\xd7Z\x95\xdd*\x1e\x02\x00\x00xr\x00\x11java.util.HashSet\xbaD\x85\x95\x96\xb8\xb74\x03\x00\x00xpw\x0c\x00\x00\x00\x10?@\x00\x00\x00\x00\x00\x01t\x00\x03appx\x01sq\x00~\x00#w\x0c\x00\x00\x00\x10?@\x00\x00\x00\x00\x00\x00xsq\x00~\x00\x17?@\x00\x00\x00\x00\x00\x00w\b\x00\x00\x00\x10\x00\x00\x00\x00xt\x00\x14http://www.baidu.compsq\x00~\x00#w\x0c\x00\x00\x00\x10?@\x00\x00\x00\x00\x00\x00xsq\x00~\x00#w\x0c\x00\x00\x00\x10?@\x00\x00\x00\x00\x00\x01q\x00~\x00\x1axsr\x00Oorg.springframework.security.authentication.UsernamePasswordAuthenticationToken\x00\x00\x00\x00\x00\x00\x01\xfe\x02\x00\x02L\x00\x0bcredentialsq\x00~\x00\x05L\x00\tprincipalq\x00~\x00\x05xq\x00~\x00\x03\x01sq\x00~\x00\asq\x00~\x00\x0b\x00\x00\x00\x00w\x04\x00\x00\x00\x00xq\x00~\x00.sr\x00Horg.springframework.security.web.authentication.WebAuthenticationDetails\x00\x00\x00\x00\x00\x00\x01\xfe\x02\x00\x02L\x00\rremoteAddressq\x00~\x00\x0fL\x00\tsessionIdq\x00~\x00\x0fxpt\x00\x0f0:0:0:0:0:0:0:1t\x00 BE24E6D5CDF30ABD858DE917B890719Apsr\x002org.springframework.security.core.userdetails.User\x00\x00\x00\x00\x00\x00\x01\xfe\x02\x00\aZ\x00\x11accountNonExpiredZ\x00\x10accountNonLockedZ\x00\x15credentialsNonExpiredZ\x00\aenabledL\x00\x0bauthoritiesq\x00~\x00\x11L\x00\bpasswordq\x00~\x00\x0fL\x00\busernameq\x00~\x00\x0fxp\x01\x01\x01\x01sq\x00~\x00 sr\x00\x11java.util.TreeSet\xdd\x98P\x93\x95\xed\x87[\x03\x00\x00xpsr\x00Forg.springframework.security.core.userdetails.User$AuthorityComparator\x00\x00\x00\x00\x00\x00\x01\xfe\x02\x00\x00xpw\x04\x00\x00\x00\x00xpt\x00\x04user"
这个key的命名结构是auth:token值 value的结构是string,存储的是OAuth2Authentication的序列化值
# client_id_to_access:client
127.0.0.1:6379> type client_id_to_access:client
list
127.0.0.1:6379> lrange client_id_to_access:client 0 -1
1) "\xac\xed\x00\x05sr\x00Corg.springframework.security.oauth2.common.DefaultOAuth2AccessToken\x0c\xb2\x9e6\x1b$\xfa\xce\x02\x00\x06L\x00\x15additionalInformationt\x00\x0fLjava/util/Map;L\x00\nexpirationt\x00\x10Ljava/util/Date;L\x00\x0crefreshTokent\x00?Lorg/springframework/security/oauth2/common/OAuth2RefreshToken;L\x00\x05scopet\x00\x0fLjava/util/Set;L\x00\ttokenTypet\x00\x12Ljava/lang/String;L\x00\x05valueq\x00~\x00\x05xpsr\x00\x1ejava.util.Collections$EmptyMapY6\x14\x85Z\xdc\xe7\xd0\x02\x00\x00xpsr\x00\x0ejava.util.Datehj\x81\x01KYt\x19\x03\x00\x00xpw\b\x00\x00\x01jE\xceB\xc1xpsr\x00%java.util.Collections$UnmodifiableSet\x80\x1d\x92\xd1\x8f\x9b\x80U\x02\x00\x00xr\x00,java.util.Collections$UnmodifiableCollection\x19B\x00\x80\xcb^\xf7\x1e\x02\x00\x01L\x00\x01ct\x00\x16Ljava/util/Collection;xpsr\x00\x17java.util.LinkedHashSet\xd8l\xd7Z\x95\xdd*\x1e\x02\x00\x00xr\x00\x11java.util.HashSet\xbaD\x85\x95\x96\xb8\xb74\x03\x00\x00xpw\x0c\x00\x00\x00\x10?@\x00\x00\x00\x00\x00\x01t\x00\x03appxt\x00\x06bearert\x00$2950a1d8-b49f-4cef-91c3-7fd14add1afe"
这个key命名结构是client_id_to_access:clientId value结构是list,存储OAuth2AccessToken的序列化值
# access:2950a1d8-b49f-4cef-91c3-7fd14add1afe
127.0.0.1:6379> type access:2950a1d8-b49f-4cef-91c3-7fd14add1afe
string
127.0.0.1:6379> get access:2950a1d8-b49f-4cef-91c3-7fd14add1afe
"\xac\xed\x00\x05sr\x00Corg.springframework.security.oauth2.common.DefaultOAuth2AccessToken\x0c\xb2\x9e6\x1b$\xfa\xce\x02\x00\x06L\x00\x15additionalInformationt\x00\x0fLjava/util/Map;L\x00\nexpirationt\x00\x10Ljava/util/Date;L\x00\x0crefreshTokent\x00?Lorg/springframework/security/oauth2/common/OAuth2RefreshToken;L\x00\x05scopet\x00\x0fLjava/util/Set;L\x00\ttokenTypet\x00\x12Ljava/lang/String;L\x00\x05valueq\x00~\x00\x05xpsr\x00\x1ejava.util.Collections$EmptyMapY6\x14\x85Z\xdc\xe7\xd0\x02\x00\x00xpsr\x00\x0ejava.util.Datehj\x81\x01KYt\x19\x03\x00\x00xpw\b\x00\x00\x01jE\xceB\xc1xpsr\x00%java.util.Collections$UnmodifiableSet\x80\x1d\x92\xd1\x8f\x9b\x80U\x02\x00\x00xr\x00,java.util.Collections$UnmodifiableCollection\x19B\x00\x80\xcb^\xf7\x1e\x02\x00\x01L\x00\x01ct\x00\x16Ljava/util/Collection;xpsr\x00\x17java.util.LinkedHashSet\xd8l\xd7Z\x95\xdd*\x1e\x02\x00\x00xr\x00\x11java.util.HashSet\xbaD\x85\x95\x96\xb8\xb74\x03\x00\x00xpw\x0c\x00\x00\x00\x10?@\x00\x00\x00\x00\x00\x01t\x00\x03appxt\x00\x06bearert\x00$2950a1d8-b49f-4cef-91c3-7fd14add1afe"
这个key的命名结构是access:token value的结构是string,存储的是OAuth2AccessToken的序列化值
# 源码
spring-security-oauth2-2.0.14.RELEASE-sources.jar!/org/springframework/security/oauth2/provider/token/store/redis/RedisTokenStore.java
public class RedisTokenStore 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:";
@Override
public void storeAccessToken(OAuth2AccessToken token, OAuth2Authentication authentication) {
byte[] serializedAccessToken = serialize(token);
byte[] serializedAuth = serialize(authentication);
byte[] accessKey = serializeKey(ACCESS + token.getValue());
byte[] authKey = serializeKey(AUTH + token.getValue());
byte[] authToAccessKey = serializeKey(AUTH_TO_ACCESS + authenticationKeyGenerator.extractKey(authentication));
byte[] approvalKey = serializeKey(UNAME_TO_ACCESS + getApprovalKey(authentication));
byte[] clientId = serializeKey(CLIENT_ID_TO_ACCESS + authentication.getOAuth2Request().getClientId());
RedisConnection conn = getConnection();
try {
conn.openPipeline();
conn.set(accessKey, serializedAccessToken);
conn.set(authKey, serializedAuth);
conn.set(authToAccessKey, serializedAccessToken);
if (!authentication.isClientOnly()) {
conn.rPush(approvalKey, serializedAccessToken);
}
conn.rPush(clientId, serializedAccessToken);
if (token.getExpiration() != null) {
int seconds = token.getExpiresIn();
conn.expire(accessKey, seconds);
conn.expire(authKey, seconds);
conn.expire(authToAccessKey, seconds);
conn.expire(clientId, seconds);
conn.expire(approvalKey, seconds);
}
OAuth2RefreshToken refreshToken = token.getRefreshToken();
if (refreshToken != null && refreshToken.getValue() != null) {
byte[] refresh = serialize(token.getRefreshToken().getValue());
byte[] auth = serialize(token.getValue());
byte[] refreshToAccessKey = serializeKey(REFRESH_TO_ACCESS + token.getRefreshToken().getValue());
conn.set(refreshToAccessKey, auth);
byte[] accessToRefreshKey = serializeKey(ACCESS_TO_REFRESH + token.getValue());
conn.set(accessToRefreshKey, refresh);
if (refreshToken instanceof ExpiringOAuth2RefreshToken) {
ExpiringOAuth2RefreshToken expiringRefreshToken = (ExpiringOAuth2RefreshToken) refreshToken;
Date expiration = expiringRefreshToken.getExpiration();
if (expiration != null) {
int seconds = Long.valueOf((expiration.getTime() - System.currentTimeMillis()) / 1000L)
.intValue();
conn.expire(refreshToAccessKey, seconds);
conn.expire(accessToRefreshKey, seconds);
}
}
}
conn.closePipeline();
} finally {
conn.close();
}
}
//......
}
spring-security-oauth2-2.0.14.RELEASE-sources.jar!/org/springframework/security/oauth2/provider/token/DefaultAuthenticationKeyGenerator.java
public String extractKey(OAuth2Authentication authentication) {
Map<String, String> values = new LinkedHashMap<String, String>();
OAuth2Request authorizationRequest = authentication.getOAuth2Request();
if (!authentication.isClientOnly()) {
values.put(USERNAME, authentication.getName());
}
values.put(CLIENT_ID, authorizationRequest.getClientId());
if (authorizationRequest.getScope() != null) {
values.put(SCOPE, OAuth2Utils.formatParameterList(new TreeSet<String>(authorizationRequest.getScope())));
}
return generateKey(values);
}
protected String generateKey(Map<String, String> values) {
MessageDigest digest;
try {
digest = MessageDigest.getInstance("MD5");
byte[] bytes = digest.digest(values.toString().getBytes("UTF-8"));
return String.format("%032x", new BigInteger(1, bytes));
} catch (NoSuchAlgorithmException nsae) {
throw new IllegalStateException("MD5 algorithm not available. Fatal (should be in the JDK).", nsae);
} catch (UnsupportedEncodingException uee) {
throw new IllegalStateException("UTF-8 encoding not available. Fatal (should be in the JDK).", uee);
}
}
# 小结
# 好处
使用redis存储token可以利用redis的过期时间来自动处理token的过期时间,而使用数据库来存储的话,则需要根据expired date来判断。
# 缺点
但是redis不能像关系数据库那样直接关联查询,因此需要自己额外构造需要关联的key来处理,具体使用需要多次查询。
# 排除refresh_token,主要key如下:
- auth_to_access:OAuth2Authentication相关信息加密后的值,value为string结构 这个主要是通过OAuth2Authentication来获取OAuth2AccessToken
- auth:token值,value为string结构 这个主要用来获取token的OAuth2Authentication,用来获取相应的权限信息
- client_id_to_access:clientId,value为list结构 这个主要是存储了每个clientId申请的OAuth2AccessToken的集合 方便用来审计和应急处理跟clientId相关的token
- access:token值,value为string 这个主要是通过token值来获取OAuth2AccessToken
- uname_to_access:clientId:userId,value的结构是list 存储OAuth2AccessToken的集合 主要是为了通过clientId,userId来获取OAuth2AccessToken集合,方便用来获取及revoke approval