(Redis使用系列) Springboot 使用redis实现接口幂等性拦截 十一

x33g5p2x  于2022-06-27 转载在 Redis  
字(13.8k)|赞(0)|评价(0)|浏览(492)

前言

近期一个老项目出现了接口幂等性 校验问题,前端加了按钮置灰,

依然被人拉着接口参数一顿输出,还是重复调用了接口,小陈及时赶到现场,通过复制粘贴,完成了后端接口幂等性调用校验。

以前写过一篇关于接口简单限流防止重复调用的,但是跟该篇还是不一样的,该篇的角度是接口和参数整体一致才当做重复。

简单限流:  (Redis使用系列) Springboot 使用redis实现接口Api限流 十

该篇内容:

实现接口调用的幂等性校验

方案 :自定义注解+redis+拦截器+MD5 实现

草图,意会(用户标识不是必要,看业务场景是针对个人还是只针对接口&参数):

话不多说,开始实战。

PS: 前排提醒,如果你还不知道怎么springboot整合redis,可以先去看下redis使用系列的 一、二。

(Redis使用系列) SpringBoot 中对应2.0.x版本的Redis配置 一

(Redis使用系列) SpringBoot中Redis的RedisConfig 二

正文

自定义注解 怎么玩的 :      

①标记哪个接口需要进行幂等性拦截        

②每个接口可以要求幂等性范围时间不一样,举例:可以2秒内,可以3秒内,时间自己传        

③ 一旦触发了,提示语可以不同 ,举例:VIP的接口,普通用户的接口,提示语不一样(开玩笑)

效果:

实战开始:

核心三件套

注解、拦截器、拦截器配置

① RepeatDaMie.java

  1. import java.lang.annotation.ElementType;
  2. import java.lang.annotation.Retention;
  3. import java.lang.annotation.RetentionPolicy;
  4. import java.lang.annotation.Target;
  5. /**
  6. * @Author: JCccc
  7. * @Date: 2022-6-13 9:04
  8. * @Description: 自定义注解,防止重复提交
  9. */
  10. @Target({ElementType.METHOD})
  11. @Retention(RetentionPolicy.RUNTIME)
  12. public @interface RepeatDaMie {
  13. /**
  14. * 时间ms限制
  15. */
  16. public int second() default 1;
  17. /**
  18. * 提示消息
  19. */
  20. public String describe() default "重复提交了,兄弟";
  21. }

