博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

使用Json改写基于对象序列化的RedisTokenStore

Posted on 2021-03-26 13:57  冲杀  阅读(1285)  评论(-1编辑  收藏  举报

之前使用的RedisTokenStroe存储token信息,由于它使用的是java二进制序列化的方式,将token信息存入Redis,导致我们在开发中就遇到了以下问题:

1. 如果UserDetails定义的字段发生增删,已存在的token,访问校验的时候,就会发生序列化错误;

2. 如果去redis中查看某个token的内容的时候,会发现全是乱码,完全看不懂;

于是某个晚上,我狠心就加班搞了4个小时,使用json改写了。

同时考虑到,如果每次token校验都去redis中读取数据,会导致redis压力增大,毕竟token信息都是些长json字符串,于是就使用了Caffeine做了本地缓存。

代码:

JsonRedisTokenStore

  1 public class JsonRedisTokenStore implements TokenStore {
  2     private static final String ACCESS = "access:";
  3     private static final String AUTH_TO_ACCESS = "auth_to_access:";
  4     private static final String AUTH = "auth:";
  5     private static final String REFRESH_AUTH = "refresh_auth:";
  6     private static final String ACCESS_TO_REFRESH = "access_to_refresh:";
  7     private static final String REFRESH = "refresh:";
  8     private static final String REFRESH_TO_ACCESS = "refresh_to_access:";
  9     private static final String CLIENT_ID_TO_ACCESS = "client_id_to_access:";
 10     private static final String UNAME_TO_ACCESS = "uname_to_access:";
 11 
 12     private final static Cache<Object, Object> CACHE;
 13 
 14     static {
 15         CACHE = Caffeine.newBuilder()
 16                 .expireAfterWrite(60, TimeUnit.SECONDS)
 17                 .maximumSize(1000).build();
 18     }
 19 
 20 
 21     private final StringRedisTemplate stringRedisTemplate;
 22     private final AuthenticationKeyGenerator authenticationKeyGenerator = new CustomAuthenticationKeyGenerator();
 23 
 24 
 25     public JsonRedisTokenStore(StringRedisTemplate stringRedisTemplate) {
 26         this.stringRedisTemplate = stringRedisTemplate;
 27     }
 28 
 29 
 30     @Override
 31     public OAuth2Authentication readAuthentication(OAuth2AccessToken token) {
 32         return this.readAuthentication(token.getValue());
 33     }
 34 
 35     @Override
 36     public OAuth2Authentication readAuthentication(String token) {
 37         String key = AUTH + token;
 38 
 39         return (OAuth2Authentication) loadCache(key, (k) -> {
 40             String json = stringRedisTemplate.opsForValue().get(key);
 41             if (StringUtils.isBlank(json)) {
 42                 return null;
 43             }
 44             return fullParseJSON(json);
 45         });
 46     }
 47 
 48     /**
 49      * 完整的OAuth2Authentication 对象转换
 50      *
 51      * @param json 完整OAuth2Authentication json字符串
 52      * @return OAuth2Authentication对象
 53      */
 54     private OAuth2Authentication fullParseJSON(String json) {
 55         //String json = "{\"authorities\":[{\"authority\":\"ROLE_system_role\"}],\"details\":null,\"authenticated\":true,\"userAuthentication\":{\"authorities\":[{\"authority\":\"ROLE_system_role\"}],\"details\":{\"client_secret\":\"secret\",\"grant_type\":\"password\",\"client_id\":\"system\",\"username\":\"test\"},\"authenticated\":true,\"principal\":{\"id\":2715353679532032,\"companyId\":null,\"password\":\"71b596cb42ee254f7416043d184fc970\",\"username\":\"运营\",\"phone\":\"test\",\"accountNonExpired\":true,\"accountNonLocked\":true,\"credentialsNonExpired\":true,\"enabled\":true,\"authorities\":[{\"authority\":\"ROLE_system_role\"}]},\"credentials\":null,\"name\":\"运营\"},\"credentials\":\"\",\"clientOnly\":false,\"oauth2Request\":{\"clientId\":\"system\",\"scope\":[\"all\"],\"requestParameters\":{\"grant_type\":\"password\",\"client_id\":\"system\",\"username\":\"test\"},\"resourceIds\":[\"base-server\",\"accident-system\",\"common-server\",\"auth-server\"],\"authorities\":[{\"authority\":\"client\"}],\"approved\":true,\"refresh\":false,\"redirectUri\":null,\"responseTypes\":[],\"extensions\":{},\"refreshTokenRequest\":null,\"grantType\":\"password\"},\"principal\":{\"id\":2715353679532032,\"companyId\":null,\"password\":\"71b596cb42ee254f7416043d184fc970\",\"username\":\"运营\",\"phone\":\"test\",\"accountNonExpired\":true,\"accountNonLocked\":true,\"credentialsNonExpired\":true,\"enabled\":true,\"authorities\":[{\"authority\":\"ROLE_system_role\"}]},\"name\":\"运营\"}";
 56         JSONObject jsonObject = JSONObject.parseObject(json);
 57 
 58         JSONObject userAuthenticationObject = jsonObject.getJSONObject("userAuthentication");
 59 
 60         TeacherUserDetails userInfoDetails = userAuthenticationObject.getObject("principal", TeacherUserDetails.class);
 61 
 62         String credentials = userAuthenticationObject.getString("credentials");
 63 
 64         JSONObject detailsJSONObject = userAuthenticationObject.getJSONObject("details");
 65         LinkedHashMap<String, Object> details = new LinkedHashMap<>();
 66         for (String key : detailsJSONObject.keySet()) {
 67             details.put(key, detailsJSONObject.get(key));
 68         }
 69 
 70         UsernamePasswordAuthenticationToken userAuthentication = new UsernamePasswordAuthenticationToken(userInfoDetails
 71                 , credentials, new ArrayList<>(0));
 72         userAuthentication.setDetails(details);
 73 
 74         JSONObject storedRequest = jsonObject.getJSONObject("oAuth2Request");
 75         String clientId = storedRequest.getString("clientId");
 76 
 77         JSONObject requestParametersJSON = storedRequest.getJSONObject("requestParameters");
 78         Map<String, String> requestParameters = new HashMap<>();
 79         for (String key : requestParametersJSON.keySet()) {
 80             requestParameters.put(key, requestParametersJSON.getString(key));
 81         }
 82 
 83         Set<String> scope = convertSetString(storedRequest, "scope");
 84         Set<String> resourceIds = convertSetString(storedRequest, "resourceIds");
 85         Set<String> responseTypes = convertSetString(storedRequest, "responseTypes");
 86 
 87         OAuth2Request oAuth2Request = new OAuth2Request(requestParameters
 88                 , clientId
            //由于这个项目不需要处理权限角色,所以就没有对权限角色集合做处理
89 , new ArrayList<>(0) 90 , storedRequest.getBoolean("approved") 91 , scope 92 , resourceIds 93 , storedRequest.getString("redirectUri") 94 , responseTypes 95 , null //extensionProperties 96 ); 97 98 return new OAuth2Authentication(oAuth2Request, userAuthentication); 99 } 100 101 private static Set<String> convertSetString(JSONObject data, String key) { 102 List<String> list = data.getJSONArray(key).toJavaList(String.class); 103 104 return new HashSet<>(list); 105 } 106 107 @Override 108 public void storeAccessToken(OAuth2AccessToken token, OAuth2Authentication authentication) { 109 String serializedAccessToken = JSONObject.toJSONString(token); 110 String serializedAuth = JSONObject.toJSONString(authentication); 111 String accessKey = ACCESS + token.getValue(); 112 String authKey = AUTH + token.getValue(); 113 String authToAccessKey = AUTH_TO_ACCESS + authenticationKeyGenerator.extractKey(authentication); 114 String approvalKey = UNAME_TO_ACCESS + getApprovalKey(authentication); 115 String clientId = CLIENT_ID_TO_ACCESS + authentication.getOAuth2Request().getClientId(); 116 117 int seconds = 30 * 24 * 60 * 60; 118 if (token.getExpiration() != null) { 119 seconds = token.getExpiresIn(); 120 } 121 122 try { 123 stringRedisTemplate.opsForValue().set(accessKey, serializedAccessToken); 124 stringRedisTemplate.opsForValue().set(authKey, serializedAuth); 125 stringRedisTemplate.opsForValue().set(authToAccessKey, serializedAccessToken); 126 127 if (!authentication.isClientOnly()) { 128 stringRedisTemplate.opsForHash().putIfAbsent(approvalKey, token.getValue(), serializedAccessToken); 129 } 130 } finally { 131 //如果中途失败,则还可以补偿过期时间 132 stringRedisTemplate.expire(accessKey, seconds, TimeUnit.SECONDS); 133 stringRedisTemplate.expire(authKey, seconds, TimeUnit.SECONDS); 134 stringRedisTemplate.expire(authToAccessKey, seconds, TimeUnit.SECONDS); 135 stringRedisTemplate.expire(clientId, seconds, TimeUnit.SECONDS); 136 stringRedisTemplate.expire(approvalKey, seconds, TimeUnit.SECONDS); 137 } 138 139 OAuth2RefreshToken refreshToken = token.getRefreshToken(); 140 if (refreshToken != null && refreshToken.getValue() != null) { 141 String refreshValue = token.getRefreshToken().getValue(); 142 String refreshToAccessKey = REFRESH_TO_ACCESS + refreshValue; 143 String accessToRefreshKey = ACCESS_TO_REFRESH + token.getValue(); 144 145 try { 146 stringRedisTemplate.opsForValue().set(refreshToAccessKey, token.getValue()); 147 stringRedisTemplate.opsForValue().set(accessToRefreshKey, refreshValue); 148 } finally { 149 //如果中途失败,则还可以补偿过期时间 150 refreshTokenProcess(refreshToken, refreshToAccessKey, accessToRefreshKey); 151 } 152 153 CACHE.put(refreshToAccessKey, token.getValue()); 154 CACHE.put(accessToRefreshKey, refreshValue); 155 } 156 157 CACHE.put(accessKey, token); 158 CACHE.put(authKey, authentication); 159 CACHE.put(authToAccessKey, token); 160 } 161 162 private void refreshTokenProcess(OAuth2RefreshToken refreshToken, String refreshKey, String refreshAuthKey) { 163 int seconds = 30 * 24 * 60 * 60; 164 if (refreshToken instanceof ExpiringOAuth2RefreshToken) { 165 ExpiringOAuth2RefreshToken expiringRefreshToken = (ExpiringOAuth2RefreshToken) refreshToken; 166 Date expiration = expiringRefreshToken.getExpiration(); 167 168 int temp; 169 if (expiration != null) { 170 temp = Long.valueOf((expiration.getTime() - System.currentTimeMillis()) / 1000L) 171 .intValue(); 172 173 } else { 174 temp = seconds; 175 } 176 stringRedisTemplate.expire(refreshKey, temp, TimeUnit.SECONDS); 177 stringRedisTemplate.expire(refreshAuthKey, temp, TimeUnit.SECONDS); 178 } 179 } 180 181 182 private String getApprovalKey(OAuth2Authentication authentication) { 183 String userName = ""; 184 if (authentication.getUserAuthentication() != null) { 185 TeacherUserDetails userInfoDetails = (TeacherUserDetails) authentication.getUserAuthentication().getPrincipal(); 186 userName = userInfoDetails.getPhone() + "_" + userInfoDetails.getUsername(); 187 } 188 189 return getApprovalKey(authentication.getOAuth2Request().getClientId(), userName); 190 } 191 192 private String getApprovalKey(String clientId, String userName) { 193 return clientId + (userName == null ? "" : ":" + userName); 194 } 195 196 @Override 197 public OAuth2AccessToken readAccessToken(String tokenValue) { 198 String key = ACCESS + tokenValue; 199 200 return (OAuth2AccessToken) loadCache(key, (k) -> { 201 String json = stringRedisTemplate.opsForValue().get(key); 202 if (StringUtils.isNotBlank(json)) { 203 return JSONObject.parseObject(json, DefaultOAuth2AccessTokenEx.class); 204 } 205 return null; 206 }); 207 } 208 209 @Override 210 public void removeAccessToken(OAuth2AccessToken accessToken) { 211 removeAccessToken(accessToken.getValue()); 212 } 213 214 public void removeAccessToken(String tokenValue) { 215 String accessKey = ACCESS + tokenValue; 216 String authKey = AUTH + tokenValue; 217 String accessToRefreshKey = ACCESS_TO_REFRESH + tokenValue; 218 219 OAuth2Authentication authentication = readAuthentication(tokenValue); 220 String access = stringRedisTemplate.opsForValue().get(accessKey); 221 222 List<String> keys = new ArrayList<>(6); 223 keys.add(accessKey); 224 keys.add(authKey); 225 keys.add(accessToRefreshKey); 226 227 stringRedisTemplate.delete(keys); 228 229 if (authentication != null) { 230 String key = authenticationKeyGenerator.extractKey(authentication); 231 String authToAccessKey = AUTH_TO_ACCESS + key; 232 String unameKey = UNAME_TO_ACCESS + getApprovalKey(authentication); 233 String clientId = CLIENT_ID_TO_ACCESS + authentication.getOAuth2Request().getClientId(); 234 235 stringRedisTemplate.delete(authToAccessKey); 236 stringRedisTemplate.opsForHash().delete(unameKey, tokenValue); 237 stringRedisTemplate.opsForList().remove(clientId, 1, access); 238 stringRedisTemplate.delete(ACCESS + key); 239 240 CACHE.invalidate(authToAccessKey); 241 CACHE.invalidate(ACCESS + key); 242 } 243 244 CACHE.invalidateAll(keys); 245 } 246 247 @Override 248 public void storeRefreshToken(OAuth2RefreshToken refreshToken, OAuth2Authentication authentication) { 249 String refreshKey = REFRESH + refreshToken.getValue(); 250 String refreshAuthKey = REFRESH_AUTH + refreshToken.getValue(); 251 String serializedRefreshToken = JSONObject.toJSONString(refreshToken); 252 253 stringRedisTemplate.opsForValue().set(refreshKey, serializedRefreshToken); 254 stringRedisTemplate.opsForValue().set(refreshAuthKey, JSONObject.toJSONString(authentication)); 255 256 refreshTokenProcess(refreshToken, refreshKey, refreshAuthKey); 257 258 CACHE.put(refreshKey, refreshToken); 259 CACHE.put(refreshAuthKey, authentication); 260 } 261 262 263 @Override 264 public OAuth2RefreshToken readRefreshToken(String tokenValue) { 265 String key = REFRESH + tokenValue; 266 return (OAuth2RefreshToken) loadCache(key, (k) -> { 267 String json = stringRedisTemplate.opsForValue().get(key); 268 if (StringUtils.isNotBlank(json)) { 269 return JSONObject.parseObject(json, DefaultOAuth2RefreshTokenEx.class); 270 } 271 272 return null; 273 }); 274 } 275 276 @Override 277 public OAuth2Authentication readAuthenticationForRefreshToken(OAuth2RefreshToken token) { 278 return this.readAuthenticationForRefreshToken(token.getValue()); 279 } 280 281 public OAuth2Authentication readAuthenticationForRefreshToken(String token) { 282 String key = REFRESH_AUTH + token; 283 284 return (OAuth2Authentication) loadCache(key, (k) -> { 285 String json = stringRedisTemplate.opsForValue().get(key); 286 if (StringUtils.isBlank(json)) { 287 return null; 288 } 289 290 return fullParseJSON(json); 291 }); 292 } 293 294 @Override 295 public void removeRefreshToken(OAuth2RefreshToken refreshToken) { 296 this.removeRefreshToken(refreshToken.getValue()); 297 } 298 299 public void removeRefreshToken(String refreshToken) { 300 String refreshKey = REFRESH + refreshToken; 301 String refreshAuthKey = REFRESH_AUTH + refreshToken; 302 String refresh2AccessKey = REFRESH_TO_ACCESS + refreshToken; 303 String access2RefreshKey = ACCESS_TO_REFRESH + refreshToken; 304 305 List<String> keys = new ArrayList<>(7); 306 keys.add(refreshKey); 307 keys.add(refreshAuthKey); 308 keys.add(refresh2AccessKey); 309 keys.add(access2RefreshKey); 310 311 stringRedisTemplate.delete(keys); 312 313 CACHE.invalidateAll(keys); 314 } 315 316 @Override 317 public void removeAccessTokenUsingRefreshToken(OAuth2RefreshToken refreshToken) { 318 this.removeAccessTokenUsingRefreshToken(refreshToken.getValue()); 319 } 320 321 private void removeAccessTokenUsingRefreshToken(String refreshToken) { 322 String key = REFRESH_TO_ACCESS + refreshToken; 323 324 String accessToken = stringRedisTemplate.opsForValue().get(key); 325 stringRedisTemplate.delete(key); 326 327 if (accessToken != null) { 328 removeAccessToken(accessToken); 329 } 330 331 CACHE.invalidate(key); 332 } 333 334 private <T> Object loadCache(String key, Function<Object, ? extends T> loadData) { 335 try { 336 Object value = CACHE.getIfPresent(key); 337 if (value == null) { 338 value = loadData.apply(key); 339 if (value != null) { 340 CACHE.put(key, value); 341 } 342 } 343 344 return value; 345 } catch (Exception e) { 346 throw new RuntimeException("JsonRedisTokenStore.loadCache从缓存中加载数据发生错误", e); 347 } 348 } 349 350 @Override 351 public OAuth2AccessToken getAccessToken(OAuth2Authentication authentication) { 352 String key = AUTH_TO_ACCESS + authenticationKeyGenerator.extractKey(authentication); 353 354 return (OAuth2AccessToken) loadCache(key, (k) -> { 355 String json = stringRedisTemplate.opsForValue().get(key); 356 357 if (StringUtils.isNotBlank(json)) { 358 DefaultOAuth2AccessToken accessToken = JSONObject.parseObject(json, DefaultOAuth2AccessTokenEx.class); 359 360 361 OAuth2Authentication storedAuthentication = readAuthentication(accessToken.getValue()); 362 363 if (storedAuthentication == null 364 || !key.equals(authenticationKeyGenerator.extractKey(storedAuthentication))) { 365 this.storeAccessToken(accessToken, authentication); 366 } 367 368 return accessToken; 369 } 370 371 return null; 372 }); 373 } 374 375 376 377 @Override 378 public Collection<OAuth2AccessToken> findTokensByClientIdAndUserName(String clientId, String userName) { 379 String approvalKey = UNAME_TO_ACCESS + getApprovalKey(clientId, userName); 380 381 return getOAuth2AccessTokens(approvalKey); 382 } 383 384 private Collection<OAuth2AccessToken> getOAuth2AccessTokens(String approvalKey) { 385 final Collection<OAuth2AccessToken> oAuth2AccessTokens = (Collection<OAuth2AccessToken>) loadCache(approvalKey, (k) -> { 386 Map<Object, Object> accessTokens = stringRedisTemplate.opsForHash().entries(approvalKey); 387 388 if (accessTokens.size() == 0) { 389 return Collections.emptySet(); 390 } 391 392 List<OAuth2AccessToken> result = new ArrayList<>(); 393 394 for (Object json : accessTokens.values()) { 395 String strJSON = json.toString(); 396 OAuth2AccessToken accessToken = JSONObject.parseObject(strJSON, DefaultOAuth2AccessTokenEx.class); 397 398 result.add(accessToken); 399 } 400 return Collections.unmodifiableCollection(result); 401 }); 402 return oAuth2AccessTokens; 403 } 404 405 @Override 406 public Collection<OAuth2AccessToken> findTokensByClientId(String clientId) { 407 String key = CLIENT_ID_TO_ACCESS + clientId; 408 409 return getOAuth2AccessTokens(key); 410 } 411 }
DefaultOAuth2AccessTokenEx:
 1 public class DefaultOAuth2AccessTokenEx extends DefaultOAuth2AccessToken {
 2     private DefaultOAuth2RefreshTokenEx refreshToken;
 3 
 4     public DefaultOAuth2AccessTokenEx() {
 5         super((String) null);
 6     }
 7 
 8     @Override
 9     public void setValue(String value) {
10         super.setValue(value);
11     }
12 
13     @Override
14     public DefaultOAuth2RefreshTokenEx getRefreshToken() {
15         return refreshToken;
16     }
17 
18     public void setRefreshToken(DefaultOAuth2RefreshTokenEx refreshToken) {
19         this.refreshToken = refreshToken;
20     }
21 }
DefaultOAuth2RefreshTokenEx
1 @Data
2 public class DefaultOAuth2RefreshTokenEx extends DefaultOAuth2RefreshToken implements ExpiringOAuth2RefreshToken {
3     private Date expiration;
4     private String value;
5 
6     public DefaultOAuth2RefreshTokenEx() {
7         super(null);
8     }
9 }
由于Oauth自己定义的实体中,用了jackson对字段做了注解,使用jackson对token对象做json序列化,有字段名称变化,可能会导致debug费时间等,就改用了fastjson,如果有IT同仁不喜欢用fastjson,就根据自己需要改用自己用的顺手的json库就行了。