系统环境:
1.网页客户端页面
2.APP客户端
3.后台管理系统
涉及字段:
1.userCode(用户code,用于区分哪一个用户操作)
2.channelCode(渠道号,用于区分网页客户端,APP客户端,后台管理)
接口幂等产生的原因:
1.RPC远程调用时网络延迟(重试发送请求)
2.表单重复提交
基于Token方式防止API接口幂等:
客户端每次在调用接口的时候,需要在请求头中传递令牌token参数,每次令牌只能用一次。
一旦使用之后,就会被删除,这样可以有效防止重复提交。
步骤:
1.每一次对数据库有影响的操作,先调取生成令牌接口,获取token,存放在Redis
(例如可以token_userCode_channelCode_uuid 可以区分用户在哪一个渠道做的操作)
2.将获取到的token放入请求头中
3.操作接口获取对应的令牌,如果能够获取该令牌(将当前令牌删除掉)放行执行该业务逻辑
4.操作接口获取对应的令牌,如果获取不到该令牌,直接返回请勿重复提交
代码部分:
// 公共请求类
@Data
public class PrepCommenReq {
private String channelCode;//渠道号
private String reqDate;//请求时间
private String reqIp;//请求ip
private Integer custCode;//客户编码
private String eventSeq;//系统参考号
private String loginAccount;//登录号
private String userCode;//用户号
}
// Redis工具类
@Component
public class RedisService {
@Autowired
private StringRedisTemplate stringRedisTemplate;
public void setString(String key, Object data, Long timeout) {
if (data instanceof String) {
String value = (String) data;
stringRedisTemplate.opsForValue().set(key, value);
}
if (timeout != null) {
stringRedisTemplate.expire(key, timeout, TimeUnit.SECONDS);
}
}
public String getString(String key) {
return stringRedisTemplate.opsForValue().get(key);
}
public void delKey(String key) {
stringRedisTemplate.delete(key);
}
}
// Redis工具类
@Component
public class RedisToken {
@Autowired
private RedisService baseRedisService;
private static final long TOKENTIMEOUT = 60 * 60;
public String getToken(PrepCommenReq req) {
// 生成token 规则保证 临时且唯一 不支持分布式场景 分布式全局ID生成规则
String token = "token_" + req.getUserCode() + "_" + req.getChannelCode() + "_" + UUID.randomUUID();
// 如何保证token临时 (缓存)使用redis 实现缓存
baseRedisService.setString(token, token, TOKENTIMEOUT);
return token;
}
// 1.在调用接口之前生成对应的令牌(Token), 存放在Redis
// 2.调用接口的时候,将该令牌放入的请求头中
// 3.接口获取对应的令牌,如果能够获取该令牌(将当前令牌删除掉) 就直接执行该访问的业务逻辑
// 4.接口获取对应的令牌,如果获取不到该令牌 直接返回请勿重复提交
public boolean findToken(String tokenKey) {
// 3.接口获取对应的令牌,如果能够获取该(从redis获取令牌)令牌(将当前令牌删除掉) 就直接执行该访问的业务逻辑
String tokenValue = (String) baseRedisService.getString(tokenKey);
if (StringUtils.isEmpty(tokenValue)) {
return false;
}
// 保证每个接口对应的token 只能访问一次,保证接口幂等性问题
baseRedisService.delKey(tokenValue);
return true;
}
}
@RequestMapping("/member")
public interface MemberService {
//获取token
@RequestMapping("/redisToken")
String redisToken(PrepCommenReq req);
//api检验 insert会员
@RequestMapping("/addMemberExtApi")
public String addMemberExtApi(@RequestBody UserEntity user, HttpServletRequest request);
}
@Slf4j
@RestController
public class MemberServiceImpl extends BaseApiService implements MemberService {
@Autowired
private MemberDao memberDao;
@Autowired
private RedisService redisService;
@Autowired
private RedisToken redisToken;
@Override
public String redisToken(PrepCommenReq req) {
return redisToken.getToken(req);
}
@Override
public String addMemberExtApi(UserEntity user, HttpServletRequest request) {
// 如何使用Token 解决幂等性
// 步骤:
// 2.调用接口的时候,将该令牌放入的请求头中(获取请求头中的令牌)
String token = request.getHeader("token_userCode_channelCode");
if (StringUtils.isEmpty(token)) {
return "参数错误";
}
// 3.接口获取对应的令牌,如果能够获取该(从redis获取令牌)令牌(将当前令牌删除掉) 就直接执行该访问的业务逻辑
boolean isToken = redisToken.findToken(token);
// 4.接口获取对应的令牌,如果获取不到该令牌 直接返回请勿重复提交
if (!isToken) {
return "请勿重复提交!";
}
int result = memberDao.insertUser(user);
return result > 0 ? "添加成功" : "添加失败" + "";
}
}
若对每一个操作都执行一遍代码会显得繁琐,此时可以将token检验提取到aop中,添加一个api幂等注解,在需要防止重复请求的接口加上该注解即可
// 解决接口幂等性 支持网络延迟和重复请求
@Target(value = ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ExtApiIdempotent {
String type();
}
public interface ConstantUtils {
static final String EXTAPIHEAD = "head";
static final String EXTAPIFROM = "from";
}
@Aspect
@Component
public class ExtApiAopIdempotent {
@Autowired
private RedisToken redisToken;
// 1.使用AOP环绕通知拦截所有访问(controller)
@Pointcut("execution(public * com.itmayiedu.api.service.*.*(..))")
public void rlAop() {
}
// 环绕通知
@Around("rlAop()")
public Object doBefore(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
// 2.判断方法上是否有加ExtApiIdempotent
MethodSignature methodSignature = (MethodSignature) proceedingJoinPoint.getSignature();
ExtApiIdempotent declaredAnnotation = methodSignature.getMethod().getDeclaredAnnotation(ExtApiIdempotent.class);
// 3.如何方法上有加上ExtApiIdempotent
if (declaredAnnotation != null) {
String type = declaredAnnotation.type();
// 如何使用Token 解决幂等性
// 步骤:
String token = null;
HttpServletRequest request = getRequest();
if (type.equals(ConstantUtils.EXTAPIHEAD)) {
token = request.getHeader("token");
} else {
token = request.getParameter("token");
}
if (StringUtils.isEmpty(token)) {
return "参数错误";
}
// 3.接口获取对应的令牌,如果能够获取该(从redis获取令牌)令牌(将当前令牌删除掉) 就直接执行该访问的业务逻辑
boolean isToken = redisToken.findToken(token);
// 4.接口获取对应的令牌,如果获取不到该令牌 直接返回请勿重复提交
if (!isToken) {
response("请勿重复提交!");
// 后面方法不在继续执行
return null;
}
}
// 放行
Object proceed = proceedingJoinPoint.proceed();
return proceed;
}
public HttpServletRequest getRequest() {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
return request;
}
public void response(String msg) throws IOException {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletResponse response = attributes.getResponse();
response.setHeader("Content-type", "text/html;charset=UTF-8");
PrintWriter writer = response.getWriter();
try {
writer.println(msg);
} catch (Exception e) {
} finally {
writer.close();
}
}
}
@Slf4j
@RestController
public class MemberServiceImpl extends BaseApiService implements MemberService {
@Autowired
private MemberDao memberDao;
@Autowired
private RedisService redisService;
@Autowired
private RedisToken redisToken;
@Override
public String redisToken(PrepCommenReq req) {
return redisToken.getToken(req);
}
@Override
@ExtApiIdempotent(type = ConstantUtils.EXTAPIHEAD)
public String addMemberExtApi(UserEntity user, HttpServletRequest request) {
int result = memberDao.insertUser(user);
return result > 0 ? "添加成功" : "添加失败" + "";
}
}