②ApiRepeatInterceptor.java

  1. import com.example.repeatdemo.annotation.RepeatDaMie;
  2. import com.example.repeatdemo.util.ContextUtil;
  3. import com.example.repeatdemo.util.Md5Encrypt;
  4. import com.example.repeatdemo.util.RedisUtils;
  5. import com.example.repeatdemo.wrapper.CustomHttpServletRequestWrapper;
  6. import com.fasterxml.jackson.databind.ObjectMapper;
  7. import org.slf4j.Logger;
  8. import org.slf4j.LoggerFactory;
  9. import org.springframework.stereotype.Component;
  10. import org.springframework.web.method.HandlerMethod;
  11. import org.springframework.web.servlet.HandlerInterceptor;
  12. import javax.servlet.http.HttpServletRequest;
  13. import javax.servlet.http.HttpServletResponse;
  14. import java.io.IOException;
  15. import java.util.Objects;
  16. /**
  17. * @Author: JCccc
  18. * @Date: 2022-6-15 9:11
  19. * @Description: 接口幂等性校验拦截器
  20. */
  21. @Component
  22. public class ApiRepeatInterceptor implements HandlerInterceptor {
  23. private final Logger log = LoggerFactory.getLogger(this.getClass());
  24. private static final String POST="POST";
  25. private static final String GET="GET";
  26. @Override
  27. public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
  28. try {
  29. if (handler instanceof HandlerMethod) {
  30. HandlerMethod handlerMethod = (HandlerMethod) handler;
  31. // 获取RepeatDaMie注解
  32. RepeatDaMie repeatDaMie = handlerMethod.getMethodAnnotation(RepeatDaMie.class);
  33. if (null==repeatDaMie) {
  34. return true;
  35. }
  36. //限制的时间范围
  37. int seconds = repeatDaMie.second();
  38. //这个用户唯一标识,可以自己细微调整,是userId还是token还是sessionId还是不需要
  39. String userUniqueKey = request.getHeader("userUniqueKey");
  40. String method = request.getMethod();
  41. String apiParams = "";
  42. if (GET.equals(method)){
  43. log.info("GET请求来了");
  44. apiParams = new ObjectMapper().writeValueAsString(request.getParameterMap());
  45. }else if (POST.equals(method)){
  46. log.info("POST请求来了");
  47. CustomHttpServletRequestWrapper wrapper = (CustomHttpServletRequestWrapper) request;
  48. apiParams = wrapper.getBody();
  49. }
  50. log.info("当前参数是:{}",apiParams);
  51. // 存储key
  52. String keyRepeatDaMie = Md5Encrypt.md5(userUniqueKey+request.getServletPath()+apiParams) ;
  53. RedisUtils redisUtils = ContextUtil.getBean(RedisUtils.class);
  54. if (Objects.nonNull(redisUtils.get(keyRepeatDaMie))){
  55. log.info("重复请求了,重复请求了,拦截了");
  56. returnData(response,repeatDaMie.describe());
  57. return false;
  58. }else {
  59. redisUtils.setWithTime(keyRepeatDaMie, true,seconds);
  60. }
  61. }
  62. return true;
  63. } catch (Exception e) {
  64. log.warn("请求出现异常,errorMsg={}",e.getMessage());
  65. returnData(response,"请求出现异常");
  66. return false;
  67. }
  68. return true;
  69. }
  70. public void returnData(HttpServletResponse response,String msg) throws IOException {
  71. response.setCharacterEncoding("UTF-8");
  72. response.setContentType("application/json; charset=utf-8");
  73. ObjectMapper objectMapper = new ObjectMapper();
  74. //这里传提示语可以改成自己项目的返回数据封装的类
  75. response.getWriter().println(objectMapper.writeValueAsString(msg));
  76. return;
  77. }
  78. }

③ WebConfig.java

  1. import org.springframework.context.annotation.Configuration;
  2. import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
  3. import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
  4. /**
  5. * @Author: JCccc
  6. * @Date: 2022-6-15 9:24
  7. * @Description:
  8. */
  9. @Configuration
  10. public class WebConfig implements WebMvcConfigurer {
  11. @Override
  12. public void addInterceptors(InterceptorRegistry registry) {
  13. registry.addInterceptor(new ApiRepeatInterceptor()).addPathPatterns("/**");
  14. }
  15. }

工具类三件套

①ContextUtil.java

  1. import org.springframework.beans.BeansException;
  2. import org.springframework.context.ApplicationContext;
  3. import org.springframework.context.ApplicationContextAware;
  4. import org.springframework.stereotype.Component;
  5. /**
  6. * @Author: JCccc
  7. * @Date: 2022-6-15 9:24
  8. * @Description:
  9. */
  10. @Component
  11. public final class ContextUtil implements ApplicationContextAware {
  12. protected static ApplicationContext applicationContext ;
  13. @Override
  14. public void setApplicationContext(ApplicationContext arg0) throws BeansException {
  15. if (applicationContext == null) {
  16. applicationContext = arg0;
  17. }
  18. }
  19. public static Object getBean(String name) {
  20. //name表示其他要注入的注解name名
  21. return applicationContext.getBean(name);
  22. }
  23. /**
  24. * 拿到ApplicationContext对象实例后就可以手动获取Bean的注入实例对象
  25. */
  26. public static <T> T getBean(Class<T> clazz) {
  27. return applicationContext.getBean(clazz);
  28. }
  29. }

