package com.hihonor.honorhome.component.apigateway.handler;
import com.hihonor.honorhome.common.response.BmsBaseResponse;
import javax.servlet.http.HttpServletRequest;
/**
* 请求重复校验处理
*/
public interface RepeatRequestHandler {
/**
* 请求重复校
* @param request HttpServletRequest
* @return BmsBaseResponse
*/
BmsBaseResponse checkRandomRequestId(HttpServletRequest request);
}
package com.hihonor.honorhome.component.apigateway.handler.impl;
import com.hihonor.honorhome.common.component.RedisKeyBuilder;
import com.hihonor.honorhome.common.response.BmsBaseResponse;
import com.hihonor.honorhome.component.apigateway.common.Constants;
import com.hihonor.honorhome.component.apigateway.common.HeaderConstant;
import com.hihonor.honorhome.component.apigateway.common.ResponseEnum;
import com.hihonor.honorhome.component.apigateway.handler.RepeatRequestHandler;
import com.alibaba.nacos.api.config.annotation.NacosValue;
import io.lettuce.core.RedisConnectionException;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.TimeUnit;
import javax.servlet.http.HttpServletRequest;
/**
* 请求重复校验处理
*/
@Slf4j
@Component
public class DefaultRepeatRequestHandler implements RepeatRequestHandler {
@NacosValue(value = "${request.expiration.time}", autoRefreshed = true)
private int expirationTime;
@NacosValue(value = "${request.min.retry}", autoRefreshed = true)
private Integer requestMinRetry;
@NacosValue(value = "${request.max.retry}", autoRefreshed = true)
private Integer requestMaxRetry;
@NacosValue(value = "${honorhome-apigateway.compatible.version}", autoRefreshed = true)
private String[] versions;
@Autowired
private RedisKeyBuilder gatewayRedisKeyBuilder;
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Override
public BmsBaseResponse checkRandomRequestId(HttpServletRequest request) {
String appVersionName = request.getHeader(HeaderConstant.HEADER_APP_VERSION_NAME);
if (StringUtils.isBlank(appVersionName)) {
return new BmsBaseResponse(ResponseEnum.BAD_REQUEST.getCode(), "x-app-version-name is empty!");
}
String requestNonce = getRequestNonce(request, appVersionName);
if (StringUtils.isBlank(requestNonce)) {
return new BmsBaseResponse(ResponseEnum.BAD_REQUEST.getCode(), "x-request-nonce is empty!");
}
String timestamp = request.getHeader(HeaderConstant.HEADER_TIMESTAMP);
if (StringUtils.isBlank(timestamp)) {
return new BmsBaseResponse(ResponseEnum.BAD_REQUEST.getCode(), "x-timestamp is empty!");
}
String redisKey = String.format(Locale.ROOT, "API-GATEWAY:%s", requestNonce + ":" + timestamp);
String key = gatewayRedisKeyBuilder.buildKey(Constants.BUSINESS_NAME, Constants.REQUEST_NAME, redisKey);
try {
String redisValue = stringRedisTemplate.opsForValue().get(key);
if (redisValue == null) {
String requestValue = String.valueOf(requestMinRetry);
// 如果不存在,说明第一次请求,直接插入
stringRedisTemplate.opsForValue().set(key, requestValue, expirationTime, TimeUnit.SECONDS);
} else {
Integer valueFrequency = Integer.parseInt(redisValue) + 1;
log.warn("{}: retry times {}", requestNonce, valueFrequency);
log.warn("url: {}", request.getRequestURL());
if (valueFrequency > requestMaxRetry) {
log.error("{}: retry more than three times in 15 minute", key);
return BmsBaseResponse.error(ResponseEnum.REQUEST_RETRY_FAILED);
}
stringRedisTemplate.opsForValue().set(key, String.valueOf(valueFrequency), 0);
}
return BmsBaseResponse.OK;
} catch (RedisConnectionException e) {
log.error("checkRandomRequestId redis connect error", e);
return new BmsBaseResponse(ResponseEnum.OK.getCode(), ResponseEnum.OK.getEnMessage());
} catch (Exception e) {
log.error("checkRandomRequestId handle error", e);
return BmsBaseResponse.error(ResponseEnum.ERROR);
}
}
private String getRequestNonce(HttpServletRequest request, String appVersionName) {
String requestNonce;
List<String> versionList = Arrays.asList(versions);
if (!CollectionUtils.isEmpty(versionList) && versionList.contains(appVersionName)) {
requestNonce = StringUtils.isBlank(request.getHeader(HeaderConstant.HEADER_REQUEST_NONCE))
? request.getHeader(HeaderConstant.HEADER_REQUEST_ID)
: request.getHeader(HeaderConstant.HEADER_REQUEST_NONCE);
} else {
requestNonce = request.getHeader(HeaderConstant.HEADER_REQUEST_NONCE);
}
return requestNonce;
}
}
package com.hihonor.honorhome.component.apigateway.handler;
import com.hihonor.honorhome.common.response.BmsBaseResponse;
import javax.servlet.http.HttpServletRequest;
/**
* 功能描述:
* 校验REQUEST HEADER
*/
public interface RequestHeaderHandler {
/**
* 检查REQUEST HEADER中的参数
*
* @param request request
* @return 校验是否通过的标识
*/
BmsBaseResponse checkHeader(HttpServletRequest request);
}
package com.hihonor.honorhome.component.apigateway.handler.impl;
import com.hihonor.honorhome.common.constants.NumberConstant;
import com.hihonor.honorhome.common.response.BmsBaseResponse;
import com.hihonor.honorhome.component.apigateway.common.HeaderConstant;
import com.hihonor.honorhome.component.apigateway.common.ResponseEnum;
import com.hihonor.honorhome.component.apigateway.handler.RequestHeaderHandler;
import com.alibaba.nacos.api.config.annotation.NacosValue;
import org.apache.commons.lang3.StringUtils;
import org.springframework.util.CollectionUtils;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Base64;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import javax.annotation.PostConstruct;
import javax.servlet.http.HttpServletRequest;
/**
* 功能描述:
* 校验REQUEST HEADER
*
* @author zw0067467
* @since 2022-06-10
*/
public class DefaultRequestHeaderHandler implements RequestHeaderHandler {
@NacosValue(value = "${honorhome-apigateway.compatible.version}", autoRefreshed = true)
private String[] versions;
private final Set<String> countrySet = new HashSet<>();
/**
* 初始化countrySet
*/
@PostConstruct
public void init() {
// 国家
String[] countryCodes = Locale.getISOCountries();
if (countryCodes.length > 0) {
for (String countryCode : countryCodes) {
if (StringUtils.isBlank(countryCode)) {
continue;
}
countrySet.add(countryCode);
}
}
}
/**
* 检查REQUEST HEADER中的参数
*
* @param request request
* @return 校验是否通过的标识
*/
@Override
public BmsBaseResponse checkHeader(HttpServletRequest request) {
String appVersionName = request.getHeader(HeaderConstant.HEADER_APP_VERSION_NAME);
if (StringUtils.isBlank(appVersionName)) {
return new BmsBaseResponse(ResponseEnum.BAD_REQUEST.getCode(), "x-app-version-name is empty!");
}
if (appVersionName.length() > NumberConstant.NUMBER_64) {
return new BmsBaseResponse(ResponseEnum.BAD_REQUEST.getCode(), "x-app-version-name is too long!");
}
String requestNonce = getRequestNonce(request, appVersionName);
if (StringUtils.isBlank(requestNonce)) {
return new BmsBaseResponse(ResponseEnum.BAD_REQUEST.getCode(), "x-request-nonce is empty!");
}
String country = request.getHeader(HeaderConstant.HEADER_COUNTRY);
if (StringUtils.isBlank(country)) {
return new BmsBaseResponse(ResponseEnum.BAD_REQUEST.getCode(), "x-country is empty!");
}
if (!isValidCountry(country)) {
return new BmsBaseResponse(ResponseEnum.BAD_REQUEST.getCode(), "x-country is invalid!");
}
return checkHeaderParam(request);
}
/**
* 校验国家简称是否符合ISO3166标准
*
* @param country 国家简称
* @return 是否符合ISO标准
*/
private boolean isValidCountry(String country) {
return countrySet.contains(country);
}
/**
* 请求头参数校验
*
* @param request Http请求
* @return 是否符合校验规则
*/
private BmsBaseResponse checkHeaderParam(HttpServletRequest request) {
String userId = request.getHeader(HeaderConstant.HEADER_USER_ID);
if (StringUtils.isNotBlank(userId)) {
try {
Base64.Decoder decoder = Base64.getDecoder();
String user = new String(decoder.decode(userId), StandardCharsets.UTF_8);
if (user.length() > NumberConstant.NUMBER_64) {
return new BmsBaseResponse(ResponseEnum.BAD_REQUEST.getCode(), "x-user-id is too long!");
}
} catch (IllegalArgumentException e) {
return new BmsBaseResponse(ResponseEnum.BAD_REQUEST.getCode(), "x-user-id is illegal!");
}
}
String udid = request.getHeader(HeaderConstant.HEADER_UDID);
if (StringUtils.isNotBlank(udid)) {
try {
Base64.Decoder decoder = Base64.getDecoder();
decoder.decode(udid);
} catch (IllegalArgumentException e) {
return new BmsBaseResponse(ResponseEnum.BAD_REQUEST.getCode(), "x-udid is illegal!");
}
}
String appId = request.getHeader(HeaderConstant.HEADER_APP_ID);
if (StringUtils.isBlank(appId)) {
return new BmsBaseResponse(ResponseEnum.BAD_REQUEST.getCode(), "x-app-id is empty!");
}
return BmsBaseResponse.OK;
}
/**
* 获取nonce参数,appVersionName符合条件,nonce为空的情况下会自动补全
*
* @param request request
* @param appVersionName appVersionName
* @return requestNonce
*/
private String getRequestNonce(HttpServletRequest request, String appVersionName) {
String requestNonce;
List<String> versionList = Arrays.asList(versions);
if (!CollectionUtils.isEmpty(versionList) && versionList.contains(appVersionName)) {
requestNonce = StringUtils.isBlank(request.getHeader(HeaderConstant.HEADER_REQUEST_NONCE))
? request.getHeader(HeaderConstant.HEADER_REQUEST_ID)
: request.getHeader(HeaderConstant.HEADER_REQUEST_NONCE);
} else {
requestNonce = request.getHeader(HeaderConstant.HEADER_REQUEST_NONCE);
}
return requestNonce;
}
}
package com.hihonor.honorhome.component.apigateway.handler;
import javax.servlet.http.HttpServletRequest;
/**
* 功能描述:
* 设置TRACE_ID
* 打印REQUEST 相关日志
* @author zw0067467
* @since 2022-06-10
*/
public interface RequestLogHandler {
/**
* 设置TRACE_ID
* 打印REQUEST 相关日志
*
* @param request request
*/
void logRequest(HttpServletRequest request);
}
package com.hihonor.honorhome.component.apigateway.handler.impl;
import com.hihonor.honorhome.component.apigateway.common.HeaderConstant;
import com.hihonor.honorhome.component.apigateway.handler.RequestLogHandler;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.MDC;
import javax.servlet.http.HttpServletRequest;
/**
* 功能描述
* 设置TRACE_ID
* 打印REQUEST 相关日志
*/
@Slf4j
public class DefaultRequestLogHandler implements RequestLogHandler {
/**
* 将request_id作为trace_id
*/
private static final String TRACE_ID_KEY = HeaderConstant.HEADER_REQUEST_ID;
/**
* 设置TRACE_ID
* 处理REQUEST 相关的日志
*
* @param request request
*/
@Override
public void logRequest(HttpServletRequest request) {
if (request == null) {
log.error("request is null");
return;
}
// 白名单请求不需要trace
Object attribute = request.getAttribute(HeaderConstant.IS_WHITE_URL);
// 不属于白名单才会进行tracing
if (!(HeaderConstant.IS_WHITE_URL_TRUE_VALUE.equals(attribute))) {
// 设置traceId
String traceId = request.getHeader(TRACE_ID_KEY);
if (StringUtils.isBlank(traceId)) {
traceId = "NULL";
}
MDC.put(TRACE_ID_KEY, traceId);
}
log.info("receive request,uri:{}", request.getRequestURI());
}
}
package com.hihonor.honorhome.component.apigateway.handler;
import com.hihonor.honorhome.common.response.BmsBaseResponse;
import javax.servlet.http.HttpServletRequest;
/**
* 权限校验处理器
*/
public interface UserAuthHandler {
/**
* 权限校验
* @param request HttpServletRequest
* @return BaseResponse
*/
BmsBaseResponse userAuth(HttpServletRequest request);
}
package com.hihonor.honorhome.component.apigateway.handler.impl;
import com.hihonor.honorhome.common.response.BmsBaseResponse;
import com.hihonor.honorhome.component.apigateway.common.Constants;
import com.hihonor.honorhome.component.apigateway.common.HeaderConstant;
import com.hihonor.honorhome.component.apigateway.common.ResponseEnum;
import com.hihonor.honorhome.component.apigateway.handler.UserAuthHandler;
import com.hihonor.honorhome.component.apigateway.utils.TokenUtils;
import com.alibaba.nacos.api.config.annotation.NacosValue;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import java.util.Arrays;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
/**
* 权限校验处理器
*/
@Slf4j
@Component
public class DefaultUserAuthHandler implements UserAuthHandler {
@NacosValue(value = "${auth.appIds}", autoRefreshed = true)
private String[] authAppIds;
@NacosValue(value = "${honorhome-apigateway.ignore.url:null}", autoRefreshed = true)
private String[] ignoreUrlLists;
@Autowired
private TokenUtils tokenUtils;
@Override
public BmsBaseResponse userAuth(HttpServletRequest request) {
if (request == null) {
return BmsBaseResponse.error(ResponseEnum.BAD_REQUEST);
}
try {
String appId = request.getHeader(HeaderConstant.HEADER_APP_ID);
// 先验证appId
if (StringUtils.isNotBlank(appId) && !checkAppId(appId)) {
log.error("[appId match error,appId is {}]", appId);
return BmsBaseResponse.error(ResponseEnum.AUTH_VERIFY_INVALID);
}
String token = request.getHeader(HeaderConstant.HEADER_TOKEN);
String userId = request.getHeader(HeaderConstant.HEADER_USER_ID);
// appid缺失或者验证不通过验证token
if (StringUtils.isNotBlank(token) && StringUtils.isNotBlank(userId)) {
return tokenUtils.tokenAuth(token, userId, Constants.BUSINESS_NAME);
} else {
// 验证url,未带用户信息同时不需要用户登录的直接跳过
List<String> stringList = Arrays.asList(ignoreUrlLists);
if (!stringList.isEmpty()) {
String uri = request.getRequestURI();
for (String ignoreUrl : stringList) {
if (uri.startsWith(ignoreUrl)) {
return BmsBaseResponse.OK;
}
}
}
log.error("[url filter error,url is {}]", request.getRequestURI());
return BmsBaseResponse.error(ResponseEnum.AUTH_VERIFY_INVALID);
}
} catch (Exception e) {
log.error("auth error", e);
return BmsBaseResponse.error(ResponseEnum.API_GATEWAY_AUTH_ERROR);
}
}
private boolean checkAppId(String appId) {
List<String> authLists = Arrays.asList(authAppIds);
if (!CollectionUtils.isEmpty(authLists)) {
return authLists.contains(appId);
}
return false;
}
}