幂等性: 接口重复调用(http延时机制) 及 表单重复提交
防止csrf 攻击 (模拟请求攻击)
本篇突出特点:AOP 拦截,自定义注解思想
自定义注解AOP 拦截思想
1、拦截通知,处理重复调用
2、rpc 接口安全加密可以统一拦截验证加密串是否有效
缺点:
1、rpc 验证太麻烦,api 接口需先掉生成token 接口
2、因为是 rpc 验证,不适应于前后端分离项目
3、表单验证太繁琐,可以使用session保存 / 可以看我的另外一篇文章接口幂等性处理(表单重复提交–简单处理)https://blog.csdn.net/qq_41463655/article/details/100176081
项目demo:
链接:https://pan.baidu.com/s/1f6RvqX5mnGDKj4Zp9wcdNw
提取码:nc6k
效果展示:
使用memcached缓存,详情看下方地址,推荐使用redis, 也可以使用 Map集合直接缓存到Jvm
https://www.cnblogs.com/wujuntian/p/4791220.html
org.springframework.boot
spring-boot-starter-web
org.springframework
spring-aop
org.springframework
spring-aspects
org.aspectj
aspectjrt
org.aspectj
aspectjweaver
cglib
cglib
2.1_3
org.springframework.boot
spring-boot-starter-thymeleaf
org.springframework.boot
spring-boot-devtools
true
org.springframework.boot
spring-boot-starter
org.springframework.boot
spring-boot-starter-test
test
net.spy
spymemcached
2.12.2
配置文件
################################################
### memcache缓存
################################################
memcache.ip=127.0.0.1
memcache.port=11211
package com.example.demo.config;
import net.spy.memcached.MemcachedClient;
import net.spy.memcached.compat.log.Logger;
import net.spy.memcached.compat.log.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.net.InetSocketAddress;
/**
* Memcached缓存配置 MemcachedClient.getClient(),获取缓存
* @author wangsong
* @date 2019年4月27日 下午5:17:22
*/
@Component
public class MemcachedRunner implements CommandLineRunner {
protected Logger logger = LoggerFactory.getLogger(this.getClass());
private MemcachedClient client = null;
@Value("${memcache.ip}")
private String ip;
@Value("${memcache.port}")
private int port;
@Override
public void run(String... args) throws Exception {
try {
client = new MemcachedClient(new InetSocketAddress(ip,port));
} catch (IOException e) {
logger.error("inint MemcachedClient failed ",e);
}
}
/**
* 获取 MemcachedClient 连接
* @return
*/
public MemcachedClient getClient() {
return client;
}
}
package com.example.demo.config;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 自定义注解(token验证注解),需要验证的接口添加
* @author wangsong
* @date: 2019年4月27日 下午9:37:02
*/
@Target(value = ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ExtApiIdempotent {
String value();
}
package com.example.demo.config;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 生成token 的注解
* @author wangsong
* @date: 2019年4月27日 下午10:09:44
*/
@Target(value = ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ExtApiToken {
}
package com.example.demo.utlis;
import com.example.demo.config.MemcachedRunner;
import net.spy.memcached.MemcachedClient;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import javax.annotation.Resource;
/**
* Token 生成与判断
* @author wangsong
* @date: 2019年4月27日 下午10:04:39
*/
@Service
public class MemcachedTokenUtils {
/**
* toket有效期
*/
private Integer TOKET_TIMEOUT = 60 * 60;
/**
* Memcached对象
*/
@Resource
private MemcachedRunner memcachedRunner;
/**
* 将token存入在Memcached || redis
* @return
*/
public String getToken() {
//token = token+时间戳
String token = "token" + System.currentTimeMillis();
MemcachedClient memcachedClient = memcachedRunner.getClient();
memcachedClient.set(token,TOKET_TIMEOUT, token);
return token;
}
/**
* 判断toket是否有效 / 是否为重复调用
* @param tokenKey
* @return
*/
public boolean findToken(String tokenKey) {
MemcachedClient memcachedClient = memcachedRunner.getClient();
String token = (String) memcachedClient.get(tokenKey);
//判断token是否为空
if (StringUtils.isEmpty(token)) {
return false;
}
// token 获取成功后 删除对应tokenMapstoken
memcachedClient.delete(tokenKey);
return true;
}
}
package com.example.demo.config;
import com.example.demo.utlis.MemcachedTokenUtils;
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.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
/**
* spring 通知验证 || 防止接口重复掉用|| 表单重复提交
*/
@Aspect
@Component
public class ExtApiAopIdempotent {
@Autowired
private MemcachedTokenUtils memcachedTokenUtils;
/**
* 拦截范围
*/
@Pointcut("execution(public * com.example.demo.controller.*.*(..))")
public void rlAop() {
}
/**
* 前置通知
* 转发Token参数()
*
* @param point
*/
@Before("rlAop()")
public void before(JoinPoint point) {
MethodSignature signature = (MethodSignature) point.getSignature();
//查询接口是否携带 @ExtApiToken 注解
ExtApiToken extApiToken = signature.getMethod().getDeclaredAnnotation(ExtApiToken.class);
//携带注解 生成toket 放到请求头
if (extApiToken != null) {
extApiToken();
}
}
/**
* 获取token(生成toket 放到请求头)
*/
public void extApiToken() {
String token = memcachedTokenUtils.getToken();
getRequest().setAttribute("token", token);
}
/**
* 环绕通知验证参数
*
* @param proceedingJoinPoint
* @return
* @throws Throwable
*/
@Around("rlAop()")
public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
MethodSignature signature = (MethodSignature) proceedingJoinPoint.getSignature();
//查询接口是否携带 @ExtApiIdempotent 注解
ExtApiIdempotent extApiIdempotent = signature.getMethod().getDeclaredAnnotation(ExtApiIdempotent.class);
//携带注解验证toket,是否为重复请求
Object proceed;
if (extApiIdempotent != null) {
//验证Token
proceed = extApiIdempotent(proceedingJoinPoint, signature);
return proceed;
}
// 未携带直接放行
proceed = proceedingJoinPoint.proceed();
return proceed;
}
/**
* 验证Token
*
* @param proceedingJoinPoint
* @param signature
* @return
* @throws Throwable
*/
private Object extApiIdempotent(ProceedingJoinPoint proceedingJoinPoint, MethodSignature signature)
throws Throwable {
//获取注解
ExtApiIdempotent extApiIdempotent = signature.getMethod().getDeclaredAnnotation(ExtApiIdempotent.class);
// 1.获取令牌,存放在请求头中|valueType = 请求类型,rpc接口/表单
HttpServletRequest request = getRequest();
String valueType = extApiIdempotent.value();
//判断某字符串是否为空,为空的标准是 str==null或 str.length()==0
if (StringUtils.isEmpty(valueType)) {
response("参数错误!");
return null;
}
//from(表单) 或者 head(请求头)
String token = null;
if (valueType.equals("head")) {
token = request.getHeader("token");
} else {
token = request.getParameter("token");
}
//判断某字符串是否为空,为空的标准是 str==null或 str.length()==0
if (StringUtils.isEmpty(token)) {
response("参数错误!");
return null;
}
//查看缓存是否存在token|| 不存在重复提交
if (!memcachedTokenUtils.findToken(token)) {
response("请勿重复提交!");
return null;
}
//删除缓存token,防止重复提交
Object proceed = proceedingJoinPoint.proceed();
return proceed;
}
/**
* 获取 request
*
* @return
*/
private HttpServletRequest getRequest() {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
return request;
}
/**
* 获取 response
*
* @param msg
* @throws IOException
*/
private void response(String msg) throws IOException {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletResponse response = attributes.getResponse();
response.setHeader("Content-type", "text/html;charset=UTF-8");
PrintWriter writer = response.getWriter();
try {
writer.println(msg);
} catch (Exception e) {
} finally {
writer.close();
}
}
}
package com.example.demo.controller;
import com.example.demo.config.ExtApiIdempotent;
import com.example.demo.config.ExtApiToken;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.servlet.http.HttpServletRequest;
@Controller
public class TokenController {
/**
* 跳转表单页
* 自定义注解 @ExtApiToken 生成token
*
* @param req
* @return
*/
@RequestMapping("/test")
@ExtApiToken
public String indexPage(HttpServletRequest req) {
return "test";
}
/**
* 提交表单
* 自定义注解验证是否重复提交 @ExtApiIdempotent(value = "form") form||head
*
* @param request
* @return
*/
@RequestMapping(value = "/tokenTest", produces = "application/json; charset=utf-8")
@ExtApiIdempotent(value = "form")
@ResponseBody
public String addOrderExtApiIdempotent(HttpServletRequest request) {
System.out.println("正常执行");
return "正常执行逻辑代码";
}
}
Insert title here