②Md5Encrypt.java

  1. import java.io.UnsupportedEncodingException;
  2. import java.security.MessageDigest;
  3. import java.security.NoSuchAlgorithmException;
  4. /**
  5. * @Author: JCccc
  6. * @CreateTime: 2018-10-30
  7. * @Description:
  8. */
  9. public class Md5Encrypt {
  10. private static final char[] DIGITS = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a',
  11. 'b', 'c', 'd', 'e', 'f'};
  12. /**
  13. * 对字符串进行MD5加密
  14. *
  15. * @param text 明文
  16. * @return 密文
  17. */
  18. public static String md5(String text) {
  19. MessageDigest msgDigest = null;
  20. try {
  21. msgDigest = MessageDigest.getInstance("MD5");
  22. } catch (NoSuchAlgorithmException e) {
  23. throw new IllegalStateException("System doesn't support MD5 algorithm.");
  24. }
  25. try {
  26. // 注意该接口是按照指定编码形式签名
  27. msgDigest.update(text.getBytes("UTF-8"));
  28. } catch (UnsupportedEncodingException e) {
  29. throw new IllegalStateException("System doesn't support your EncodingException.");
  30. }
  31. byte[] bytes = msgDigest.digest();
  32. String md5Str = new String(encodeHex(bytes));
  33. return md5Str;
  34. }
  35. private static char[] encodeHex(byte[] data) {
  36. int l = data.length;
  37. char[] out = new char[l << 1];
  38. // two characters form the hex value.
  39. for (int i = 0, j = 0; i < l; i++) {
  40. out[j++] = DIGITS[(0xF0 & data[i]) >>> 4];
  41. out[j++] = DIGITS[0x0F & data[i]];
  42. }
  43. return out;
  44. }
  45. }

