SpringBoot实现接口等幂次校验的示例代码
作者:one_smail
本文主要介绍了SpringBoot实现接口等幂次校验的示例代码,文中通过示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
接口等幂性通俗的来说就是同一时间内,发起多次请求只有一次请求成功;其目的时防止多次提交,数据重复入库,表单验证网络延迟重复提交等问题。
比如:
- 订单接口, 不能多次创建订单
- 支付接口, 重复支付同一笔订单只能扣一次钱
- 支付宝回调接口, 可能会多次回调, 必须处理重复回调
- 普通表单提交接口, 因为网络超时等原因多次点击提交, 只能成功一次
等等
主流的实现方案如下:
1、唯一索引:给表加唯一索引,该方法最简单,当数据重复插入时,直接报sql异常,对应用影响不大;
alter table 表名 add unique(字段)
示例,两个字段为唯一索引,如果出现完全一样的order_name,create_time就直接重复报异常;
alter table 'order' add unique(order_name,create_time)
2、先查询后判断:入库时先查询是否有该数据,如果没有则插入。否则不插入;
3、token机制:发起请求的时候先去redis获取token,将获取的token放入请求的hearder,当请求到达服务端的时候拦截请求,对请求的hearder中的token,进行校验,如果校验通过则放开拦截,删除token,否则使用自定义异常返回错误信息。
第一步:书写redis工具类
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Component; import java.util.concurrent.TimeUnit; @Component public class RedisUtils { @Autowired private RedisTemplate<String,Object> redisTemplate; /** * 判断key是否存在 * @param key 键 * @return */ public boolean hasKey(String key){ try { return redisTemplate.hasKey(key); }catch (Exception e){ e.printStackTrace(); return false; } } /** * 删除key * @param key 键 * @return */ public Boolean del(String key){ if (key != null && key.length() > 0){ return redisTemplate.delete(key); }else { return false; } } /** * 普通缓存获取 * @param key 键 * @return */ public Object get(String key){ return key == null ? null : redisTemplate.opsForValue().get(key); } /** * 普通缓存放入并设置时间 * @param key 键 * @param value 值 * @param time 时间(秒) time要大于0 如果time小于等于0 将设置无限期 * @return */ public boolean set(String key,Object value,long time){ try { if (time > 0){ redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS); } return true; }catch (Exception e){ e.printStackTrace(); return false; } } }
第二步、书写token工具类
import com.smile.project.exception.utils.CodeMsg; import com.smile.project.exception.utils.CommonException; import io.netty.util.concurrent.GlobalEventExecutor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.springframework.util.DigestUtils; import org.springframework.util.StringUtils; import javax.servlet.http.HttpServletRequest; import java.util.UUID; /** * 使用uuid生成随机字符串, * 通过md5加密防止token被解密,保证token的唯一性与安全性 * 设置过期时间为30秒,即在30秒内之恶能提交成功一次请求 */ @Component public class TokenUtils { @Autowired RedisUtils redisUtils; //token过期时间为30秒 private final static Long TOKEN_EXPIRE = 30L; private final static String TOKEN_NAME = "token"; /** * 生成token放入缓存 */ public String generateToken(){ String uuid = UUID.randomUUID().toString(); String token = DigestUtils.md5DigestAsHex(uuid.getBytes()); redisUtils.set(TOKEN_NAME,token,TOKEN_EXPIRE); return token; } /** * token校验 */ public boolean verifyToken(HttpServletRequest request){ String token = request.getHeader(TOKEN_NAME); //header中不存在token if (StringUtils.isEmpty(token)){ //抛出自定义异常 System.out.println("token不存在"); throw new CommonException(CodeMsg.NOT_TOKEN); } //缓存中不存在 if (!redisUtils.hasKey(TOKEN_NAME)){ System.out.println("token已经过期"); throw new CommonException(CodeMsg.TIME_OUT_TOKEN); } String cachToken = (String) redisUtils.get(TOKEN_NAME); if (!token.equals(cachToken)){ System.out.println("token检验失败"); throw new CommonException(CodeMsg.VALIDA_ERROR_TOKEN); } //移除token Boolean del = redisUtils.del(TOKEN_NAME); if (!del){ System.out.println("token删除失败"); throw new CommonException(CodeMsg.DEL_ERROR_TOKEN); } return true; } }
第三步:定义注解,使用在方法上,当控制层的方法上被注释时,表示该请求为等幂性请求
import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * 当控制层的方法上被注释时,表示该请求为等幂性请求 */ @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface Idempotent { }
第四步:拦截器配置。选择前置拦截器,每次请求都校验到达的方法上是否有等幂性注解,如果有则进行token校验
import com.smile.project.redis.utils.Idempotent; import com.smile.project.redis.utils.TokenUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.springframework.web.method.HandlerMethod; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.ModelAndView; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.lang.reflect.Method; @Component public class IdempotentInterceptor implements HandlerInterceptor { @Autowired private TokenUtils tokenUtils; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (!(handler instanceof HandlerMethod)){ return true; } //对有Idempotent注解的方法进行拦截校验 HandlerMethod handlerMethod = (HandlerMethod) handler; Method method = handlerMethod.getMethod(); Idempotent methodAnnotation = method.getAnnotation(Idempotent.class); if (methodAnnotation != null){ //token校验 tokenUtils.verifyToken(request); } return true; } @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { } }
第五步:对拦截器进行url模式匹配,并注入spring容器
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; /** * 对拦截器进行url模式匹配,并注入spring容器 */ @Configuration public class WebConfiguration implements WebMvcConfigurer { @Autowired IdempotentInterceptor idempotentInterceptor; @Override public void addInterceptors(InterceptorRegistry registry) { //拦截所有请求 registry.addInterceptor(idempotentInterceptor).addPathPatterns("/**"); } }
第六步:控制层
对控制层进行编写,发起请求时通过getToken方法获取token,将获取的token放入hearder后,再请求具体方法。正常请求具体方法的时候注意在hearder中加入token,否则是失败
import com.alibaba.fastjson.JSONObject; import com.smile.project.exception.utils.CodeMsg; import com.smile.project.exception.utils.ResultPage; import com.smile.project.redis.utils.Idempotent; import com.smile.project.redis.utils.TokenUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class SmileController { @Autowired TokenUtils tokenUtils; @GetMapping("smile/token") public ResultPage getToken(){ String token = tokenUtils.generateToken(); JSONObject jsonObject = new JSONObject(); jsonObject.put("token",token); return ResultPage.success(CodeMsg.SUCCESS,jsonObject); } @Idempotent @GetMapping("smile/test") public ResultPage testIdempotent(){ return ResultPage.success(CodeMsg.SUCCESS,"校验成功"); } }
到此这篇关于SpringBoot实现接口等幂次校验的示例代码的文章就介绍到这了,更多相关SpringBoot实现接口等幂次校验 内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!