增加接口签名校验

This commit is contained in:
2023-11-28 14:09:31 +08:00
parent 58bafb0616
commit b6535a2efc
6 changed files with 194 additions and 18 deletions

View File

@@ -11,9 +11,12 @@ public class Constants {
public static final String CACHE_RESOURCE_API_TREE = "resourceApiTree";
public static final String CACHE_RESOURCE_MENU_TREE = "resourceMenuTree";
public static final String HTTP_HEADER_TOKEN = "X-Token";
public static final String HTTP_HEADER_CLIENT_SIGN = "X-Client-Sign";
public static final String HTTP_HEADER_TS = "X-TS";
public static final String RATE_LIMIT_GLOBAL_SESSION_PREFIX = "global-session-rl-";
public static final String RATE_LIMIT_GLOBAL_USER_PREFIX = "global-user-rl-";
public static String SYSCONFIG_ENABLE_CAPTCHA = "enable_captcha";
public static String SYSCONFIG_ENABLE_REQUEST_SIGN = "enable_request_sign";
public static String SYSCONFIG_SESSION_TIMEOUT = "session_timeout";
public static String SYSCONFIG_ACCOUNT_MAX_ONLINE = "account_max_online";
public static String SYSCONFIG_RESETPWD_ENABLE = "resetpwd_enable";

View File

@@ -1,15 +1,23 @@
package cn.lihongjie.coal.common;
import cn.lihongjie.coal.base.dto.R;
import cn.lihongjie.coal.exception.BizException;
import com.fasterxml.jackson.databind.ObjectMapper;
import eu.bitwalker.useragentutils.Browser;
import eu.bitwalker.useragentutils.OperatingSystem;
import eu.bitwalker.useragentutils.UserAgent;
import eu.bitwalker.useragentutils.Version;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.SneakyThrows;
import lombok.experimental.UtilityClass;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.entity.ContentType;
@UtilityClass
public class RequestUtils {
@@ -67,4 +75,14 @@ public class RequestUtils {
return "";
}
}
@SneakyThrows
public static void writeResponse(BizException ex, HttpServletResponse response) {
response.setStatus(200);
response.setContentType(ContentType.APPLICATION_JSON.getMimeType());
R<Object> fail = R.fail(ex.getCode(), ex.getMessage());
response.getOutputStream().write(Ctx.getContext().getBean(ObjectMapper.class).writeValueAsBytes(fail));
response.getOutputStream().flush();
}
}

View File

@@ -1,8 +1,8 @@
package cn.lihongjie.coal.filter;
import cn.lihongjie.coal.base.dto.R;
import cn.lihongjie.coal.common.Constants;
import cn.lihongjie.coal.common.Ctx;
import cn.lihongjie.coal.common.RequestUtils;
import cn.lihongjie.coal.exception.BizException;
import cn.lihongjie.coal.permission.dto.PermissionDto;
import cn.lihongjie.coal.permission.service.PermissionService;
@@ -23,12 +23,9 @@ import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.SneakyThrows;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.entity.ContentType;
import org.slf4j.MDC;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
@@ -77,7 +74,7 @@ public class AuthFilter extends OncePerRequestFilter {
} catch (Exception e) {
logger.info("系统异常", e);
writeResponse(new BizException("系统异常"), response);
RequestUtils.writeResponse(new BizException("系统异常"), response);
}
}
@@ -109,7 +106,7 @@ public class AuthFilter extends OncePerRequestFilter {
resourceService.findUrlFromCache(getRequestURI(request));
if (resource.isEmpty()) {
writeResponse(new BizException("invalidUrl", "资源未找到"), response);
RequestUtils.writeResponse(new BizException("invalidUrl", "资源未找到"), response);
return;
}
@@ -140,7 +137,7 @@ public class AuthFilter extends OncePerRequestFilter {
} else {
writeResponse(new BizException("loginRequired", "请先登录"), response);
RequestUtils.writeResponse(new BizException("loginRequired", "请先登录"), response);
}
return;
@@ -153,7 +150,7 @@ public class AuthFilter extends OncePerRequestFilter {
} catch (BizException ex) {
writeResponse(ex, response);
RequestUtils.writeResponse(ex, response);
return;
}
@@ -169,7 +166,7 @@ public class AuthFilter extends OncePerRequestFilter {
if (userResource.isEmpty() && BooleanUtils.isFalse(user.getSysAdmin())) {
writeResponse(new BizException("invalidAccess", "当前资源未授权,请联系机构管理员处理。"), response);
RequestUtils.writeResponse(new BizException("invalidAccess", "当前资源未授权,请联系机构管理员处理。"), response);
} else {
doFilter(request, response, filterChain);
@@ -202,13 +199,4 @@ public class AuthFilter extends OncePerRequestFilter {
}
}
@SneakyThrows
private void writeResponse(BizException ex, HttpServletResponse response) {
response.setStatus(200);
response.setContentType(ContentType.APPLICATION_JSON.getMimeType());
R<Object> fail = R.fail(ex.getCode(), ex.getMessage());
response.getOutputStream().write(objectMapper.writeValueAsBytes(fail));
response.getOutputStream().flush();
}
}

View File

@@ -0,0 +1,35 @@
package cn.lihongjie.coal.filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.util.ContentCachingRequestWrapper;
import org.springframework.web.util.ContentCachingResponseWrapper;
import java.io.IOException;
/**
*
*/
@Component
@Order(0)
@Slf4j
public class CacheFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
var req = new ContentCachingRequestWrapper(request);
var res = new ContentCachingResponseWrapper(response);
filterChain.doFilter(req, res);
res.copyBodyToResponse();
}
}

