温馨提示×

温馨提示×

您好,登录后才能下订单哦!

密码登录×
登录注册×
其他方式登录
点击 登录注册 即表示同意《亿速云用户服务条款》

怎么用token机制实现接口自动幂等

发布时间:2022-03-22 17:00:02 来源:亿速云 阅读:244 作者:iii 栏目:大数据

这篇“怎么用token机制实现接口自动幂等”文章的知识点大部分人都不太理解,所以小编给大家总结了以下内容,内容详细,步骤清晰,具有一定的借鉴价值,希望大家阅读完这篇文章能有所收获,下面我们一起来看看这篇“怎么用token机制实现接口自动幂等”文章吧。

什么叫幂等接口

幂等性,就是只多次操作的结果是一致的。这里可能有人会有疑问。

问:为什么要多次操作结果都一致呢?比如我查询数据,每次查出来的都一样,即使我修改了每次查出来的也都要一样吗?

答:我们说的多次,是指同一次请求中的多次操作。这个多次操作可能会在如下情况发生:


  • 前端重复提交。比如这个业务处理需要2秒钟,我在2秒之内,提交按钮连续点了3次,如果非幂等性接口,那么后端就会处理3次。如果是查询,自然是没有影响的,因为查询本身就是幂等操作,但如果是新增,本来只是新增1条记录的,连点3次,就增加了3条,这显然不行。



  • 响应超时而导致请求重试:在微服务相互调用的过程中,假如订单服务调用支付服务,支付服务支付成功了,但是订单服务接收支付服务返回的信息时超时了,于是订单服务进行重试,又去请求支付服务,结果支付服务又扣了一遍用户的钱。如果真这样的话,用户估计早就提着砍刀来了。


如何设计幂等接口

经过上面的描述,相信大家已经清楚了什么叫接口幂等性及其重要性。那么如何设计呢?大致有以下几种方案:


  • 数据库记录状态机制:即每次操作前先查询状态,根据数据库记录的状态来判断是否要继续执行操作。比如订单服务调用支付服务,每次调用之前,先查询该笔订单的支付状态,从而避免重复操作。



  • token机制:请求业务接口之前,先请求token接口(会将生成的token放入redis中)获取一个token,然后请求业务接口时,带上token。在进行业务操作之前,我们先获取请求中携带的token,看看在redis中是否有该token,有的话,就删除,删除成功说明token校验通过,并且继续执行业务操作;如果redis中没有该token,说明已经被删除了,也就是已经执行过业务操作了,就不让其再进行业务操作。大致流程如下:


怎么用token机制实现接口自动幂等

  • 其他方案:接口幂等性设计还有很多其他方案,比如全局唯一id、乐观锁等。本文主要讲token机制的使用,若感兴趣可以自行研究。


用token机制实现接口自动幂等
   

1、pom.xml:主要是引入了redis相关依赖

<dependency>    <groupId>org.springframework.boot</groupId>    <artifactId>spring-boot-starter-web</artifactId></dependency><!-- spring-boot-starter-data-redis --><dependency>    <groupId>org.springframework.boot</groupId>    <artifactId>spring-boot-starter-data-redis</artifactId></dependency><dependency>    <groupId>org.apache.commons</groupId>    <artifactId>commons-pool2</artifactId></dependency><!-- jedis --><dependency>    <groupId>redis.clients</groupId>    <artifactId>jedis</artifactId></dependency><dependency>    <groupId>org.projectlombok</groupId>    <artifactId>lombok</artifactId>    <optional>true</optional></dependency><dependency>    <groupId>org.springframework.boot</groupId>    <artifactId>spring-boot-starter-test</artifactId>    <scope>test</scope></dependency><!-- commons-lang3 --><dependency>    <groupId>org.apache.commons</groupId>    <artifactId>commons-lang3</artifactId></dependency><!-- org.json/json --><dependency>    <groupId>org.json</groupId>    <artifactId>json</artifactId>    <version>20190722</version></dependency>

2、application.yml:主要是配置redis

server:  port: 6666spring:  application:    name: idempotent-api  redis:    host: 192.168.2.43    port: 6379

3、业务代码:

•新建一个枚举,列出常用返回信息,如下:

@Getter@AllArgsConstructorpublic enum ResultEnum {  REPEATREQUEST(405, "重复请求"),  OPERATEEXCEPTION(406, "操作异常"),  HEADERNOTOKEN(407, "请求头未携带token"),  ERRORTOKEN(408, "token正确")  ;  private Integer code;  private String msg;}
     

     •新建一个JsonUtil,当请求异常时往页面中输出json:

public class JsonUtil {  private JsonUtil() {}  public static void writeJsonToPage(HttpServletResponse response, String msg) {      PrintWriter writer = null;      response.setCharacterEncoding("UTF-8");      response.setContentType("text/html; charset=utf-8");      try {          writer = response.getWriter();          writer.print(msg);      } catch (IOException e) {      } finally {          if (writer != null)              writer.close();      }  }}
     
     •新建一个RedisUtil,用来操作redis:      
@Componentpublic class RedisUtil {
 private RedisUtil() {}
 private static RedisTemplate redisTemplate;
 @Autowired  public  void setRedisTemplate(@SuppressWarnings("rawtypes") RedisTemplate redisTemplate) {      redisTemplate.setKeySerializer(new StringRedisSerializer());      //设置序列化Value的实例化对象      redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());      RedisUtil.redisTemplate = redisTemplate;  }
 /**   * 设置key-value,过期时间为timeout秒   * @param key   * @param value   * @param timeout   */  public static void setString(String key, String value, Long timeout) {      redisTemplate.opsForValue().set(key, value, timeout, TimeUnit.SECONDS);  }
 /**   * 设置key-value   * @param key   * @param value   */  public static void setString(String key, String value) {      redisTemplate.opsForValue().set(key, value);  }
 /**   * 获取key-value   * @param key   * @return   */  public static String getString(String key) {      return (String) redisTemplate.opsForValue().get(key);  }
 /**   * 判断key是否存在   * @param key   * @return   */  public static boolean isExist(String key) {      return redisTemplate.hasKey(key);  }
 /**   * 删除key   * @param key   * @return   */  public static boolean delKey(String key) {      return redisTemplate.delete(key);  }}
     
