之前使用的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库就行了。