使用拦截器和redis+token实现防重复提交完整代码

redis配置:

# redis
spring.redis.host=47.101.210.219
spring.redis.port=6379
spring.redis.timeout=0
     <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

其实也可以用jedis连接redis服务器,与使用redistemplate可以起到相同效果。
redistemplate需要配置序列化,以防出现乱码:


import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

/**
 * @Author h
 * @Description redis配置类
 * @Date 11.17
 **/
@Configuration
public class redisConfig {

    @Bean
    //1.项目启动时此方法先被注册成bean被spring管理
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        // 配置连接工厂
        template.setConnectionFactory(factory);

        //使用Jackson2JsonRedisSerializer来序列化和反序列化redis的value值(默认使用JDK的序列化方式)
        Jackson2JsonRedisSerializer jacksonSeial = new Jackson2JsonRedisSerializer(Object.class);

        ObjectMapper om = new ObjectMapper();
        // 指定要序列化的域,field,get和set,以及修饰符范围,ANY是都有包括private和public
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        // 指定序列化输入的类型,类必须是非final修饰的,final修饰的类,比如String,Integer等会跑出异常
        //om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
        jacksonSeial.setObjectMapper(om);

        // 值采用json序列化
        template.setValueSerializer(jacksonSeial);

        //使用StringRedisSerializer来序列化和反序列化redis的key值
        template.setKeySerializer(new StringRedisSerializer());

        // 设置hash key 和value序列化模式
        template.setHashKeySerializer(new StringRedisSerializer());
        template.setHashValueSerializer(jacksonSeial);
        template.afterPropertiesSet();

        return template;
    }
}

用于操作键值的工具:


/**
 * @Author hzy
 * @Description redis工具类
 * @Date 11.17
 **/
@Component
public class RedisTemplateUtil {
    @Qualifier("redisTemplate")
    @Autowired
    private RedisTemplate redisTemplate;

    /**
     * 写入缓存
     * @param key
     * @param value
     * @return
     */
    public boolean set(final String key, Object value) {
        boolean result = false;
        try{
            ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
            operations.set(key, value);
            result = true;
        } catch(Exception e) {
            e.printStackTrace();
        }
        return result;
    }
    /**
     * 写入缓存设置失效时间
     * @param key
     * @param value
     * @param expireTime
     * @return
     */
    public boolean setEx(final String key, Object value, Long expireTime) {
        boolean result = false;
        try{
            ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
            operations.set(key, value);
            redisTemplate.expire(key, expireTime, TimeUnit.SECONDS);
            result = true;
        } catch(Exception e) {
            e.printStackTrace();
        }
        return result;
    }
    /**
     * 判断缓存中是否有对应的value
     * @param key
     * @return
     */
    public boolean exists(final String key) {
        return redisTemplate.hasKey(key);
    }
    /**
     * 读取缓存
     * @param key
     * @return
     */
    public Object get(final String key) {
        Object result = null;
        ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
        result = operations.get(key);
        return result;
    }
    /**
     * 删除对应的value
     * @param key
     */
    public boolean remove(final String key) {
        if(exists(key)) {
            Boolean delete= redisTemplate.delete(key);
            return delete;
        }
        return false;
    }
}

自定义一个注解:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ForbidSubmit {


}

自定义类继承HandlerInterceptor

关于HandlerInterceptor:
允许自定义处理程序执行链的工作流接口。 应用程序可以为某些处理程序组注册任意数量的现有或自定义拦截器,以添加常见的预处理行为,而无需修改每个处理程序实现。
HandlerInterceptor 在适当的 HandlerAdapter 触发处理程序本身的执行之前被调用。 这种机制可用于预处理方面的大量领域,例如授权检查或常见的处理程序行为,如区域设置或主题更改。 它的主要目的是允许分解出重复的处理程序代码。
在异步处理场景中,处理程序可能在单独的线程中执行,而主线程退出而不呈现或调用postHandle和afterCompletion回调。 当并发处理程序执行完成时,请求被分派回来以继续渲染模型,并再次调用此合约的所有方法。 有关更多选项和详细信息,请参阅org.springframework.web.servlet.AsyncHandlerInterceptor
通常每个 HandlerMapping bean 定义一个拦截器链,共享它的粒度。 为了能够将某个拦截器链应用于一组处理程序,需要通过一个 HandlerMapping bean 映射所需的处理程序。 拦截器本身被定义为应用程序上下文中的 bean,映射 bean 定义通过其“拦截器”属性(在 XML 中: 的 )引用。
HandlerInterceptor 基本上类似于 Servlet 过滤器,但与后者相反,它只允许自定义预处理和禁止执行处理程序本身的选项,以及自定义后处理。 过滤器更强大,例如它们允许交换传递给链的请求和响应对象。 请注意,过滤器在 web.xml 中配置,即应用程序上下文中的 HandlerInterceptor。
作为基本准则,与细粒度处理程序相关的预处理任务是 HandlerInterceptor 实现的候选对象,尤其是分解出的公共处理程序代码和授权检查。 另一方面,过滤器非常适合请求内容和视图内容处理,例如多部分表单和 GZIP 压缩。 这通常显示何时需要将过滤器映射到某些内容类型(例如图像)或所有请求。

定义拦截器:


