跳到主要内容

SpringBoot如何保证接口安全

请求头:

RequestHeader类来表示请求头的相关信息,包括签名、时间戳和随机数。

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class RequestHeader {
private String sign;
private Long timestamp;
private String nonce;
}

过滤器中,从请求中获取请求头信息,并进行一系列的验证。验证过程包括以下几个方面:

  1. 重放验证:判断时间戳与当前时间的差值是否超过设定的过期时间,如果超过则提示签名过期。
  2. 判断随机数:检查随机数是否已存在于 Redis 中,如果存在则表示请求重复。

然后,根据请求的方法(GET 或 POST)获取请求参数,并使用SignUtil工具类验证签名。

// SpringBoot 如何保证接口安全?高手都是这么玩的!
public class SignFilter implements Filter {
@Resource
private RedisUtil redisUtil;

// 从fitler配置中获取sign过期时间
private Long signMaxTime;

private static final String NONCE_KEY = "x-nonce-";

@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) servletRequest;
HttpServletResponse httpResponse = (HttpServletResponse) servletResponse;

System.out.println(httpRequest.getRequestURI());

HttpServletRequestWrapper requestWrapper = new SignRequestWrapper(httpRequest);
// 构建请求头
RequestHeader requestHeader = new RequestHeader();
requestHeader.setNonce(httpRequest.getHeader("x-Nonce"));
requestHeader.setSign(httpRequest.getHeader("X-Sign"));
String header = httpRequest.getHeader("X-Time");
if (StringUtils.isEmpty(header)) {
responseFail(httpResponse, ReturnCode.ILLEGAL_HEADER);
return;
}
requestHeader.setTimestamp(Long.parseLong(header));
// 验证请求头是否存在
if (StringUtils.isEmpty(requestHeader.getSign()) || ObjectUtils.isEmpty(requestHeader.getTimestamp()) || StringUtils.isEmpty(requestHeader.getNonce())) {
responseFail(httpResponse, ReturnCode.ILLEGAL_HEADER);
return;
}

/*
* 1.重放验证
* 判断timestamp时间戳与当前时间是否超过60s(过期时间根据业务情况设置),如果超过了就提示签名过期。
*/
long now = System.currentTimeMillis() / 1000;

if (now - requestHeader.getTimestamp() > signMaxTime) {
responseFail(httpResponse, ReturnCode.REPLAY_ERROR);
return;
}

// 2. 判断nonce
boolean nonceExists = redisUtil.hasKey(NONCE_KEY + requestHeader.getNonce());
if (nonceExists) {
// 请求重复
responseFail(httpResponse, ReturnCode.REPLAY_ERROR);
return;
} else {
redisUtil.set(NONCE_KEY + requestHeader.getNonce(), requestHeader.getNonce(), signMaxTime);
}


boolean accept;
SortedMap<String, String> paramMap;
switch (httpRequest.getMethod()) {
case "GET":
paramMap = HttpDataUtil.getUrlParams(requestWrapper);
accept = SignUtil.verifySign(paramMap, requestHeader);
break;
case "POST":
paramMap = HttpDataUtil.getBodyParams(requestWrapper);
accept = SignUtil.verifySign(paramMap, requestHeader);
break;
default:
accept = true;
break;
}
if (accept) {
filterChain.doFilter(requestWrapper, servletResponse);
} else {
responseFail(httpResponse, ReturnCode.ARGUMENT_ERROR);
}

}

private void responseFail(HttpServletResponse httpResponse, ReturnCode returnCode) {
ResultData<Object> resultData = ResultData.fail(returnCode.getCode(), returnCode.getMessage());
WebUtils.writeJson(httpResponse, resultData);
}

@Override
public void init(FilterConfig filterConfig) throws ServletException {
String signTime = filterConfig.getInitParameter("signMaxTime");
signMaxTime = Long.parseLong(signTime);
}
}

通过@Configuration注解创建了相关的配置类,用于注册过滤器并设置初始化参数。

@Configuration
public class SignFilterConfiguration {

@Value("${sign.maxTime}")
private String signMaxTime;

//filter中的初始化参数
private Map<String, String> initParametersMap = new HashMap<>();

@Bean
public FilterRegistrationBean contextFilterRegistrationBean() {
initParametersMap.put("signMaxTime", signMaxTime);
FilterRegistrationBean registration = new FilterRegistrationBean();
registration.setFilter(signFilter());
registration.setInitParameters(initParametersMap);
registration.addUrlPatterns("/*");
registration.setName("SignFilter");
// 设置过滤器被调用的顺序
registration.setOrder(1);
return registration;
}

@Bean
public Filter signFilter() {
return new SignFilter();
}
}

签名工具类:

public class SignUtil {
/**
* 验证签名
* 验证算法:把timestamp + JsonUtil.object2Json(SortedMap)合成字符串,然后MD5
*/
@SneakyThrows
public static boolean verifySign(SortedMap<String, String> map, RequestHeader requestHeader) {
String params = requestHeader.getNonce() + requestHeader.getTimestamp() + JSON.toJSONString(map);
return verifySign(params, requestHeader);
}

/**
* 验证签名
*/
public static boolean verifySign(String params, RequestHeader requestHeader) {
if (StringUtils.isEmpty(params)) {
return false;
}
String paramsSign = DigestUtils.md5DigestAsHex(params.getBytes()).toUpperCase();
return requestHeader.getSign().equals(paramsSign);
}
}