首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >开放平台的简单实现(access key & access secret)

开放平台的简单实现(access key & access secret)

作者头像
北漂的我
发布2021-03-12 11:23:34
2.6K0
发布2021-03-12 11:23:34
举报
文章被收录于专栏:北漂的我北漂的我

1. 服务端需要维护一个表,保存客户端调用的 access key 和 access secret

2. 所有的客户端发起的请求都为 POST 请求,post 请求参数都放在 body 中

{
	"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值就直接拒绝访问.

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;
	}
}
本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
云数据库 Redis
腾讯云数据库 Redis(TencentDB for Redis)是腾讯云打造的兼容 Redis 协议的缓存和存储服务。丰富的数据结构能帮助您完成不同类型的业务场景开发。支持主从热备,提供自动容灾切换、数据备份、故障迁移、实例监控、在线扩容、数据回档等全套的数据库服务。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档