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

简介: 互联网并发与安全系列教程(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";
  }
}


目录
相关文章
|
8月前
|
缓存 监控 前端开发
顺企网 API 开发实战:搜索 / 详情接口从 0 到 1 落地(附 Elasticsearch 优化 + 错误速查)
企业API开发常陷参数、缓存、错误处理三大坑?本指南拆解顺企网双接口全流程,涵盖搜索优化、签名验证、限流应对,附可复用代码与错误速查表,助你2小时高效搞定开发,提升响应速度与稳定性。
|
9月前
|
人工智能 数据可视化 测试技术
Postman 性能测试教程:快速上手 API 压测
本文介绍API上线后因高频调用导致服务器告警,通过Postman与Apifox进行压力测试排查性能瓶颈。对比两款工具在批量请求、断言验证、可视化报告等方面的优劣,探讨API性能优化策略及行业未来发展方向。
Postman 性能测试教程:快速上手 API 压测
|
9月前
|
消息中间件 JSON Java
高效的并发管理:房间预订 API 的乐观锁和消息队列
本文探讨了在高并发场景下酒店预订系统的挑战,重点分析了乐观锁定和消息队列两种策略。通过代码示例展示了如何使用 Spring Boot 和 RabbitMQ 实现异步流程,以及如何通过乐观锁和 ReentrantLock 避免超卖问题,从而确保系统在多用户同时请求时的稳定性与数据一致性。
213 0
高效的并发管理:房间预订 API 的乐观锁和消息队列
|
9月前
|
数据可视化 测试技术 API
从接口性能到稳定性:这些API调试工具,让你的开发过程事半功倍
在软件开发中,接口调试与测试对接口性能、稳定性、准确性及团队协作至关重要。随着开发节奏加快,传统方式已难满足需求,专业API工具成为首选。本文介绍了Apifox、Postman、YApi、SoapUI、JMeter、Swagger等主流工具,对比其功能与适用场景,并推荐Apifox作为集成度高、支持中文、可视化强的一体化解决方案,助力提升API开发与测试效率。
|
8月前
|
JSON 算法 API
Python采集淘宝商品评论API接口及JSON数据返回全程指南
Python采集淘宝商品评论API接口及JSON数据返回全程指南
|
8月前
|
JSON API 数据安全/隐私保护
Python采集淘宝拍立淘按图搜索API接口及JSON数据返回全流程指南
通过以上流程,可实现淘宝拍立淘按图搜索的完整调用链路,并获取结构化的JSON商品数据,支撑电商比价、智能推荐等业务场景。
|
9月前
|
JSON 前端开发 API
如何调用体育数据足篮接口API
本文介绍如何调用体育数据API:首先选择可靠服务商并注册获取密钥,接着阅读文档了解基础URL、端点、参数及请求头,然后使用Python等语言发送请求、解析JSON数据,最后将数据应用于Web、App或分析场景,同时注意密钥安全、速率限制与错误处理。
940 152
|
8月前
|
存储 监控 安全
132_API部署:FastAPI与现代安全架构深度解析与LLM服务化最佳实践
在大语言模型(LLM)部署的最后一公里,API接口的设计与安全性直接决定了模型服务的可用性、稳定性与用户信任度。随着2025年LLM应用的爆炸式增长,如何构建高性能、高安全性的REST API成为开发者面临的核心挑战。FastAPI作为Python生态中最受青睐的Web框架之一,凭借其卓越的性能、强大的类型安全支持和完善的文档生成能力,已成为LLM服务化部署的首选方案。
1317 3
|
9月前
|
JSON API 数据安全/隐私保护
Python采集淘宝评论API接口及JSON数据返回全流程指南
Python采集淘宝评论API接口及JSON数据返回全流程指南
|
8月前
|
人工智能 自然语言处理 测试技术
Apipost智能搜索:只需用业务语言描述需求,就能精准定位目标接口,API 搜索的下一代形态!
在大型项目中,API 数量庞大、命名不一,导致“找接口”耗时费力。传统工具依赖关键词搜索,难以应对语义模糊或命名不规范的场景。Apipost AI 智能搜索功能,支持自然语言查询,如“和用户登录有关的接口”,系统可理解语义并精准匹配目标接口。无论是新人上手、模糊查找还是批量定位,都能大幅提升检索效率,降低协作成本。从关键词到语义理解,智能搜索让开发者少花时间找接口,多专注核心开发,真正实现高效协作。

热门文章

最新文章