/**
 * @Author h
 * @Description 防止重复提交拦截器
 * @Date 11.17
 **/

public class RepeatInterceptor implements HandlerInterceptor {
@Autowired
private TokenService tokenService;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
            throws Exception {
        //HandlerMethod:封装有关由方法和bean组成的处理程序方法的信息。
        // 提供对方法参数、方法返回值、方法注解等的便捷访问。
        //类可以使用 bean 实例或 bean 名称(例如,lazy-init bean、prototype bean)创建。

if(handler instanceof HandlerMethod)
{
HandlerMethod handlerMethod = (HandlerMethod) handler;
    Method method = handlerMethod.getMethod();
    //被ForbidSubmit注释的方法
    ForbidSubmit annotation  = method.getAnnotation(ForbidSubmit.class);
if(annotation != null) {
   try {
       tokenService.checkToken(request,method.getName());
       tokenService.createToken(response,method.getName());
        }
        catch (Exception e)
        {
        Result result = Result.fail("不允许重复提交,请稍后重试");
     ServletUtils.renderString(response, JSONUtils.marshal(result));
     throw e;
        }
    }
    //renderstring方法:  response.setContentType("application/json");
    //            response.setCharacterEncoding("utf-8");
    //            response.getWriter().print(string);
    return true;
}
   else {
       return true;
       //return preHandle(request,response,handler);
   }
    }




}

mvc添加刚刚自定义的拦截器使之生效


@Configuration
@EnableTransactionManagement
public class MyConfig extends WebMvcConfigurationSupport {
@Autowired
private RepeatInterceptor repeatInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry)
{
    registry.addInterceptor(repeatInterceptor);
    super.addInterceptors(registry);
    //添加重复提交的拦截器
}
}

tokenservice


/**
 * @Author h
 * @Description 用于创建token, 在防止重复提交时使用到
 * @Date 11.17
 **/
@Service
public class TokenService {
  String TOKEN_NAME = "token";
    String TOKEN_PREFIX = "token:";
    // 过期时间, 10s,
    Integer EXPIRE_TIME_MINUTE = 10;
    // 过期时间, 一小时
    Integer EXPIRE_TIME_HOUR = 60 * 60;
    // 过期时间, 一天
    Integer EXPIRE_TIME_DAY = 60 * 60 * 24;
    //token引用了redis服务,创建token采用随机算法工具类生成随机uuid字符串,然后放入到redis中
    // (为了防止数据的冗余保留,这里设置过期时间为xx秒,具体可视业务而定),如果放入成功,最后返回这个token值。
    // checkToken方法就是从redis中获取token到值(如果拿不到就添加一个键值对),
    // 如若不存在,直接抛出异常。这个异常信息可以被拦截器捕捉到,然后返回给前端。
    @Autowired
    private RedisTemplateUtil redisTemplateUtil;
    public String createToken(HttpServletResponse response,String name)
    {

        String str = UUID.randomUUID().toString();
        StringBuilder token = new StringBuilder();
        response.setHeader("Access-Control-Expose-Headers",TOKEN_NAME);


        try {
            token.append(TOKEN_PREFIX).append(str);
            String key = TOKEN_PREFIX+ IpUtils.getHostIp()+name;
            response.setHeader(TOKEN_NAME,token.toString());
            if(redisTemplateUtil.exists(key))
            {
                return redisTemplateUtil.get(key).toString();
            }
            redisTemplateUtil.setEx(key, token.toString(), EXPIRE_TIME_MINUTE.longValue());
            boolean notEmpty = Objects.nonNull(token.toString());
            if(notEmpty)
            {
                return token.toString();
            }
        }catch (Exception e)
        {
            e.printStackTrace();
        }
        return new String();
    }

    public boolean checkToken(HttpServletRequest request,String name) throws Exception{

        String key = TOKEN_PREFIX+IpUtils.getHostIp()+name;
        boolean exists = redisTemplateUtil.exists(key);

        if(exists)
        {
            System.out.println("已经存在");
            throw new CustomException(400L, "请勿重复提交");
        }
        return true;
    }
}

自己在拦截器中先检查redis库是否有相应的键,如果有则直接返回禁止提交的信息给前端

controller

   /**
     * 新增保存角色信息
     */
    @ForbidSubmit
    @ApiOperation("新增一个角色")
    @PostMapping("/add/{RoleName}")
    @ResponseBody
    public Result addSave(@PathVariable("RoleName") String newRoleName,HttpServletResponse response)
    {

        InowRole inowRole = new InowRole();
        inowRole.setRoleName(newRoleName);
        inowRole.setCreateBy("ShiroUtils.getLoginName()");//获得登陆者的姓名

        inowRole.setCreateTime(new Date());
        inowRole.setDelFlag(0);
        if(inowRoleService.save(inowRole))
        {
            return Result.success(inowRole);
        }
        else
        {
            return Result.fail("新增失败");
        }
    }

在上面的controller方法中加了 @ForbidSubmit注解,这样访问时可以起到拦截作用。

测试:
第一次访问接口
在这里插入图片描述
第二次访问时已经弹出了异常信息:
在这里插入图片描述

后记:
aop也可以起到拦截器相同的功能,且细粒度更大,只需要定义一个防止重复提交的aspect即可,以后有时间再试试。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值