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)状态机 – 状态变更, 更新数据时判断状态

利用拦截器的代码实现

  1. 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>
  1. 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;
    }
}
  1. 自定义注解@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 {
}
  1. 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();
            }
        }
    }
}
  1. 拦截器配置
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);
    }
}
  1. 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;
    }
}
  1. 测试类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的代码实现

  1. 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>
  1. 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的访问控制”设计缺陷引起的漏洞,又叫做权限提升攻击。
在这里插入图片描述

防范措施

  1. 前后端同时对用户输入信息进行校验,双重验证机制
  2. 调用功能前验证用户是否有权限调用相关功能
  3. 执行关键操作前必须验证用户身份,验证用户是否具备操作数据的权限
  4. 直接对象引用的加密资源ID,防止攻击者枚举ID,敏感数据特殊化处理
  5. 永远不要相信来自用户的输入,对于可控参数进行严格的检查与过滤

关于重放攻击

重放攻击(Replay Attacks)又称重播攻击、回放攻击,是指攻击者发送一个目的主机已接收过的包,来达到欺骗系统的目的,主要用于身份认证过程,破坏认证的正确性。重放攻击可以由发起者,也可以由拦截并重发该数据的敌方进行。

参考文章:

  1. Springboot + redis + 注解 + 拦截器来实现接口幂等性校验
    https://blog.csdn.net/jiahao1186/article/details/91793691
  2. 水平越权访问与垂直越权访问漏洞:
    https://blog.csdn.net/u012068483/article/details/89553797
posted @ 2021-03-16 17:58  Huathy  阅读(58)  评论(0)    收藏  举报  来源