互联网并发与安全系列教程(08) - API接口幂等设计与实现

本文涉及的产品
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
云数据库 Tair(兼容Redis),内存型 2GB
简介: 互联网并发与安全系列教程(08) - API接口幂等设计与实现

实现思路:

  • 客户端每次在调用接口的时候,需要在请求头中,传递令牌参数,每次令牌只能用一次,有超时时间限定。
  • 一旦使用之后,就会被删除,这样可以有效防止重复提交。

本文目录结构:

l____1.BaseRedisService封装Redis

l____2. RedisTokenUtils工具类

l____3.自定义Api幂等注解和切面

l____4.幂等注解使用

l____5.封装生成token注解

l____6.改造ExtApiAopIdempotent

l____7.API接口保证幂等性

l____8. 页面防止重复提交

下面直接上代码

1.BaseRedisService封装Redis

@Component
public class BaseRedisService {
  @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 Object getString(String key) {
    return stringRedisTemplate.opsForValue().get(key);
  }
  public void delKey(String key) {
    stringRedisTemplate.delete(key);
  }
}

2. RedisTokenUtils工具类

@Component
public class RedisTokenUtils {
  private long timeout = 60 * 60;
  @Autowired
  private BaseRedisService baseRedisService;
  // 将token存入在redis
  public String getToken() {
    String token = "token" + System.currentTimeMillis();
    baseRedisService.setString(token, token, timeout);
    return token;
  }
  public boolean findToken(String tokenKey) {
    String token = (String) baseRedisService.getString(tokenKey);
    if (StringUtils.isEmpty(token)) {
      return false;
    }
    // token 获取成功后 删除对应tokenMapstoken
    baseRedisService.delKey(token);
    return true;
  }
}

3.自定义Api幂等注解和切面

1. 注解:

@Target(value = ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ExtApiIdempotent {
  String value();
}

2. 切面:

@Aspect
@Component
public class ExtApiAopIdempotent {
  @Autowired
  private RedisTokenUtils redisTokenUtils;
  @Pointcut("execution(public * com.itmayiedu.controller.*.*(..))")
  public void rlAop() {
  }
  @Around("rlAop()")
  public Object doBefore(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
    MethodSignature signature = (MethodSignature) proceedingJoinPoint.getSignature();
    ExtApiIdempotent extApiIdempotent = signature.getMethod().getDeclaredAnnotation(ExtApiIdempotent.class);
    if (extApiIdempotent == null) {
      // 直接执行程序
      Object proceed = proceedingJoinPoint.proceed();
      return proceed;
    }
    // 代码步骤:
    // 1.获取令牌 存放在请求头中
    HttpServletRequest request = getRequest();
    String token = request.getHeader("token");
    if (StringUtils.isEmpty(token)) {
      response("参数错误!");
      return null;
    }
    // 2.判断令牌是否在缓存中有对应的令牌
    // 3.如何缓存没有该令牌的话,直接报错(请勿重复提交)
    // 4.如何缓存有该令牌的话,直接执行该业务逻辑
    // 5.执行完业务逻辑之后,直接删除该令牌。
    if (!redisTokenUtils.findToken(token)) {
      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();
    }
  }
}

4.幂等注解使用

// 从redis中获取Token
@RequestMapping("/redisToken")
public String RedisToken() {
  return redisTokenUtils.getToken();
}
// 验证Token
@RequestMapping(value = "/addOrderExtApiIdempotent", produces = "application/json; charset=utf-8")
@ExtApiIdempotent
public String addOrderExtApiIdempotent(@RequestBody OrderEntity orderEntity, HttpServletRequest request) {
  int result = orderMapper.addOrder(orderEntity);
  return result > 0 ? "添加成功" : "添加失败" + "";
}

5.封装生成token注解

@Target(value = ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ExtApiToken {
}

6.改造ExtApiAopIdempotent

@Aspect
@Component
public class ExtApiAopIdempotent {
  @Autowired
  private RedisTokenUtils redisTokenUtils;
  @Pointcut("execution(public * com.itmayiedu.controller.*.*(..))")
  public void rlAop() {
  }
  // 前置通知转发Token参数
  @Before("rlAop()")
  public void before(JoinPoint point) {
    MethodSignature signature = (MethodSignature) point.getSignature();
    ExtApiToken extApiToken = signature.getMethod().getDeclaredAnnotation(ExtApiToken.class);
    if (extApiToken != null) {
      extApiToken();
    }
  }
  // 环绕通知验证参数
  @Around("rlAop()")
  public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
    MethodSignature signature = (MethodSignature) proceedingJoinPoint.getSignature();
    ExtApiIdempotent extApiIdempotent = signature.getMethod().getDeclaredAnnotation(ExtApiIdempotent.class);
    if (extApiIdempotent != null) {
      return extApiIdempotent(proceedingJoinPoint, signature);
    }
    // 放行
    Object proceed = proceedingJoinPoint.proceed();
    return proceed;
  }
  // 验证Token
  public Object extApiIdempotent(ProceedingJoinPoint proceedingJoinPoint, MethodSignature signature)
      throws Throwable {
    ExtApiIdempotent extApiIdempotent = signature.getMethod().getDeclaredAnnotation(ExtApiIdempotent.class);
    if (extApiIdempotent == null) {
      // 直接执行程序
      Object proceed = proceedingJoinPoint.proceed();
      return proceed;
    }
    // 代码步骤:
    // 1.获取令牌 存放在请求头中
    HttpServletRequest request = getRequest();
    String valueType = extApiIdempotent.value();
    if (StringUtils.isEmpty(valueType)) {
      response("参数错误!");
      return null;
    }
    String token = null;
    if (valueType.equals(ConstantUtils.EXTAPIHEAD)) {
      token = request.getHeader("token");
    } else {
      token = request.getParameter("token");
    }
    if (StringUtils.isEmpty(token)) {
      response("参数错误!");
      return null;
    }
    if (!redisTokenUtils.findToken(token)) {
      response("请勿重复提交!");
      return null;
    }
    Object proceed = proceedingJoinPoint.proceed();
    return proceed;
  }
  public void extApiToken() {
    String token = redisTokenUtils.getToken();
    getRequest().setAttribute("token", token);
  }
  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();
    }
  }
}