③RedisUtils.java

  1. import org.springframework.beans.factory.annotation.Autowired;
  2. import org.springframework.data.redis.core.*;
  3. import org.springframework.stereotype.Component;
  4. import java.io.Serializable;
  5. import java.util.List;
  6. import java.util.Set;
  7. import java.util.concurrent.TimeUnit;
  8. @Component
  9. public class RedisUtils {
  10. @Autowired
  11. private RedisTemplate redisTemplate;
  12. /**
  13. * 写入String型 [ 键,值]
  14. *
  15. * @param key
  16. * @param value
  17. * @return
  18. */
  19. public boolean set(final String key, Object value) {
  20. boolean result = false;
  21. try {
  22. ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
  23. operations.set(key, value);
  24. result = true;
  25. } catch (Exception e) {
  26. e.printStackTrace();
  27. }
  28. return result;
  29. }
  30. /**
  31. * 写入String型,顺便带有过期时间 [ 键,值]
  32. *
  33. * @param key
  34. * @param value
  35. * @return
  36. */
  37. public boolean setWithTime(final String key, Object value,int seconds) {
  38. boolean result = false;
  39. try {
  40. ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
  41. operations.set(key, value,seconds, TimeUnit.SECONDS);
  42. result = true;
  43. } catch (Exception e) {
  44. e.printStackTrace();
  45. }
  46. return result;
  47. }
  48. /**
  49. * 批量删除对应的value
  50. *
  51. * @param keys
  52. */
  53. public void remove(final String... keys) {
  54. for (String key : keys) {
  55. remove(key);
  56. }
  57. }
  58. /**
  59. * 批量删除key
  60. *
  61. * @param pattern
  62. */
  63. public void removePattern(final String pattern) {
  64. Set<Serializable> keys = redisTemplate.keys(pattern);
  65. if (keys.size() > 0)
  66. redisTemplate.delete(keys);
  67. }
  68. /**
  69. * 删除对应的value
  70. *
  71. * @param key
  72. */
  73. public void remove(final String key) {
  74. if (exists(key)) {
  75. redisTemplate.delete(key);
  76. }
  77. }
  78. /**
  79. * 判断缓存中是否有对应的value
  80. *
  81. * @param key
  82. * @return
  83. */
  84. public boolean exists(final String key) {
  85. return redisTemplate.hasKey(key);
  86. }
  87. /**
  88. * 读取缓存
  89. *
  90. * @param key
  91. * @return
  92. */
  93. public Object get(final String key) {
  94. Object result = null;
  95. ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
  96. result = operations.get(key);
  97. return result;
  98. }
  99. /**
  100. * 哈希 添加
  101. * hash 一个键值(key->value)对集合
  102. *
  103. * @param key
  104. * @param hashKey
  105. * @param value
  106. */
  107. public void hmSet(String key, Object hashKey, Object value) {
  108. HashOperations<String, Object, Object> hash = redisTemplate.opsForHash();
  109. hash.put(key, hashKey, value);
  110. }
  111. /**
  112. * Hash获取数据
  113. *
  114. * @param key
  115. * @param hashKey
  116. * @return
  117. */
  118. public Object hmGet(String key, Object hashKey) {
  119. HashOperations<String, Object, Object> hash = redisTemplate.opsForHash();
  120. return hash.get(key, hashKey);
  121. }
  122. /**
  123. * 列表添加
  124. * list:lpush key value1
  125. *
  126. * @param k
  127. * @param v
  128. */
  129. public void lPush(String k, Object v) {
  130. ListOperations<String, Object> list = redisTemplate.opsForList();
  131. list.rightPush(k, v);
  132. }
  133. /**
  134. * 列表List获取
  135. * lrange: key 0 10 (读取的个数 从0开始 读取到下标为10 的数据)
  136. *
  137. * @param k
  138. * @param l
  139. * @param l1
  140. * @return
  141. */
  142. public List<Object> lRange(String k, long l, long l1) {
  143. ListOperations<String, Object> list = redisTemplate.opsForList();
  144. return list.range(k, l, l1);
  145. }
  146. /**
  147. * Set集合添加
  148. *
  149. * @param key
  150. * @param value
  151. */
  152. public void add(String key, Object value) {
  153. SetOperations<String, Object> set = redisTemplate.opsForSet();
  154. set.add(key, value);
  155. }
  156. /**
  157. * Set 集合获取
  158. *
  159. * @param key
  160. * @return
  161. */
  162. public Set<Object> setMembers(String key) {
  163. SetOperations<String, Object> set = redisTemplate.opsForSet();
  164. return set.members(key);
  165. }
  166. /**
  167. * Sorted set :有序集合添加
  168. *
  169. * @param key
  170. * @param value
  171. * @param scoure
  172. */
  173. public void zAdd(String key, Object value, double scoure) {
  174. ZSetOperations<String, Object> zset = redisTemplate.opsForZSet();
  175. zset.add(key, value, scoure);
  176. }
  177. /**
  178. * Sorted set:有序集合获取
  179. *
  180. * @param key
  181. * @param scoure
  182. * @param scoure1
  183. * @return
  184. */
  185. public Set<Object> rangeByScore(String key, double scoure, double scoure1) {
  186. ZSetOperations<String, Object> zset = redisTemplate.opsForZSet();
  187. return zset.rangeByScore(key, scoure, scoure1);
  188. }
  189. /**
  190. * 根据key获取Set中的所有值
  191. *
  192. * @param key 键
  193. * @return
  194. */
  195. public Set<Integer> sGet(String key) {
  196. try {
  197. return redisTemplate.opsForSet().members(key);
  198. } catch (Exception e) {
  199. e.printStackTrace();
  200. return null;
  201. }
  202. }
  203. /**
  204. * 根据value从一个set中查询,是否存在
  205. *
  206. * @param key 键
  207. * @param value 值
  208. * @return true 存在 false不存在
  209. */
  210. public boolean sHasKey(String key, Object value) {
  211. try {
  212. return redisTemplate.opsForSet().isMember(key, value);
  213. } catch (Exception e) {
  214. e.printStackTrace();
  215. return false;
  216. }
  217. }
  218. }

REDIS配置类

