RestTemplate设置动态token
前言
这里服务之间调用使用的是RestTemplate,因为在某些特殊的场景下RestTemplate相比Feign和Dubbo来说也是有它的方便之处的,这里我就不细说了,知道这里用的RestTemplate来调用上游微服务就可以了
为什么需要动态获取token?
我们在调用上游服务时大多数情况是需要认证的,这时我们是需要把认证信息(这里是token)放到请求头header里。但是我们肯定不能把token字符串写死,因为token一般都是有过期时间的。
那我们该怎么办,每次向上游服务请求时都先获取一下新的token?
这样token确实动态了,但是每次请求都获取一次token、生成一个新的token,每生成一次token都会把新的token放到Redis服务器上的,这样一来就有点浪费Redis的内存了,虽然token都会过期、会移除,但那也挡不住你每次向上游服务都发送请求都生成新的token往Redis里塞得快呀,这样太浪费Redis内存空间了。
解决思路
我们可以自定义一个XxxRestTemplate类继承一下RestTemplate,重写RestTemplate的exchange方法。
@Component
public class FireFlyRestTemplate extends RestTemplate {
public static final String CODE_401 = "401";
private static final Logger log = LoggerFactory.getLogger(FireFlyRestTemplate.class);
@Autowired
private TokenProvider tokenProvider;
@Override
public <T> ResponseEntity<T> exchange(String url, HttpMethod method, HttpEntity<?> requestEntity,
Class<T> responseType, Object... uriVariables) throws RestClientException {
log.info("【FireFlyRestTemplate.exchange】");
ResponseEntity<T> result = super.exchange(url, method, requestEntity, responseType, uriVariables);
T body = result.getBody();
// 强转成JSONObject类型
JSONObject jsonResult = (JSONObject) body;
boolean isUnAuthentication = CODE_401.equals(jsonResult.getString("code"));
// 如果返回的code是401
if (isUnAuthentication) {
log.info("【401:token过期了】");
// 刷新本地缓存里的token
tokenProvider.refreshToken(1000*60*10L);
// 利用新的token发送请求
return super.exchange(url, method, tokenProvider.createNewEntity(requestEntity), responseType, uriVariables);
}
return result;
}
}
请看重写的exchange的代码,有如下几个步骤:
- 首先调用父类的exchange方法,根据传来的参数发送请求
- 获取请求返回的结果,随后我将返回结果转成JSONObject类型、并获取里面的code这个key是因为我们公司的编程习惯,一般返回的结果都是JSON类型、且都有code这个字段(这也是这个代码的局限性)
- 根据获取的code进行判断,如果code为401,那么很显然是你的token过期了、或者没传toekn,我就又重新调用exchage(父类的exchage)发送了一个请求,不过这里我传的HttpEntity是tokenProvider.createNewEntity(requestEntity),这里tokenProvider是我自己写的工具类,实现效果就是往HttpEntity对象里的header里加上新的token。
- 回到第3步,如果code不是401,那么就直接返回前面返回的结果
接下来看一下TokenProvider这个工具类:
@Component
public class TokenProvider {
private static final Logger log = LoggerFactory.getLogger(TokenProvider.class);
private static final String TOKEN_STR = "token";
@Autowired
private RestTemplate restTemplate;
@Value("${api.firefly.url}")
private String BASE_URL;
@Value("${api.firefly.auth-username}")
private String autUsername;
@Value("${api.firefly.auth-password}")
private String authPassword;
@Value("${auth.defaultTokenExpireTime}")
private Long defaultTokenExpireTime;
/**
* 获取新的token
* @return
*/
public String getNewToken() {
log.info("【getNewToken】");
// 先拿到 公钥
String publicKey = this.getPublicKey();
String encryptedPassword = "";
// 对密码进行加密
try {
// 利用公钥 使用RSA算法为密码进行加密
encryptedPassword = RSAUtils.encrypt(authPassword, publicKey);
} catch (Exception e) {
log.info("密码加密失败");
e.printStackTrace();
}
// 向上游服务发送请求、获取token。 参数:用户名、加密后的密码
JSONObject loginResult = this.login(autUsername, encryptedPassword);
return loginResult.getString(TOKEN_STR);
}
/**
* 请求上游服务获取 公钥
* @return
*/
private String getPublicKey() {
String FULL_URL = BASE_URL + "/getPublicKey";
ResponseEntity<JSONObject> result = restTemplate.exchange(FULL_URL, HttpMethod.GET, null, JSONObject.class);
return result.getBody().getString("msg");
}
/**
* 真正发起请求获取token的方法
* @param username
* @param password
* @return
*/
public JSONObject login(String username, String password) {
log.info("【login】");
String FULL_URL = BASE_URL + "/login";
Map<String, Object> requestBody = new HashMap<>();
requestBody.put("username", username);
requestBody.put("password", password);
HttpEntity<Map<String, Object>> httpEntity = new HttpEntity<>(requestBody, null);
ResponseEntity<JSONObject> result = restTemplate.exchange(FULL_URL, HttpMethod.POST, httpEntity, JSONObject.class);
return result.getBody();
}
/**
* 1、请求获取新的token
* 2、并将其设置到LocalCacheUtil中
*/
public void refreshToken(Long expireTime) {
log.info("【refreshToken】");
// 获取新token
String newToken = getNewToken();
// 将获取的新token设置到缓存类(LocalCacheUtil)中
if (expireTime == 0) expireTime = defaultTokenExpireTime;
LocalCacheUtil.set(TOKEN_STR, newToken, expireTime);
}
/**
* 从缓存中获取token,这个方法只能保重可以从本地缓存中拿到token,但是获取的token可能对上游服务来说是过期的。
* @return
*/
public String getTokenFromCache() {
log.info("【getTokenFromCache】");
Object token = LocalCacheUtil.get(TOKEN_STR);
// 如果拿到的数据为null 则说明cache中的token已经过期不存在了,要重新请求一次token放到缓存中,再从缓存中获取token
if (token == null) {
refreshToken(1000*60*10L);
// 再次从缓存中获取token
token = LocalCacheUtil.get(TOKEN_STR);
}
return (String)token;
}
/**
* 根据老的HttpEntity 获取新的 HttpEntity
* @param requestEntity
* @return
*/
public HttpEntity<Object> createNewEntity(HttpEntity<?> requestEntity) {
log.info("【createNewEntity】");
Object body = requestEntity.getBody();
HttpHeaders headers = new HttpHeaders();
String token = (String)LocalCacheUtil.get(TOKEN_STR);
headers.add("Authorization", token);
HttpEntity<Object> newHttpEntity = new HttpEntity<>(body, headers);
return newHttpEntity;
}
}
TokenProvider这个工具类的主要有如下功能:
- 从本地缓存中获取token的方法
- 向上游服务发送获取token的请求的方法
- 将获取的新的token存到本地缓存中(这里是本地缓存,可不是redis那个)怎么做本地缓存?你可以使用一些jar,这些有好多开源的jar可以下载;也可以自定义一个LocalCache类,里面加个静态的map集合,你可以把你要存到数据都放里面,这个你自己实现,这样数据就相当于存在JVM的方法区的常量池里了。我用的本地缓存是第二个方案
- 刷新token,将新的token更新到本地缓存中的方法
- createNewEntity方法将传过来的requestEntity封装一下,在header里面加入token
结语
上述代码直接拷贝下来是不能直接执行的,我只是提供一个方案,希望这篇文章能帮到你。