Files
youlai-boot/src/main/java/com/youlai/boot/common/aspect/RepeatSubmitAspect.java

103 lines
3.7 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package com.youlai.boot.common.aspect;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.digest.DigestUtil;
import com.youlai.boot.common.constant.RedisConstants;
import com.youlai.boot.common.constant.SecurityConstants;
import com.youlai.boot.common.result.ResultCode;
import com.youlai.boot.common.exception.BusinessException;
import com.youlai.boot.common.annotation.RepeatSubmit;
import com.youlai.boot.common.util.IPUtils;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.http.HttpHeaders;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import java.util.concurrent.TimeUnit;
/**
* 防重复提交切面
*
* @author Ray.Hao
* @since 2.3.0
*/
@Aspect
@Component
@RequiredArgsConstructor
@Slf4j
public class RepeatSubmitAspect {
private final RedissonClient redissonClient;
/**
* 防重复提交切点
*/
@Pointcut("@annotation(repeatSubmit)")
public void repeatSubmitPointCut(RepeatSubmit repeatSubmit) {
}
/**
* 环绕通知:处理防重复提交逻辑
*/
@Around(value = "repeatSubmitPointCut(repeatSubmit)", argNames = "pjp,repeatSubmit")
public Object handleRepeatSubmit(ProceedingJoinPoint pjp, RepeatSubmit repeatSubmit) throws Throwable {
String lockKey = buildLockKey();
int expire = repeatSubmit.expire();
RLock lock = redissonClient.getLock(lockKey);
boolean locked = lock.tryLock(0, expire, TimeUnit.SECONDS);
if (!locked) {
throw new BusinessException(ResultCode.DUPLICATE_SUBMISSION);
}
return pjp.proceed();
}
/**
* 生成防重复提交锁的 key
* @return 锁的 key
*/
private String buildLockKey() {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
// 用户唯一标识
String userIdentifier = getUserIdentifier(request);
// 请求唯一标识 = 请求方法 + 请求路径 + 请求参数(严谨的做法)
String requestIdentifier = StrUtil.join(":", request.getMethod(), request.getRequestURI());
return StrUtil.format(RedisConstants.Lock.RESUBMIT, userIdentifier, requestIdentifier);
}
/**
* 获取用户唯一标识
* 1. 从请求头中获取 Token使用 SHA-256 加密 Token 作为用户唯一标识
* 2. 如果 Token 为空,使用 IP 作为用户唯一标识
*
* @param request 请求对象
* @return 用户唯一标识
*/
private String getUserIdentifier(HttpServletRequest request) {
// 用户身份唯一标识
String userIdentifier;
// 从请求头中获取 Token
String tokenHeader = request.getHeader(HttpHeaders.AUTHORIZATION);
if (StrUtil.isNotBlank(tokenHeader) && tokenHeader.startsWith(SecurityConstants.BEARER_TOKEN_PREFIX)) {
String rawToken = tokenHeader.substring(SecurityConstants.BEARER_TOKEN_PREFIX.length()); // 去掉 Bearer 后的 Token
userIdentifier = DigestUtil.sha256Hex(rawToken); // 使用 SHA-256 加密 Token 作为用户唯一标识
} else {
userIdentifier = IPUtils.getIpAddr(request); // 使用 IP 作为用户唯一标识
}
return userIdentifier;
}
}