RedisConfig.java

  1. import com.fasterxml.jackson.annotation.JsonAutoDetect;
  2. import com.fasterxml.jackson.annotation.PropertyAccessor;
  3. import com.fasterxml.jackson.databind.ObjectMapper;
  4. import org.springframework.cache.CacheManager;
  5. import org.springframework.cache.annotation.EnableCaching;
  6. import org.springframework.context.annotation.Bean;
  7. import org.springframework.context.annotation.Configuration;
  8. import org.springframework.data.redis.cache.RedisCacheConfiguration;
  9. import org.springframework.data.redis.cache.RedisCacheManager;
  10. import org.springframework.data.redis.connection.RedisConnectionFactory;
  11. import org.springframework.data.redis.core.RedisTemplate;
  12. import org.springframework.data.redis.core.StringRedisTemplate;
  13. import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
  14. import org.springframework.data.redis.serializer.RedisSerializationContext;
  15. import org.springframework.data.redis.serializer.StringRedisSerializer;
  16. import static org.springframework.data.redis.cache.RedisCacheConfiguration.defaultCacheConfig;
  17. /**
  18. * @Author: JCccc
  19. * @CreateTime: 2018-09-11
  20. * @Description:
  21. */
  22. @Configuration
  23. @EnableCaching
  24. public class RedisConfig {
  25. @Bean
  26. public CacheManager cacheManager(RedisConnectionFactory connectionFactory) {
  27. RedisCacheConfiguration cacheConfiguration =
  28. defaultCacheConfig()
  29. .disableCachingNullValues()
  30. .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new Jackson2JsonRedisSerializer(Object.class)));
  31. return RedisCacheManager.builder(connectionFactory).cacheDefaults(cacheConfiguration).build();
  32. }
  33. @Bean
  34. public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
  35. RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
  36. redisTemplate.setConnectionFactory(factory);
  37. Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
  38. ObjectMapper om = new ObjectMapper();
  39. om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
  40. om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
  41. jackson2JsonRedisSerializer.setObjectMapper(om);
  42. //序列化设置 ,这样为了存储操作对象时正常显示的数据,也能正常存储和获取
  43. redisTemplate.setKeySerializer(new StringRedisSerializer());
  44. redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
  45. redisTemplate.setHashKeySerializer(new StringRedisSerializer());
  46. redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
  47. return redisTemplate;
  48. }
  49. @Bean
  50. public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory factory) {
  51. StringRedisTemplate stringRedisTemplate = new StringRedisTemplate();
  52. stringRedisTemplate.setConnectionFactory(factory);
  53. return stringRedisTemplate;
  54. }
  55. }

处理流数据三件套

【文章】流数据只能读一次的三件套,以后直接复制粘贴使用

最后写测试接口,看看效果(一个POST,一个GET):
故意把时间放大,1000秒内重复调用,符合我们拦截规则的都会被拦截。

TestController.java

  1. import com.example.repeatdemo.dto.PayOrderApply;
  2. import com.example.repeatdemo.annotation.RepeatDaMie;
  3. import org.slf4j.Logger;
  4. import org.slf4j.LoggerFactory;
  5. import org.springframework.web.bind.annotation.*;
  6. /**
  7. * @Author: JCccc
  8. * @Date: 2022-6-05 9:44
  9. * @Description:
  10. */
  11. @RestController
  12. public class TestController {
  13. private final Logger log = LoggerFactory.getLogger(this.getClass());
  14. @RepeatDaMie(second = 1000,describe = "尊敬的客户,您慢点")
  15. @PostMapping(value = "/doPost")
  16. @ResponseBody
  17. public void test(@RequestBody PayOrderApply payOrderApply) {
  18. log.info("Controller POST请求:"+payOrderApply.toString());
  19. }
  20. @RepeatDaMie(second = 1000,describe = "大哥,你冷静点")
  21. @GetMapping(value = "/doGet")
  22. @ResponseBody
  23. public void doGet( PayOrderApply payOrderApply) {
  24. log.info("Controller GET请求:"+payOrderApply.toString());
  25. }
  26. }

PayOrderApply.java

  1. /**
  2. * @Author: JCccc
  3. * @Date: 2022-6-12 9:46
  4. * @Description:
  5. */
  6. public class PayOrderApply {
  7. private String sn;
  8. private Long amount;
  9. private String proCode;
  10. public String getSn() {
  11. return sn;
  12. }
  13. public void setSn(String sn) {
  14. this.sn = sn;
  15. }
  16. public Long getAmount() {
  17. return amount;
  18. }
  19. public void setAmount(Long amount) {
  20. this.amount = amount;
  21. }
  22. public String getProCode() {
  23. return proCode;
  24. }
  25. public void setProCode(String proCode) {
  26. this.proCode = proCode;
  27. }
  28. @Override
  29. public String toString() {
  30. return "PayOrderApply{" +
  31. "sn='" + sn + '\'' +
  32. ", amount=" + amount +
  33. ", proCode='" + proCode + '\'' +
  34. '}';
  35. }
  36. }

redis生成了值:

好了,该篇就到这吧、

CSDN 社区图书馆,开张营业!

深读计划,写书评领图书福利~

相关文章

最新文章

更多