【安全】Java幂等性校验解决重复点击(6种实现方式)_幂等校验(2)

还有兄弟不知道网络安全面试可以提前刷题吗?费时一周整理的160+网络安全面试题,金九银十,做网络安全面试里的显眼包!

王岚嵚工程师面试题(附答案),只能帮兄弟们到这儿了!如果你能答对70%,找一个安全工作,问题不大。

对于有1-3年工作经验,想要跳槽的朋友来说,也是很好的温习资料!

【完整版领取方式在文末!!】

93道网络安全面试题

内容实在太多,不一一截图了

黑客学习资源推荐

最后给大家分享一份全套的网络安全学习资料,给那些想学习 网络安全的小伙伴们一点帮助!

对于从来没有接触过网络安全的同学,我们帮你准备了详细的学习成长路线图。可以说是最科学最系统的学习路线,大家跟着这个大的方向学习准没问题。

1️⃣零基础入门
① 学习路线

对于从来没有接触过网络安全的同学,我们帮你准备了详细的学习成长路线图。可以说是最科学最系统的学习路线,大家跟着这个大的方向学习准没问题。

image

② 路线对应学习视频

同时每个成长路线对应的板块都有配套的视频提供:

image-20231025112050764

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化资料的朋友,可以点击这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!


然后使用 version=1 和 订单ID 一起作为条件,再去更新:



update order set version = version +1,status=‘P’ where order_id=‘666’ and version =1


最后,更新成功才可以处理业务逻辑,如果更新失败,默认为重复请求,直接返回。


**流程图如下:**


