需求
先说下需求,在发起第三方接口调用之前,需要跟除了获取token接口之外的其他接口增加 Authorization header ,由于获取AccessToken的逻辑是公共的,所以没有必要每次调用业务接口之前,都要自己手动处理,第三方返回的AccessToken是没有referenceToken的,所以必然有一次业务在调用的时候Token是过期了,如果过期,第三方那边返回的状态码是401,所以我想统一在响应之后,增加判断如果状态码是401,重新获取AccessToken接口,然后把新的AccessToken放到header当中,重新替用户发起请求,这样在开发看来,我只需要关注业务接口本身,不需要对token过期的问题进行增加逻辑。
实现逻辑
先说下成果,目前请求之前和响应之后要做的,都已经实现了。
请求之前使用:RequestInterceptor
接口响应之后:Decoder
我在发起接口调用之前有2个功能点要实现:
- 判断Redis中是否有指定key的缓存,如果为空,调用获取AccessToken接口,然后把获取的AccessToken放到Redis当中。
- 从Redis中获取缓存,然后把AccessToken放到非 /get/oauth/token 接口其它接口的 header 中。
在响应之后有1个功能需要实现:
- 如果状态码或者业务码是401,重新发起获取AccessToken接口,然后替换旧的Header中的内容,重新代替用户再次发起业务请求。
代码
RequestInterceptor
import com.zcckj.hikvision.starter.constants.HikVisionConstants;
import com.zcckj.hikvision.starter.constants.HikVisionRedisConstants;
import feign.RequestInterceptor;
import feign.RequestTemplate;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
/**
* @author lwh
* @date 2025/1/13
* @description 统一给业务接口加上请求头
**/
@Component
@Slf4j
public class HikVisionAccessTokenInterceptor implements RequestInterceptor {
@Resource
private StringRedisTemplate stringRedisTemplate;
@Resource
private HikVisionAccessTokenSet hikVisionAccessTokenSet;
/**
* @author lwh
* @date2025/1/13
* @description 统一给接口增加请求头
**/
@Override
public void apply(RequestTemplate template) {
String url = template.url();
Boolean accessTokenExistsStatus = stringRedisTemplate.hasKey(HikVisionRedisConstants.OAUTH_TOKEN_KEY);
// 用于只给加了指定的Header才能在解码器中拦截
template.header("HikVisionDT", "DT");
// 判断URL中是否包含OAuth token获取请求,或访问令牌不存在的状态
if (url.contains(HikVisionConstants.OAUTH_TOKEN) || Boolean.FALSE.equals(accessTokenExistsStatus)) {
hikVisionAccessTokenSet.setGlobalAccessToken();
}
// 如果URL中包含OAuth的请求,手动给该请求加上指定的请求参数
if (url.contains(HikVisionConstants.OAUTH_TOKEN)) {
// 组装海康威视AccessToken参数
String oAuthTokenParam = hikVisionAccessTokenSet.getOAuthTokenParam();
template.body(oAuthTokenParam);
} else {
// 如果URL中不包含OAuth的请求,手动给该请求加上指定的请求头
template.header("Authorization", stringRedisTemplate.opsForValue().get(HikVisionRedisConstants.OAUTH_TOKEN_KEY));
}
}
}
Decoder
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.type.TypeFactory;
import com.zcckj.hikvision.starter.config.enums.HikVisionErrorCodeEnum;
import com.zcckj.hikvision.starter.constants.HikVisionRedisConstants;
import com.zcckj.hikvision.starter.model.common.HikVisionResult;
import feign.*;
import feign.codec.Decoder;
import org.springframework.data.redis.core.StringRedisTemplate;
import javax.annotation.Resource;
import java.io.IOException;
import java.lang.reflect.Type;
import java.util.*;
/**
* @author lwh
* @date 2025/1/14
* @description 自定义feign解码器
**/
@Component
public class HikVisionFeignResultDecoder implements Decoder {
@Resource
private ObjectMapper objectMapper;
@Resource
private HikVisionAccessTokenSet hikVisionAccessTokenSet;
@Resource
private StringRedisTemplate stringRedisTemplate;
@Resource
private Client feignClient;
/**
* @author lwh
* @date 2025/1/14
* @description 解码
**/
@Override
public Object decode(Response response, Type type) throws IOException, FeignException {
Map<String, Collection<String>> headers = response.request().headers();
// 如果拦截到没有加指定Header的Feign,则不增加特殊逻辑
if (!headers.containsKey("HikVisionDT")) {
Response.Body body = response.body();
if (body == null) {
return null;
}
if (String.class.equals(type)) {
return Util.toString(body.asReader());
}
if (byte[].class.equals(type)) {
return Util.toByteArray(response.body().asInputStream());
}
String bodyStr = Util.toString(response.body().asReader(Util.UTF_8));
//对结果进行转换
return json2obj(bodyStr, type);
}
int status = response.status();
if (status == HikVisionErrorCodeEnum.ERROR_CODE_401.getCode()) {
response = resetRequest(response);
}
if (Objects.isNull(response.body())) {
throw new RuntimeException( "没有返回有效的数据");
}
String bodyStr = Util.toString(response.body().asReader(Util.UTF_8));
//对结果进行转换
HikVisionResult result = json2obj(bodyStr, type);
//如果返回错误,且为内部错误,则直接抛出异常
if (Objects.nonNull(result.getCode()) && HikVisionErrorCodeEnum.ERROR_CODE_401.getCode().equals(result.getCode())) {
response = resetRequest(response);
if (Objects.isNull(response.body())) {
throw new RuntimeException( "没有返回有效的数据");
}
bodyStr = Util.toString(response.body().asReader(Util.UTF_8));
//对结果进行转换
result = json2obj(bodyStr, type);
}
return result;
}
/**
* @author lwh
* @date 2025/1/14
* @description 重置请求
**/
private Response resetRequest(Response response) throws IOException {
hikVisionAccessTokenSet.setGlobalAccessToken();
// 获取原始请求信息
Request originalRequest = response.request();
Map<String, Collection<String>> headers = originalRequest.headers();
LinkedHashMap<String, Collection<String>> newHeaderMap = new LinkedHashMap<>();
newHeaderMap.put("Authorization", Collections.singletonList(stringRedisTemplate.opsForValue().get(HikVisionRedisConstants.OAUTH_TOKEN_KEY)));
headers.forEach((key, value) -> {
if (!key.equalsIgnoreCase("Authorization")) {
newHeaderMap.put(key, value);
}
});
Request request = Request.create(originalRequest.httpMethod(), originalRequest.url(), newHeaderMap, originalRequest.body(), originalRequest.charset());
Request.Options options = new Request.Options(10, 60, true);
return feignClient.execute(request, options);
}
/**
* @author lwh
* @date 2025/1/14
* @description json转换
**/
private <T> T json2obj(String jsonStr, Type targetType) {
try {
JavaType javaType = TypeFactory.defaultInstance().constructType(targetType);
return objectMapper.readValue(jsonStr, javaType);
} catch (IOException e) {
throw new IllegalArgumentException("将JSON转换为对象时发生错误:" + jsonStr, e);
}
}
}
FeignConfig
拦截器和解码器不需要在FeignConfig配置,只需要在对应的Feign接口的configuration配置,如果在FeignConfig配置,则所有的Feign接口都会生效,而我只想让指定的Feign接口生效,如果你想让所有的Feign接口都生效,那么可以配置在这里面。
import feign.Client;
import feign.Logger;
import feign.Retryer;
import feign.codec.Decoder;
import okhttp3.ConnectionPool;
import okhttp3.OkHttpClient;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.cloud.commons.httpclient.OkHttpClientConnectionPoolFactory;
import org.springframework.cloud.commons.httpclient.OkHttpClientFactory;
import org.springframework.cloud.openfeign.FeignAutoConfiguration;
import org.springframework.cloud.openfeign.support.FeignHttpClientProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.concurrent.TimeUnit;
/**
* @author lwh
* @date 2024/11/12
* @description feign配置
**/
@Configuration
@AutoConfigureBefore(FeignAutoConfiguration.class)
public class HikVisionFeignConfig {
/**
* @author lwh
* @date 2025/1/14
* @description 日志级别
**/
@Bean
@ConditionalOnMissingBean(Logger.Level.class)
Logger.Level feignLoggerLevel() {
return Logger.Level.BASIC;
}
/**
* @author lwh
* @date 2025/1/14
* @description 调用重试
**/
@Bean
@ConditionalOnMissingBean(Retryer.class)
Retryer feignRetryer() {
return new Retryer.Default();
}
/**
* @author lwh
* @date 2025/1/14
* @description feign客户端
**/
@Bean
@ConditionalOnMissingBean({Client.class})
public Client feignClient(OkHttpClient client) {
return new feign.okhttp.OkHttpClient(client);
}
/**
* @author lwh
* @date 2025/1/14
* @description 连接池
**/
@Bean
@ConditionalOnMissingBean({ConnectionPool.class})
public ConnectionPool httpClientConnectionPool(FeignHttpClientProperties httpClientProperties, OkHttpClientConnectionPoolFactory connectionPoolFactory) {
Integer maxTotalConnections = httpClientProperties.getMaxConnections();
Long timeToLive = httpClientProperties.getTimeToLive();
TimeUnit ttlUnit = httpClientProperties.getTimeToLiveUnit();
return connectionPoolFactory.create(maxTotalConnections, timeToLive, ttlUnit);
}
/**
* @author lwh
* @date 2025/1/14
* @description feign客户端
**/
@Bean
public OkHttpClient client(OkHttpClientFactory httpClientFactory, ConnectionPool connectionPool, FeignHttpClientProperties httpClientProperties) {
Boolean followRedirects = httpClientProperties.isFollowRedirects();
Integer connectTimeout = httpClientProperties.getConnectionTimeout();
Boolean disableSslValidation = httpClientProperties.isDisableSslValidation();
return httpClientFactory.createBuilder(disableSslValidation)
.connectTimeout((long)connectTimeout, TimeUnit.SECONDS)
.followRedirects(followRedirects)
.connectionPool(connectionPool)
.build();
}
结尾
上面的代码只是示例,代码是不全的,核心在于如何在请求之前和响应之后,做一些逻辑处理。