互联网并发与安全系列教程(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数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore     ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库 ECS 实例和一台目标数据库 RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
目录
打赏
0
0
0
0
241
分享
相关文章
速卖通商品详情接口(速卖通API系列)
速卖通(AliExpress)是阿里巴巴旗下的跨境电商平台,提供丰富的商品数据。通过速卖通开放平台(AliExpress Open API),开发者可获取商品详情、订单管理等数据。主要功能包括商品搜索、商品详情、订单管理和数据报告。商品详情接口aliexpress.affiliate.productdetail.get用于获取商品标题、价格、图片等详细信息。开发者需注册账号并创建应用以获取App Key和App Secret,使用PHP等语言调用API。该接口支持多种请求参数和返回字段,方便集成到各类电商应用中。
【重磅发布】 免费领取阿里云百炼AI大模型100万Tokens教程出炉,API接口实战操作,DeepSeek-R1满血版即刻体验!
阿里云百炼是一站式大模型开发及应用构建平台,支持开发者和业务人员轻松设计、构建大模型应用。通过简单操作,用户可在5分钟内开发出大模型应用或在几小时内训练专属模型,专注于创新。
437 88
【重磅发布】 免费领取阿里云百炼AI大模型100万Tokens教程出炉,API接口实战操作,DeepSeek-R1满血版即刻体验!
👉「免费满血DeepSeek实战-联网搜索×Prompt秘籍|暨6平台横评」
满血 DeepSeek 免费用!支持联网搜索!创作声明:真人攥写-非AI生成,Written-By-Human-Not-By-AI
2496 9
👉「免费满血DeepSeek实战-联网搜索×Prompt秘籍|暨6平台横评」
API安全防护探析:F5助企业应对关键安全挑战
API安全防护探析:F5助企业应对关键安全挑战
20 6
以项目登录接口为例-大前端之开发postman请求接口带token的请求测试-前端开发必学之一-如果要学会联调接口而不是纯写静态前端页面-这个是必学-本文以优雅草蜻蜓Q系统API为实践来演示我们如何带token请求接口-优雅草卓伊凡
以项目登录接口为例-大前端之开发postman请求接口带token的请求测试-前端开发必学之一-如果要学会联调接口而不是纯写静态前端页面-这个是必学-本文以优雅草蜻蜓Q系统API为实践来演示我们如何带token请求接口-优雅草卓伊凡
57 5
以项目登录接口为例-大前端之开发postman请求接口带token的请求测试-前端开发必学之一-如果要学会联调接口而不是纯写静态前端页面-这个是必学-本文以优雅草蜻蜓Q系统API为实践来演示我们如何带token请求接口-优雅草卓伊凡
淘宝拍立淘按图搜索API接口系列的应用与数据解析
淘宝拍立淘按图搜索API接口是阿里巴巴旗下淘宝平台提供的一项基于图像识别技术的创新服务。以下是对该接口系列的应用与数据解析的详细分析
亚马逊商品详情接口(亚马逊 API 系列)
亚马逊作为全球最大的电商平台之一,提供了丰富的商品资源。开发者和电商从业者可通过亚马逊商品详情接口获取商品的描述、价格、评论、排名等数据,对市场分析、竞品研究、价格监控及业务优化具有重要价值。接口基于MWS服务,支持HTTP/HTTPS协议,需注册并获得API权限。Python示例展示了如何使用mws库调用接口获取商品详情。应用场景包括价格监控、市场调研、智能选品、用户推荐和库存管理等,助力电商运营和决策。
91 23
lazada商品详情接口 (lazada API系列)
Lazada 是东南亚知名电商平台,提供海量商品资源。通过其商品详情接口,开发者和商家可获取商品标题、价格、库存、描述、图片、用户评价等详细信息,助力市场竞争分析、商品优化及库存管理。接口采用 HTTP GET 请求,返回 JSON 格式的响应数据,支持 Python 等语言调用。应用场景包括竞品分析、价格趋势研究、用户评价分析及电商应用开发,为企业决策和用户体验提升提供有力支持。
82 21
eBay商品详情接口(ebay API系列)
eBay 商品详情接口是电商从业者、开发者和数据分析师获取商品详细信息的重要工具,涵盖标题、价格、库存、卖家信息等。使用前需在 eBay 开发者平台注册并获取 API 凭证,通过 HTTP GET 请求调用接口,返回 JSON 格式数据。Python 示例代码展示了如何发送请求并解析响应,确保合法合规使用数据。
53 12
阿里巴巴商品详情接口(阿里巴巴 API 系列)
在电商开发中,获取阿里巴巴商品详情信息对数据分析、竞品研究等至关重要。通过调用其商品详情接口,开发者可获取标题、价格、图片、描述等数据,满足多种业务需求。接口采用HTTPS协议,支持GET/POST请求,返回JSON格式数据。示例代码展示了如何使用Python的requests库进行接口请求,需传递商品ID和访问令牌。实际应用时,请依据官方文档调整参数并确保安全性。
51 10

热门文章

最新文章

AI助理

你好,我是AI助理

可以解答问题、推荐解决方案等