1. 服务端需要维护一个表,保存客户端调用的 access key 和 access secret
2. 所有的客户端发起的请求都为 POST 请求,post 请求参数都放在 body 中
代码语言:javascript复制{
"appId": "fdsafdsafdsaf",
"timestamp": 1608190943132,
"businessData": "{"createTime":1608195735340,"id":1,"name":"tom"}",
"sign": "jhgfjhgfjhgfjhgjhgfjhgfjhgfjfghjhgfjhgfjhgfjfg"
}
appId: 客户端的唯一标识(access key)
timestamp: 时间戳毫秒数
businessData: 业务数据
sign: appSecret timestamp businessData 的 MD5 签名值
3. 服务端获取到请求后对请求进行验证
① 验证请求的类型
② 验证请求参数的合法性
③ 验证请求时间戳是否过期(比如: 与服务端时间差再±120秒之内)
④ 验证请求签名的有效性
4. 如果想要防止重放攻击, 让一个请求只能请求一次
可以在 body 中添加一个参数 nonce (一个随机字符串), 请求之后把 nonce 放到redis缓存中, 过期时间可以设置比请求时间戳过期时间略长(比如125秒), 请求发起时先验证请求时间戳是否过期, 如果没过期再验证nonce, 只要发现redis中有对应的nonce值就直接拒绝访问.
代码语言:javascript复制package com.bytedance.itmd.open.filter;
import java.io.IOException;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.groups.Default;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.http.HttpMethod;
import org.springframework.util.DigestUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import com.alibaba.fastjson.JSON;
import com.bytedance.itmd.open.common.Constant;
import com.bytedance.itmd.open.common.CustomHttpServletRequestWrapper;
import com.bytedance.itmd.open.common.LockCallback;
import com.bytedance.itmd.open.common.RedissonLockTemplate;
import com.bytedance.itmd.open.common.ResultEnum;
import com.bytedance.itmd.open.model.dto.ParamDTO;
import com.bytedance.itmd.open.service.ExternalAppService;
public class ValidateRequestFilter extends OncePerRequestFilter {
private final Logger logger = LoggerFactory.getLogger(getClass());
private ExternalAppService externalAppService;
private RedisTemplate<String, Object> redisTemplate;
private RedissonLockTemplate redissonLockTemplate;
private static final Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// 验证请求类型
if(request.getServletPath().startsWith("/open-api") && !HttpMethod.POST.matches(request.getMethod())) {
request.setAttribute("resultEnum", ResultEnum.HTTP_METHOD_ERROR);
request.getRequestDispatcher("/exception").forward(request, response);
return;
}
// 验证参数
CustomHttpServletRequestWrapper customHttpServletRequestWrapper = new CustomHttpServletRequestWrapper(request);
String bodyJsonStr = new String(customHttpServletRequestWrapper.getBody());
ParamDTO paramDTO = JSON.parseObject(bodyJsonStr, ParamDTO.class);
if(paramDTO == null) {
request.setAttribute("resultEnum", ResultEnum.PARAMETER_VALIDATE_FAIL);
request.setAttribute("message", "请求参数为空");
request.getRequestDispatcher("/exception").forward(request, response);
return;
}
Set<ConstraintViolation<ParamDTO>> constraintViolationSet = validator.validate(paramDTO, Default.class);
if(constraintViolationSet.size() != 0) {
StringBuffer message = new StringBuffer("");
for (ConstraintViolation<ParamDTO> constraintViolation : constraintViolationSet) {
String prefix = message.toString().equals("") ? "" : ", ";
message.append(prefix).append(constraintViolation.getPropertyPath().toString()).append(" ").append(constraintViolation.getMessage());
}
request.setAttribute("resultEnum", ResultEnum.PARAMETER_VALIDATE_FAIL);
request.setAttribute("message", message.toString());
request.getRequestDispatcher("/exception").forward(request, response);
return;
}
// 验证请求时间戳, 在正负120秒之内
long timestamp = paramDTO.getTimestamp();
long nowTimestamp = System.currentTimeMillis();
if(Math.abs(nowTimestamp - timestamp) > 120000) {
request.setAttribute("resultEnum", ResultEnum.HTTP_REQUEST_EXPIRED);
request.getRequestDispatcher("/exception").forward(request, response);
return;
}
// 验证请求签名
String appId = paramDTO.getAppId();
String appIdKey = Constant.KEY_APP_ID_PREFIX ":" appId;
String appSecret = (String) redisTemplate.opsForValue().get(appIdKey);
if(appSecret == null) {
appSecret = redissonLockTemplate.tryLock("lock:" appIdKey, 5, 10, TimeUnit.SECONDS, new LockCallback<String>() {
@Override
public String doBusiness() {
String appSecret = (String) redisTemplate.opsForValue().get(appIdKey);
if(appSecret != null) {
return appSecret;
}
appSecret = externalAppService.getAppSecret(appId);
redisTemplate.opsForValue().set(appIdKey, appSecret == null ? "" : appSecret, 1, TimeUnit.DAYS);
return appSecret;
}
});
}
String businessData = paramDTO.getBusinessData();
String md5Sign = DigestUtils.md5DigestAsHex((appSecret timestamp businessData).getBytes("UTF-8"));
String sign = paramDTO.getSign();
if(logger.isInfoEnabled()) {
logger.info("客户端md5签名: {}", sign);
logger.info("appSecret timestamp businessData: {}", appSecret timestamp businessData);
logger.info("服务端md5签名: {}", md5Sign);
}
if(!sign.equals(md5Sign)) {
request.setAttribute("resultEnum", ResultEnum.HTTP_SIGN_INVALID);
request.getRequestDispatcher("/exception").forward(request, response);
return;
}
filterChain.doFilter(customHttpServletRequestWrapper, response);
}
public void setExternalAppService(ExternalAppService externalAppService) {
this.externalAppService = externalAppService;
}
public void setRedisTemplate(RedisTemplate<String, Object> redisTemplate) {
this.redisTemplate = redisTemplate;
}
public void setRedissonLockTemplate(RedissonLockTemplate redissonLockTemplate) {
this.redissonLockTemplate = redissonLockTemplate;
}
}