         •新建一个TokenUtil,用来生成和校验token:生成token没什么好说的,这里为了简单直接用uuid生成,然后放入redis中。校验token,如果用户没有携带token,直接返回false;如果携带了token,但是redis中没有这个token,说明已经被删除了,即已经访问了,返回false;如果redis中有,但是redis中的token和用户携带的token不一致,也返回false;有且一致,说明是第一次访问,就将redis中的token删除,然后返回true。      
public class TokenUtil {
private TokenUtil() {}
private static final String KEY = "token"; private static final String CODE = "code"; private static final String MSG = "msg"; private static final String JSON = "json"; private static final String RESULT = "result";
/**  * 生成token并放入redis中  * @return  */ public static String createToken() {     String token = UUID.randomUUID().toString();     RedisUtil.setString(KEY, token, 60L);     return RedisUtil.getString(KEY); }
/**  * 校验token  * @param request  * @return  * @throws JSONException  */ public static Map<String, Object> checkToken(HttpServletRequest request) throws JSONException {     String headerToken = request.getHeader(KEY);     JSONObject json = new JSONObject();     Map<String, Object> resultMap = new HashMap<>();     // 请求头中没有携带token,直接返回false     if (StringUtils.isEmpty(headerToken)) {         json.put(CODE, ResultEnum.HEADERNOTOKEN.getCode());         json.put(MSG, ResultEnum.HEADERNOTOKEN.getMsg());         resultMap.put(RESULT, false);         resultMap.put(JSON, json.toString());         return resultMap;     }
    if (StringUtils.isEmpty(RedisUtil.getString(KEY))) {         // 如果redis中没有token,说明已经访问成功过了,直接返回false         json.put(CODE, ResultEnum.REPEATREQUEST.getCode());         json.put(MSG, ResultEnum.REPEATREQUEST.getMsg());         resultMap.put(RESULT, false);         resultMap.put(JSON, json.toString());         return resultMap;     } else {         // 如果redis中有token,就删除掉,删除成功返回true,删除失败返回false         String redisToken = RedisUtil.getString(KEY);         boolean result = false;         if (!redisToken.equals(headerToken)) {             json.put(CODE, ResultEnum.ERRORTOKEN.getCode());             json.put(MSG, ResultEnum.ERRORTOKEN.getMsg());         } else {             result = RedisUtil.delKey(KEY);             String msg = result ? null : ResultEnum.OPERATEEXCEPTION.getMsg();             json.put(CODE, 400);             json.put(MSG, msg);         }         resultMap.put(RESULT, result);         resultMap.put(JSON, json.toString());         return resultMap;     } }}
     
         新建一个注解,用来标注需要进行幂等的接口:      
@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)public @interface NeedIdempotent {}
         接着要新建一个拦截器,对有@NeedIdempotent注解的方法进行拦截,进行自动幂等。      
public class IdempotentInterceptor implements HandlerInterceptor{
 @Override  public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse,Object object) throws JSONException {      // 拦截的不是方法,直接放行      if (!(object instanceof HandlerMethod)) {          return true;      }      HandlerMethod handlerMethod = (HandlerMethod) object;      Method method = handlerMethod.getMethod();      // 如果是方法,并且有@NeedIdempotent注解,就自动幂等      if (method.isAnnotationPresent(NeedIdempotent.class)) {          Map<String, Object> resultMap = TokenUtil.checkToken(httpServletRequest);          boolean result = (boolean) resultMap.get("result");          String json = (String) resultMap.get("json");          if (!result) {              JsonUtil.writeJsonToPage(httpServletResponse, json);          }          return result;      } else {          return true;      }  }
 @Override  public void postHandle(HttpServletRequest httpServletRequest,HttpServletResponse httpServletResponse, Object o,ModelAndView modelAndView) {  }
 @Override  public void afterCompletion(HttpServletRequest httpServletRequest,HttpServletResponse httpServletResponse,Object o, Exception e) {  }}     
          然后将这个拦截器配置到spring中去:        
@Configurationpublic class InterceptorConfig implements WebMvcConfigurer {
   @Override    public void addInterceptors(InterceptorRegistry registry)  {        registry.addInterceptor(idempotentInterceptor())                .addPathPatterns("/**");      }    @Bean    public IdempotentInterceptor idempotentInterceptor() {        return new IdempotentInterceptor();    }
}     

•最后新建一个controller,就可以愉快地进行测试了:

@RestController@RequestMapping("/idempotent")public class IdempotentApiController {
   @NeedIdempotent    @GetMapping("/hello")    public String hello() {        return "are you ok?";    }
   @GetMapping("/token")    public String token() {        return TokenUtil.createToken();    }}

访问/token,不需要什么校验,访问/hello,就会自动幂等,每一次访问都要先获取token,一个token不能用两次。

以上就是关于“怎么用token机制实现接口自动幂等”这篇文章的内容,相信大家都有了一定的了解,希望小编分享的内容对大家有帮助,若想了解更多相关的知识内容,请关注亿速云行业资讯频道。

向AI问一下细节

免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。

AI