View File

@@ -0,0 +1,131 @@
package cn.lihongjie.coal.filter;
import cn.lihongjie.coal.common.Constants;
import cn.lihongjie.coal.common.RequestUtils;
import cn.lihongjie.coal.exception.BizException;
import cn.lihongjie.coal.sysconfig.service.SysConfigService;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.binary.Hex;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.codec.digest.HmacAlgorithms;
import org.apache.commons.codec.digest.HmacUtils;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.util.*;
import javax.crypto.Mac;
/** 请求签名验证 */
@Component
@Order(10)
@Slf4j
public class SignFilter extends OncePerRequestFilter {
@Autowired ObjectMapper objectMapper;
@Autowired SysConfigService sysConfigService;
@Override
protected void doFilterInternal(
HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
if (sysConfigService.isEnable(Constants.SYSCONFIG_ENABLE_REQUEST_SIGN)) {
StringBuilder sb = new StringBuilder();
// 请求方法
sb.append(StringUtils.defaultString(request.getMethod().toUpperCase()));
sb.append(StringUtils.defaultString("\n"));
// 请求地址
sb.append(StringUtils.defaultString(request.getRequestURI()));
sb.append(StringUtils.defaultString("\n"));
// 请求参数
sb.append(StringUtils.defaultString(request.getQueryString()));
sb.append(StringUtils.defaultString("\n"));
// 请求头
String token =
ObjectUtils.defaultIfNull(
request.getHeader(Constants.HTTP_HEADER_TOKEN), "anonymous");
String ts = request.getHeader(Constants.HTTP_HEADER_TS);
if (StringUtils.isEmpty(ts)) {
RequestUtils.writeResponse(
new BizException(Constants.HTTP_HEADER_TS + " 请求头缺失"), response);
return;
}
long tsi = 0;
try {
tsi = Long.parseLong(ts);
} catch (Exception e) {
RequestUtils.writeResponse(
new BizException(Constants.HTTP_HEADER_TS + " 请求头格式错误"), response);
return;
}
long current = System.currentTimeMillis();
if (Math.abs(tsi - current) > 1000 * 60 * 2) {
RequestUtils.writeResponse(
new BizException(
Constants.HTTP_HEADER_TS
+ " 客户端时间误差过大,请校准时间. 服务器时间为: "
+ LocalDateTime.now()),
response);
return;
}
sb.append(StringUtils.defaultString(token));
sb.append(StringUtils.defaultString("\n"));
sb.append(StringUtils.defaultString(ts));
sb.append(StringUtils.defaultString("\n"));
String sha256Hex = DigestUtils.sha256Hex(request.getInputStream()).toUpperCase();
sb.append(StringUtils.defaultString(sha256Hex));
sb.append(StringUtils.defaultString("\n"));
Mac mac =
HmacUtils.getInitializedMac(
HmacAlgorithms.HMAC_SHA_256, token.getBytes(StandardCharsets.UTF_8));
mac.update(sb.toString().getBytes(StandardCharsets.UTF_8));
String sign = Hex.encodeHexString(mac.doFinal()).toUpperCase();
String clientSign = request.getHeader(Constants.HTTP_HEADER_CLIENT_SIGN);
if (!sign.equals(clientSign)) {
log.debug("key: {} \ndata:{}", token, sb);
log.warn("签名错误: {} {}", clientSign, sign);
RequestUtils.writeResponse(
new BizException(Constants.HTTP_HEADER_CLIENT_SIGN + " 请求头签名错误"), response);
return;
}
}
doFilter(request, response, filterChain);
}
}

View File

@@ -60,6 +60,7 @@ public class SysConfigService extends BaseService<SysConfigEntity, SysConfigRepo
.collect(Collectors.toMap(e -> e.getCode(), e -> e));
addDictConfig(all, Constants.SYSCONFIG_ENABLE_CAPTCHA, "验证码状态", "1", "status.type");
addDictConfig(all, Constants.SYSCONFIG_ENABLE_REQUEST_SIGN, "请求签名验证", "1", "status.type");
addNumberConfig(
all,