![](https://img-blog.csdnimg.cn/a58d42ddc8504236a5e35449339778de.png)
**为什么版本号建议自增呢?**



> 
> 因为乐观锁存在 ABA 的问题,如果 version 版本一直是自增的就不会出现 ABA 的情况。
> 
> 
> 


#### 3.3 数据库层面,悲观锁(select for update)【不推荐】


`悲观锁`:通俗点讲就是很悲观,每次去操作数据时,都觉得别人中途会修改,所以每次在拿数据的时候都会上锁。官方点讲就是,共享资源每次只给一个线程使用,其他线程阻塞,用完后再把资源转让给其它资源。


**悲观锁的实现:**



> 
> 在订单业务场景中,假设先查询出订单,如果查到的是处理中状态,就处理完业务,然后再更新订单状态为完成。如果查到订单,并且不是处理中的状态,则直接返回。
> 
> 
> 


可以使用数据库悲观锁(select … for update)解决这个问题:



begin; # 1.开始事务
select * from order where order_id=‘666’ for update # 查询订单,判断状态,锁住这条记录
if(status !=处理中){
//非处理中状态,直接返回;
return ;
}

处理业务逻辑

update order set status=‘完成’ where order_id=‘666’ # 更新完成
commit; # 5.提交事务


注意:


* 这里的 order\_id 需要是主键或索引,只用行级锁锁住这条数据即可,如果不是主键或索引,会锁住整张表。
* 悲观锁在同一事务操作过程中,锁住了一行数据。这样 **别的请求过来只能等待**,如果当前事务耗时比较长,就很影响接口性能。所以一般 **不建议用悲观锁的实现方式**。


#### 3.4 数据库层面,状态机


很多业务表,都是由状态的,比如:转账流水表,就会有 0-待处理,1-处理中,2-成功,3-失败的状态。转账流水更新的时候,都会涉及流水状态更新,即涉及 **状态机(即状态变更图)**。我们可以利用状态机来实现幂等性校验。


**状态机的实现:**


比如:转账成功后,把 **处理中** 的转账流水更新为成功的状态,SQL 如下:



update transfor_flow set status = 2 where biz_seq=‘666’ and status = 1;


**流程图如下:**


![在这里插入图片描述](https://img-blog.csdnimg.cn/823a1b5397ed4cb1a7825e2f5c622dc3.png)


* 第1次请求来时,bizSeq 流水号是 666,该流水的状态是处理中,值是 1,要更新为 2-成功的状态,所以该 update 语句可以正常更新数据,sql 执行结果的影响行数是 1,流水状态最后变成了 2。
* 第2次请求也过来了,如果它的流水号还是 666,因为该流水状态已经变为 2-成功的状态,所以更新结果是0,不会再处理业务逻辑,接口直接返回。


**伪代码实现如下:**



Rsp idempotentTransfer(Request req){
String bizSeq = req.getBizSeq();
int rows= “update transfr_flow set status=2 where biz_seq=#{bizSeq} and status=1;”
if(rows==1){
log.info(“更新成功,可以处理该请求”);
//其他业务逻辑处理
return rsp;
} else if(rows == 0) {
log.info(“更新不成功,不处理该请求”);
//不处理,直接返回
return rsp;
}

log.warn("数据异常")
return rsp:

}


#### 3.5 应用层面,token令牌【不推荐】


token 唯一令牌方案一般包括两个请求阶段:


1. 客户端请求申请获取请求接口用的token,服务端生成token返回;
2. 客户端带着token请求,服务端校验token。


**流程图如下:**


![在这里插入图片描述](https://img-blog.csdnimg.cn/8773e8f5d39e420091d270f44dd13bcf.png)


1. 客户端发送请求,申请获取 token。
2. 服务端生成全局唯一的 token,保存到 redis 中(一般会设置一个过期时间),然后返回给客户端。
3. 客户端带着 token,发起请求。
4. 服务端去 redis 确认 token 是否存在,一般用 `redis.del(token)` 的方式,如果存在会删除成功,即处理业务逻辑,如果删除失败,则直接返回结果。



> 
> **补充:** 这种方式个人不推荐,说两方面原因:
> 
> 
> 1. 需要前后端联调才能实现,存在沟通成本,最终效果可能与设想不一致。
> 2. 如果前端多次获取多个 token,还是可以重复请求的,如果再在获取 token 处加分布式锁控制,就不如直接用分布式锁来控制幂等性了,即下面这种解决方式。
> 
> 
> 


#### 3.6 应用层面,分布式锁【推荐】


`分布式锁` 实现幂等性的逻辑就是,请求过来时,先去尝试获取分布式锁,如果获取成功,就执行业务逻辑,反之获取失败的话,就舍弃请求直接返回成功。


**流程图如下:**


![](https://img-blog.csdnimg.cn/8905e1d813394a9c8fe1c9e59fb72be6.png)
* 分布式锁可以使用 Redis,也可以使用 Zookeeper,不过 Redis 相对好点,比较轻量级。
* Redis 分布式锁,可以使用 `setIfAbsent()` 来实现,注意分布式锁的 key 必须为业务的唯一标识。
* Redis 执行设置 key 的动作时,要设置过期时间,防止释放锁失败。这个过期时间不能太短,太短拦截不了重复请求,也不能设置太长,请求量多的话会占用存储空间。




---


### 四、Java 代码实现


#### 4.1 @NotRepeat 注解


@NotRepeat 注解用于修饰需要进行幂等性校验的类。


**NotRepeat.java**



import java.lang.annotation.*;

/**
* 幂等性校验注解
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface NotRepeat {

}


#### 4.2 AOP 切面


AOP切面监控被 @Idempotent 注解修饰的方法调用,实现幂等性校验逻辑。


**IdempotentAOP.java**



import com.demo.util.RedisUtils;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.util.concurrent.TimeUnit;

/**
* 重复点击校验
*/
@Slf4j
@Aspect
@Component
public class IdempotentAOP {

/\*\* Redis前缀 \*/
private String API\_IDEMPOTENT\_CHECK = "API\_IDEMPOTENT\_CHECK:";

@Resource
private HttpServletRequest request;
@Resource
private RedisUtils redisUtils;

/\*\*

* 定义切面
*/
@Pointcut(“@annotation(com.demo.annotation.NotRepeat)”)
public void notRepeat() {
}

/\*\*

* 在接口原有的方法执行前,将会首先执行此处的代码
*/
@Before(“notRepeat()”)
public void doBefore(JoinPoint joinPoint) {
String uri = request.getRequestURI();

    // 登录后才做校验
    UserInfo loginUser = AuthUtil.getLoginUser();
    if (loginUser != null) {
        assert uri != null;
        String key = loginUser.getAccount() + "\_" + uri;
        log.info(">>>>>>>>>> 【IDEMPOTENT】开始幂等性校验,加锁,account: {},uri: {}", loginUser.getAccount(), uri);

        // 加分布式锁
        boolean lockSuccess = redisUtils.setIfAbsent(API\_IDEMPOTENT\_CHECK + key, "1", 30, TimeUnit.MINUTES);
        log.info(">>>>>>>>>> 【IDEMPOTENT】分布式锁是否加锁成功:{}", lockSuccess);
        if (!lockSuccess) {
            if (uri.contains("contract/saveDraftContract")) {
                log.error(">>>>>>>>>> 【IDEMPOTENT】文件保存中,请稍后");
                throw new IllegalArgumentException("文件保存中,请稍后");

            } else if (uri.contains("contract/saveContract")) {
                log.error(">>>>>>>>>> 【IDEMPOTENT】文件发起中,请稍后");
                throw new IllegalArgumentException("文件发起中,请稍后");
            }
        }
    }
}

/\*\*

* 在接口原有的方法执行后,都会执行此处的代码(final)
*/
@After(“notRepeat()”)
public void doAfter(JoinPoint joinPoint) {
// 释放锁
String uri = request.getRequestURI();
assert uri != null;
UserInfo loginUser = SysUserUtil.getloginUser();
if (loginUser != null) {
String key = loginUser.getAccount() + “_” + uri;
log.info(“>>>>>>>>>> 【IDEMPOTENT】幂等性校验结束,释放锁,account: {},uri: {}”, loginUser.getAccount(), uri);
redisUtils.del(API_IDEMPOTENT_CHECK + key);
}
}
}


#### 4.3 RedisUtils 工具类


**RedisUtils.java**



import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

import java.util.Arrays;
import java.util.concurrent.TimeUnit;

/**
* redis工具类
*/
@Slf4j
@Component
public class RedisUtils {

/\*\*

* 默认RedisObjectSerializer序列化
*/
@Autowired
private RedisTemplate<String, Object> redisTemplate;

/\*\*

* 加分布式锁
*/
public boolean setIfAbsent(String key, String value, long timeout, TimeUnit unit) {
return redisTemplate.opsForValue().setIfAbsent(key, value, timeout, unit);
}

/\*\*

* 释放锁
*/
public void del(String… keys) {
if (keys != null && keys.length > 0) {
//将参数key转为集合
redisTemplate.delete(Arrays.asList(keys));
}
}
}


#### 4.4 测试类


**OrderController.java**



import com.demo.annotation.NotRepeat;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Arrays;
import java.util.List;

/**
* 幂等性校验测试类
*/
@RequestMapping(“/order”)
@RestController
public class OrderController {

@NotRepeat
@GetMapping("/orderList")
public List<String> orderList() {
    // 查询列表

先自我介绍一下,小编浙江大学毕业,去过华为、字节跳动等大厂,目前阿里P7

深知大多数程序员,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年最新网络安全全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友。
img
img
img
img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上网络安全知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

需要这份系统化资料的朋友,可以点击这里获取

iYmbU2J-1715566530776)]
[外链图片转存中…(img-DnjkVDTU-1715566530778)]
[外链图片转存中…(img-I6yl60lp-1715566530778)]
[外链图片转存中…(img-w2s8lCqV-1715566530779)]
[外链图片转存中…(img-on5fJTOb-1715566530780)]
[外链图片转存中…(img-cD953W20-1715566530781)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上网络安全知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

需要这份系统化资料的朋友,可以点击这里获取

  • 12
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
在Spring Boot中,可以使用以下几种方式来进行校验: 1. Token或Nonce机制:在每次请求中包含一个唯一的标识符,例如使用Token或Nonce。服务端在接收到请求后,校验该标识符是否已经被使用过,如果已经使用过,则拒绝处理该请求。 2. 请求参数校验:在每次请求中包含一个用于校验的请求参数,例如使用一个特定的参数名。服务端在接收到请求后,校验该参数的值是否已经被使用过,如果已经使用过,则拒绝处理该请求。 3. 通过唯一标识符存储状态:在服务端通过唯一标识符将请求的处理状态进行存储,例如使用数据库或缓存。在每次请求到达时,服务端先检查该标识符对应的状态,如果已经处理过,则拒绝处理该请求。 4. 使用分布式锁:在每次请求到达时,使用分布式锁来保证同一时刻只有一个请求能够被处理。可以使用Redis等分布式锁工具实现这个功能。 需要根据具体的业务场景和需求选择适合的校验方式。同时,为了保证校验的有效,需要注意以下几点: - 标识符的唯一:确保每个标识符的唯一,避免重复使用导致校验失效。 - 存储状态的有效期:设置合适的存储状态的有效期,确保及时释放已处理的请求标识符。 - 锁的适用:如果使用分布式锁来实现校验,需要考虑锁的粒度和能开销,避免影响系统能。 在具体实现上,你可以在Spring Boot的控制器(Controller)中进行校验,并根据校验结果来决定是否继续处理请求。例如,可以在控制器方法中使用自定义注解或AOP方式来统一处理校验逻辑。 ```java @RestController public class MyController { @PostMapping("/myEndpoint") @Idempotent // 自定义注解用于标识校验 public ResponseEntity<String> myEndpoint() { // 处理请求逻辑 return ResponseEntity.ok("Success"); } } ``` 上述示例中的`@Idempotent`注解是一个自定义注解,用于标识需要进行校验的请求。在注解的处理逻辑中,可以选择合适的方式进行校验
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值