7.API接口保证幂等性

@RestController
public class OrderController {
  @Autowired
  private OrderMapper orderMapper;
  @Autowired
  private RedisTokenUtils redisTokenUtils;
  // 从redis中获取Token
  @RequestMapping("/redisToken")
  public String RedisToken() {
    return redisTokenUtils.getToken();
  }
  // 验证Token
  @RequestMapping(value = "/addOrderExtApiIdempotent", produces = "application/json; charset=utf-8")
  @ExtApiIdempotent(value = ConstantUtils.EXTAPIHEAD)
  public String addOrderExtApiIdempotent(@RequestBody OrderEntity orderEntity, HttpServletRequest request) {
    int result = orderMapper.addOrder(orderEntity);
    return result > 0 ? "添加成功" : "添加失败" + "";
  }
}

8. 页面防止重复提交

@Controller
public class OrderPageController {
  @Autowired
  private OrderMapper orderMapper;
  @RequestMapping("/indexPage")
  @ExtApiToken
  public String indexPage(HttpServletRequest req) {
    return "indexPage";
  }
  @RequestMapping("/addOrderPage")
  @ExtApiIdempotent(value = ConstantUtils.EXTAPIFROM)
  public String addOrder(OrderEntity orderEntity) {
    int addOrder = orderMapper.addOrder(orderEntity);
    return addOrder > 0 ? "success" : "fail";
  }
}


相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
目录
相关文章
|
2月前
|
JSON 监控 API
在线网络PING接口检测服务器连通状态免费API教程
接口盒子提供免费PING检测API,可测试域名或IP的连通性与响应速度,支持指定地域节点,适用于服务器运维和网络监控。
|
2月前
|
JSON API PHP
通用图片搜索API:百度源免费接口教程
本文介绍一款基于百度图片搜索的免费API接口,由接口盒子提供。支持关键词搜索,具备详细请求与返回参数说明,并提供PHP及Python调用示例。开发者可快速集成实现图片搜索功能,适用于内容聚合、素材库建设等场景。
|
2月前
|
JSON 机器人 API
随机昵称网名API接口教程:轻松获取百万创意昵称库
接口盒子提供随机昵称网名API,拥有百万级中文昵称库,支持聊天机器人、游戏角色等场景的昵称生成。提供详细调用指南及多语言示例代码,助力开发者高效集成。
|
2月前
|
JSON API PHP
天气预报免费API接口【地址查询版】使用教程
本文介绍了如何使用中国气象局官方数据提供的免费天气预报API接口,通过省份和地点查询指定地区当日天气信息。该接口由接口盒子支持,提供JSON格式数据、GET/POST请求方式,并需注册获取用户ID和KEY进行身份验证。
1412 2
|
2月前
|
存储 JSON API
文本存储免费API接口教程
接口盒子提供免费文本存储服务,支持1000条记录,每条最多5000字符,适用于公告、日志、配置等场景,支持修改与读取。
|
2月前
|
数据采集 JSON 监控
获取网页状态码(可指定地域)免费API接口教程
本文介绍如何使用接口盒子的免费API获取网页状态码,支持国内、香港、美国等不同地域访问节点。内容包括接口参数、调用方法及示例,适用于网站监控、链接检查等场景。
|
2月前
|
JSON 物联网 API
天气预报免费API接口【IP查询版】使用教程
IP查询天气API是一款免费实用的接口,可根据IP地址自动获取所在地天气预报,支持自定义IP查询。核心功能包括自动识别请求IP、全国IP天气查询,数据源自中国气象局,无日调用上限。提供详细的返回参数及多语言示例代码,适用于网站、APP、物联网设备等应用场景。
|
22天前
|
JSON API 数据格式
淘宝/天猫图片搜索API接口,json返回数据。
淘宝/天猫平台虽未开放直接的图片搜索API,但可通过阿里妈妈淘宝联盟或天猫开放平台接口实现类似功能。本文提供基于淘宝联盟的图片关联商品搜索Curl示例及JSON响应说明,适用于已获权限的开发者。如需更高精度搜索,可选用阿里云视觉智能API。
|
20天前
|
JSON API 数据安全/隐私保护
深度分析淘宝卖家订单详情API接口,用json返回数据
淘宝卖家订单详情API(taobao.trade.fullinfo.get)是淘宝开放平台提供的重要接口,用于获取单个订单的完整信息,包括订单状态、买家信息、商品明细、支付与物流信息等,支撑订单管理、ERP对接及售后处理。需通过appkey、appsecret和session认证,并遵守调用频率与数据权限限制。本文详解其使用方法并附Python调用示例。
|
25天前
|
监控 算法 API
电商API接口对接实录:淘宝优惠券接口对接处理促销监控系统
在电商开发中,淘宝详情页的“券后价计算”是极易出错的环节。本文作者结合实战经验,分享了因忽略满减券门槛、有效期、适用范围等导致的踩坑经历,并提供了完整的解决方案,包括淘宝API签名生成、券后价计算逻辑、常见坑点及优化建议,助力开发者精准实现券后价功能,避免业务损失。