Spring Boot+Redis+Interceptor拦截器 / AOP+自定义注解实现接口幂等
Spring Boot+Redis+Interceptor拦截器 / AOP+自定义注解实现接口幂等
前几天在写代码,碰到了问题,就是一个获取用户详细信息接口,如果用户通过修改参数,那么就可以获取到其他用户的信息,甚至是管理员的信息。则发生了垂直越权。然后今天看到了 Spring Boot+Redis+Interceptor+自定义Annotation实现接口自动幂等 这篇文章,参考着写了一下。便想了想可以通过 拦截器+接口幂等性校验token来防止用户篡改参数,重复请求。
幂等性的概念
幂等性:通俗的说就是一个接口, 多次发起同一个请求, 必须保证操作只能执行一次。
应用场景:
1)订单接口, 不能多次创建订单;
2)支付接口, 重复支付同一笔订单只能扣一次钱;
3)支付宝回调接口, 可能会多次回调, 必须处理重复回调;
4)普通表单提交接口, 因为网络超时等原因多次点击提交, 只能成功一次等。
解决方案
1)唯一索引 – 防止新增脏数据
2)token机制 – 防止页面重复提交
3)悲观锁 – 获取数据的时候加锁(锁表或锁行)
4)乐观锁 – 基于版本号version实现, 在更新数据那一刻校验数据
5)分布式锁 – redis(jedis、redisson)或zookeeper实现
6)状态机 – 状态变更, 更新数据时判断状态
利用拦截器的代码实现
- pom.xml
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.5.2</version>
</dependency>
<dependency>
<groupId>com.hx</groupId>
<artifactId>hx-core</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.16</version><!--版本号自己选一个就行-->
<scope>provided</scope>
</dependency>
</dependencies>
- RedisUtil
package com.hx.tokendemo.utils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Component;
import java.io.Serializable;
import java.util.concurrent.TimeUnit;
/**
* @Author: Huathy
* @ClassPath: com.hx.tokendemo.service.RedisService
* @Date: 2021-03-16 10:00
* @Description:
*/
@Component
public class RedisUtil {
private RedisTemplate redisTemplate;
ValueOperations<Serializable,Object> operations;
@Autowired
public RedisUtil(RedisTemplate redisTemplate){
this.redisTemplate = redisTemplate;
if(this.redisTemplate != null){
operations = this.redisTemplate.opsForValue();
}
}
/**
* set
* @param key
* @param val
* @return
*/
public boolean set(final String key,Object val){
boolean result = false;
try {
operations.set(key,val);
result = true;
} catch (Exception e) {
e.printStackTrace();
}
return result;
}
/**
* 超时set
* @param key
* @param val
* @param timeout
* @return
*/
public boolean setEx(final String key, Object val, long timeout){
boolean result = false;
try {
operations.set(key,val);
redisTemplate.expire(key,timeout, TimeUnit.SECONDS);
result = true;
} catch (Exception e) {
e.printStackTrace();
}
return result;
}
/**
* 判断键是否存在
* @param key
* @return
*/
public boolean exist(final String key){
return redisTemplate.hasKey(key);
}
/**
* 取值
* @param key
* @return
*/
public Object get(final String key){
Object obj = operations.get(key);
return obj;
}
/**
* 删除键
* @param key
* @return
*/
public boolean remove(final String key){
if(this.exist(key)){
redisTemplate.delete(key);
return true;
}
return false;
}
}
- 自定义注解@AutoIdempotent
package com.hx.tokendemo.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 定义此注解的主要目的是把它添加在需要实现幂等的方法上,凡是某个方法注解了它,都会实现自动幂等
* 后台利用反射如果扫描到这个注解,就会处理这个方法实现自动幂等,
* @author Huathy
*/
@Target(ElementType.METHOD) //表示该注解作用域方法上
@Retention(RetentionPolicy.RUNTIME) //表示该注解的保留策略为运行时
public @interface AutoIdempotent {
}
- AutoIdempotentInterceptor拦截器
package com.hx.tokendemo.Interceptor;
import cn.hutool.json.JSONUtil;
import com.hx.tokendemo.annotation.AutoIdempotent;
import com.hx.tokendemo.service.TokenService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.lang.reflect.Method;
/**
* @Author: Huathy
* @ClassPath: com.hx.tokendemo.Interceptor.AutoIdempotentInterceptor
* @Date: 2021-03-16 10:57
* @Description:
*/
@Component
@Slf4j
public class AutoIdempotentInterceptor implements HandlerInterceptor {
@Autowired
private TokenService tokenService;
/**
* 拦截器
* @param request
* @param response
* @param handler
* @return
* @throws Exception
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//关于instanceof:是Java中的一个双目运算符,用来测试一个对象是否是一个类的实例
if(!(handler instanceof HandlerMethod)){
return true;
}
HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod();
AutoIdempotent methodAnnotation = method.getAnnotation(AutoIdempotent.class);
if(methodAnnotation != null){
try {
//进行幂等校验,校验通过则放行,校验失败则抛出异常,并通过统一的异常处理返回友好提示
return tokenService.checkToken(request);
} catch (Exception e) {
log.error("token校验失败==》",e);
writeReturnJson(response, JSONUtil.toJsonStr(e));
throw e;
}
}
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
}
/**
* 返回的json值
* @param response
* @param json
* @throws Exception
*/
private void writeReturnJson(HttpServletResponse response, String json) throws Exception{
PrintWriter writer = null;
response.setCharacterEncoding("UTF-8");
response.setContentType("text/html; charset=utf-8");
try {
writer = response.getWriter();
writer.print(json);
} catch (IOException e) {
} finally {
if (writer != null) {
writer.close();
}
}
}
}
- 拦截器配置
package com.hx.tokendemo.config;
import com.hx.tokendemo.Interceptor.AutoIdempotentInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import javax.annotation.Resource;
/**
* @Author: Huathy
* @ClassPath: com.hx.tokendemo.config.WebConfig
* @Date: 2021-03-16 10:47
* @Description:
*/
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Resource
private AutoIdempotentInterceptor autoIdempotentInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(autoIdempotentInterceptor);
}
}
- TokenService
package com.hx.tokendemo.service;
import cn.hutool.core.lang.UUID;
import cn.hutool.core.text.StrBuilder;
import cn.hutool.core.util.StrUtil;
import com.hx.core.api.ApiError;
import com.hx.core.exception.ServiceException;
import com.hx.tokendemo.constant.Constant;
import com.hx.tokendemo.utils.RedisUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.servlet.http.HttpServletRequest;
/**
* @Author: Huathy
* @ClassPath: com.hx.tokendemo.service.Service
* @Date: 2021-03-16 10:18
* @Description:
*/
@Service
public class TokenService {
@Autowired
private RedisUtil redisService;
/**
* Create Token Method
* @return
*/
public String createToken(){
String str = UUID.randomUUID().toString();
StrBuilder token = new StrBuilder();
try {
token.append(Constant.TOKEN_PREFIX.getName()).append(str);
redisService.setEx(token.toString(),token.toString(),1000L);
boolean notEmpty = StrUtil.isNotEmpty(token.toString());
if(notEmpty){
return token.toString();
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* Check Token Method
* @return
*/
public boolean checkToken(HttpServletRequest request){
String token = request.getHeader(Constant.TOKEN_NAME.getName());
if(StrUtil.isBlank(token)){
token = request.getParameter(Constant.TOKEN_NAME.getName());
if(StrUtil.isBlank(token)){
throw new ServiceException(ApiError.ERROR_10010001.getCode(),"没有token参数");
}
}
if(!redisService.exist(token)){
throw new ServiceException(ApiError.ERROR_10010002.getCode(),"服务器token已经失效");
}
boolean remove = redisService.remove(token);
if (!remove){
throw new ServiceException(ApiError.ERROR_60000001.getCode(),"清除token失败");
}
return true;
}
}
- 测试类BusinessController
package com.hx.tokendemo.test;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.hx.core.api.ApiRest;
import com.hx.core.api.controller.BaseController;
import com.hx.core.utils.IpUtils;
import com.hx.tokendemo.annotation.AutoIdempotent;
import com.hx.tokendemo.service.TokenService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
import java.util.Enumeration;
/**
* @Author: Huathy
* @ClassPath: com.hx.tokendemo.test.BusinessController
* @Date: 2021-03-16 12:04
* @Description:
*/
@RestController
@Slf4j
//@RequestMapping("/business")
public class BusinessController extends BaseController {
@Autowired
private TokenService tokenService;
@Autowired
private HttpServletRequest request;
@RequestMapping("/get/token")
public String getToken(){
String token = tokenService.createToken();
if(StrUtil.isNotEmpty(token)){
ApiRest<String> success = this.success(token);
return JSONUtil.toJsonStr(success);
}
return StrUtil.EMPTY;
}
@AutoIdempotent
@PostMapping("/test/Idempotence")
public String testIdempotence() {
Enumeration<String> headerNames = request.getHeaderNames();
String clientIp = IpUtils.extractClientIp(request);
log.info("收到客户端:" + clientIp + "请求testIdempotence");
while (headerNames.hasMoreElements()){
String headerName = headerNames.nextElement();
String header = request.getHeader(headerName);
System.out.println(headerName + "==" + header);
}
return "SUCCESS";
}
}
利用AOP的代码实现
- pom.xml
<!-- AOP -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- aspectjrt和aspectjweaver是与aspectj相关的包,用来支持切面编程的; -->
<!-- aspectjrt包是aspectj的runtime包;-->
<!-- aspectjweaver是aspectj的织入包;-->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>1.8.0</version>
</dependency>
<!-- cglib包是用来动态代理用的,基于类的代理; -->
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>2.1</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>5.3.4</version>
</dependency>
- ContrllerAspect.java
package com.hx.tokendemo.aspect;
import cn.hutool.json.JSONUtil;
import com.hx.core.exception.ServiceException;
import com.hx.tokendemo.service.TokenService;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
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;
/**
* @Author: Huathy
* @ClassPath: com.hx.tokendemo.aspect.ControllerAspect
* @Date: 2021-03-16 16:28
* @Description:
*/
@Aspect
@Component
@Slf4j
public class ControllerAspect {
@Autowired
private TokenService tokenService;
/**
* 定义切点
* 要求匹配:在controller包下的所有方法
* 并持有@AutoIdempotent注解
*/
@Pointcut("@annotation(com.hx.tokendemo.annotation.AutoIdempotent))" +
"&&execution(public * com.hx.tokendemo.controller..*.*(..))")
private void checkToken(){
}
//引入切点
@Before("checkToken()")
public void doBefore(JoinPoint joinPoint) throws Exception {
log.info("===== AOP校验token请求 =====");
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
HttpServletResponse response = attributes.getResponse();
String token = request.getHeader("token");
log.info("token===>" + token);
try {
//进行幂等校验,校验通过则放行,校验失败则抛出异常,并通过统一的异常处理返回友好提示
tokenService.checkToken(request);
} catch (Exception e) {
log.error("token校验失败==》",e);
writeReturnJson(response, JSONUtil.toJsonStr(e));
throw e;
}
}
/**
* 返回的json值
* @param response
* @param json
* @throws Exception
*/
private void writeReturnJson(HttpServletResponse response, String json) throws Exception{
PrintWriter writer = null;
response.setCharacterEncoding("UTF-8");
response.setContentType("text/html; charset=utf-8");
try {
writer = response.getWriter();
writer.print(json);
} catch (IOException e) {
} finally {
if (writer != null) {
writer.close();
}
}
}
}
测试

第一次请求,带上刚刚返回的Token,则返回请求成功!

当再次请求,则会发现token已经失效!

附录:
关于垂直越权
垂直越权是一种“基于URL的访问控制”设计缺陷引起的漏洞,又叫做权限提升攻击。
防范措施
- 前后端同时对用户输入信息进行校验,双重验证机制
- 调用功能前验证用户是否有权限调用相关功能
- 执行关键操作前必须验证用户身份,验证用户是否具备操作数据的权限
- 直接对象引用的加密资源ID,防止攻击者枚举ID,敏感数据特殊化处理
- 永远不要相信来自用户的输入,对于可控参数进行严格的检查与过滤
关于重放攻击
重放攻击(Replay Attacks)又称重播攻击、回放攻击,是指攻击者发送一个目的主机已接收过的包,来达到欺骗系统的目的,主要用于身份认证过程,破坏认证的正确性。重放攻击可以由发起者,也可以由拦截并重发该数据的敌方进行。
参考文章:
- Springboot + redis + 注解 + 拦截器来实现接口幂等性校验
:https://blog.csdn.net/jiahao1186/article/details/91793691 - 水平越权访问与垂直越权访问漏洞:
https://blog.csdn.net/u012068483/article/details/89553797
本文来自博客园,作者:Huathy,遵循 CC 4.0 BY-NC-SA 版权协议。转载请注明原文链接:https://www.cnblogs.com/huathy/p/17253834.html


浙公网安备 33010602011771号