提交 55dc95e4 编写于 作者: 无难事者若执's avatar 无难事者若执

doc : 【幂等】: 防重token令牌方案实现

- 使用默认内存token存储【后续实现redis】
- 使用请求依赖Const.REQUEST_TOKEN_VALUE_HOLDER 二维数组模拟请求参数传参数。【后续需要在web环境中,获取 请求Header中的token和服务中UserId】
上级 ba2bbfe5
......@@ -45,6 +45,10 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
</dependencies>
......@@ -61,12 +65,6 @@
<encoding>UTF-8</encoding>
</configuration>
</plugin>
<!--<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>2.3.11.RELEASE</version>
</plugin>-->
</plugins>
</build>
<repositories>
......
package com.kx;
/**
* 全局常量定义
* @author kongxiang
*/
public class Const {
/**
* 工具项目基准包
*/
public static final String BASE_PACKAGE = "com.kx.utils";
/**
* 【幂等】默认请求体实现下,采用该方式作为携带的 token和value 参数列表
* <pre>
* Const.REQUEST_TOKEN_VALUE_ARGS[0] = "token"
* Const.REQUEST_TOKEN_VALUE_ARGS[1] = "userid"
*
* // 调用含有注解@Idempotent的类或方法,自动去传入到幂等 token校验中。
* </pre>
*/
public static final String[] REQUEST_TOKEN_VALUE_ARGS = new String[2];
}
package com.kx.config;
import com.kx.Const;
import org.springframework.context.annotation.ComponentScan;
/**
* @author kongxiang
*/
@ComponentScan("com.kx.utils.id")
@ComponentScan(Const.BASE_PACKAGE + ".id")
public class IdAutoConfiguration {
}
package com.kx.config;
import com.kx.Const;
import com.kx.utils.idempotent.TokenCache;
import com.kx.utils.idempotent.TokenValueRequestHolder;
import com.kx.utils.idempotent.impl.DefaultTokenValueRequestHolder;
import com.kx.utils.idempotent.impl.InMemoryTokenCache;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.stereotype.Component;
/**
* 幂等工具自动配置类
* @author kongxiang
*/
@ComponentScan(Const.BASE_PACKAGE+".idempotent")
@Slf4j
public class IdempotentAutoConfiguration {
@Bean
@ConditionalOnMissingBean(TokenCache.class)
public InMemoryTokenCache inMemoryTokenCache(){
log.info("加载默认TokenCache: {}",InMemoryTokenCache.class);
return new InMemoryTokenCache();
}
@Bean
@ConditionalOnMissingBean(TokenValueRequestHolder.class)
public DefaultTokenValueRequestHolder defaultTokenValueRequestHolder(){
log.info("加载默认TTokenValueRequestHolder: {}",DefaultTokenValueRequestHolder.class);
return new DefaultTokenValueRequestHolder();
}
}
package com.kx.utils.idempotent;
/**
* 存放token缓存的地方
* @author kongxiang
*/
public interface TokenCache {
/**
* 服务器存入token
* @param token
* @param value
* @return
*/
public boolean putToken(String token,String value);
/**
* 验证token存在并删除成功,否则为false
* @param token
* @param value
* @return
*/
public boolean validToken(String token,String value);
}
package com.kx.utils.idempotent;
/**
* 请求中防重 token 和 value的获取方式
* @author kongxiang
*/
public interface TokenValueRequestHolder {
/**
* 获取 该次请求中携带的token信息
* @return 该次请求中携带的token信息
*/
String getToken();
/**
* 获取 验证辅助信息
* @return 验证辅助信息
*/
String getValidValue();
}
package com.kx.utils.idempotent.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 幂等注解
*
* @author kongxiang
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE,ElementType.METHOD})
public @interface Idempotent {
String value() default "";
/**
* 幂等实现方式
*/
String type() default "";
}
package com.kx.utils.idempotent.aop;
import com.kx.utils.idempotent.TokenValueRequestHolder;
import com.kx.utils.idempotent.annotation.Idempotent;
import com.kx.utils.idempotent.bean.IdempotentProperties;
import com.kx.utils.idempotent.impl.AntiDuplicateTokenHandler;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
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.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.lang.reflect.Method;
import java.util.function.DoubleToIntFunction;
@Component
@Aspect
@Slf4j
public class IdempotentAnnotationAspect {
@Autowired
private AntiDuplicateTokenHandler antiDuplicateTokenHandler;
@Autowired
private TokenValueRequestHolder tokenValueRequestHolder;
/**
* aop 切面
* \@annotation 所有注解的方法
* \@within 所有注解的类,类中的方法执行都会拦截
*/
@Pointcut("@annotation(com.kx.utils.idempotent.annotation.Idempotent) || @within(com.kx.utils.idempotent.annotation.Idempotent)")
public void condition() {
}
@Around(value = "condition()")
public Object round(ProceedingJoinPoint joinPoint ) throws Throwable {
// 获取执行的方法
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Method method = methodSignature.getMethod();
Object target = joinPoint.getTarget();
log.debug("joinPoint.getTarget().toString() : " + joinPoint.getTarget().toString());
log.debug("joinPoint.getTarget().toString() : " + joinPoint.getTarget().toString());
log.debug("methodSignature.getName() : " + methodSignature.getName());
log.debug("method.getName() : " + method.getName());
log.debug("method.getReturnType().getName() : " + method.getReturnType().getName());
Idempotent methodAnnotation = AnnotationUtils.findAnnotation(method, Idempotent.class);
Idempotent classAnnotation = AnnotationUtils.findAnnotation(target.getClass(), Idempotent.class);
log.debug("方法上注解存在 : " + (methodAnnotation != null));
log.debug("类上注解存在 : " + (classAnnotation != null));
IdempotentProperties idempotentProperties = getIdempotentPropertiesFromAnnotation(classAnnotation,methodAnnotation);
log.info("幂等配置: cacheKey : {}", idempotentProperties.getCacheKey());
String token = tokenValueRequestHolder.getToken();
String value = tokenValueRequestHolder.getValidValue();
boolean validStatus = antiDuplicateTokenHandler.validToken(idempotentProperties,token, value);
if (validStatus){
Object object = joinPoint.proceed();
return object;
}else {
throw new RuntimeException("操作已经处理,请稍后再试");
}
}
/**
* 从 {@link Idempotent} 注解中获取 幂等配置
* 优先级: 方法上的优先级高于类上的优先级,方法上配置的properties覆盖类上的。同属性覆盖而非 method 替换 class 所有的。
*
* @param classAnnotation 类上的Idempotent注解
* @param methodAnnotation 方法 上的Idempotent注解
* @return 获取的幂等配置对象
*
*/
private IdempotentProperties getIdempotentPropertiesFromAnnotation(Idempotent classAnnotation, Idempotent methodAnnotation) {
IdempotentProperties idempotentProperties = new IdempotentProperties();
// 先加载类注解信息
if (classAnnotation != null ){
idempotentProperties = getIdempotentPropertiesFromAnnotation(idempotentProperties,classAnnotation);
}
// 后加载方法上注解信息,如果有存在的配置,替换类上的
if (methodAnnotation != null){
idempotentProperties = getIdempotentPropertiesFromAnnotation(idempotentProperties,methodAnnotation);
}
return idempotentProperties;
}
/**
* 获取配置
* @param annotation 注解
* @return 配置
*/
private IdempotentProperties getIdempotentPropertiesFromAnnotation(IdempotentProperties idempotentProperties,Idempotent annotation ) {
if (annotation == null){
return new IdempotentProperties();
}
if (idempotentProperties == null){
idempotentProperties = new IdempotentProperties();
}
if (!StringUtils.isEmpty(annotation.value())){
idempotentProperties.setCacheKey(annotation.value());
}
return idempotentProperties;
}
}
package com.kx.utils.idempotent.bean;
import lombok.Data;
/**
* 幂等配置
* @author kongxiang
*/
@Data
public class IdempotentProperties {
private String cacheKey;
}
package com.kx.utils.idempotent.impl;
import com.kx.utils.id.IdService;
import com.kx.utils.idempotent.TokenCache;
import com.kx.utils.idempotent.bean.IdempotentProperties;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* <pre>接口幂等性方案: 防重token令牌实现幂等性
* </pre>
*
* 针对客户端连续点击或者调用方的超时重试等情况,例如提交订单,此种操作就可以用 Token 的机制实现防止重复提交。
* <p>
* 针对客户端连续点击或者调用方的超时重试等情况,例如提交订单,此种操作就可以用 Token 的机制实现防止重复提交。
* 简单的说就是调用方在调用接口的时候先向后端请求一个全局 ID(Token),请求的时候携带这个全局 ID 一起请求(Token 最好将其放到 Headers 中),后端需要对这个 Token 作为 Key,用户信息作为 Value 到 Redis 中进行键值内容校验,如果 Key 存在且 Value 匹配就执行删除命令,然后正常执行后面的业务逻辑。如果不存在对应的 Key 或 Value 不匹配就返回重复执行的错误信息,这样来保证幂等操作。
* <pre>
*
......@@ -22,7 +26,38 @@ import org.springframework.stereotype.Component;
@Component
public class AntiDuplicateTokenHandler {
@Autowired
private IdService idService;
@Autowired
private TokenCache tokenCache;
/**
* 生成token,放入到 服务器缓冲中【如redis】
*
* @param value value 存储在Redis中的辅助验证信息 如用户唯一id
* @return 服务器下发token
*/
public String getToken(String value) {
String token = idService.generate();
boolean isSuccess = tokenCache.putToken(token, value);
if (isSuccess){
return token;
}
return null;
}
/**
* 验证token
*
* @param idempotentProperties
* @param token 客户端传入的token
* @param value value 存储在Redis中的辅助验证信息 如用户唯一id
* @return 验证结果
*/
public boolean validToken(IdempotentProperties idempotentProperties, String token, String value){
return tokenCache.validToken(token, value);
}
}
package com.kx.utils.idempotent.impl;
import com.kx.Const;
import com.kx.utils.idempotent.TokenValueRequestHolder;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.stereotype.Component;
/**
* 该框架默认支持的一种解决方案
* @author kongxiang
*/
public class DefaultTokenValueRequestHolder implements TokenValueRequestHolder {
private String[] args = Const.REQUEST_TOKEN_VALUE_ARGS;
@Override
public String getToken() {
return args[0];
}
@Override
public String getValidValue() {
return args[1];
}
}
package com.kx.utils.idempotent.impl;
import com.kx.utils.idempotent.TokenCache;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
/**
* @author kongxiang
* 内存缓存
*/
public class InMemoryTokenCache implements TokenCache {
private Map<String,String> tokenStore = new HashMap<>();
@Override
public synchronized boolean putToken(String token, String value) {
tokenStore.put(token, value);
return true;
}
@Override
public synchronized boolean validToken(String token, String value) {
// 没有token,表示已经有请求消费了
if (!tokenStore.containsKey(token)){
return false;
}
String remove = tokenStore.remove(token);
if (Objects.equals(remove,value)){
return true;
}
return false;
}
}
......@@ -2,4 +2,5 @@
# Auto config
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.kx.config.ConfigPropertiesAutoConfiguration,\
com.kx.config.IdAutoConfiguration
\ No newline at end of file
com.kx.config.IdAutoConfiguration,\
com.kx.config.IdempotentAutoConfiguration
\ No newline at end of file
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册