使用拦截器和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即可,以后有时间再试试。

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
【资源说明】 1、该资源包括项目的全部源码,下载可以直接使用! 2、本项目适合作为计算机、数学、电子信息等专业的课程设计、期末大作业和毕设项目,作为参考资料学习借鉴。 3、本资源作为“参考资料”如果需要实现其他功能,需要能看懂代码,并且热爱钻研,自行调试。 基于SSM框架+Redis实现的线上秒杀系统项目源码+项目说明.zip 最近把SSM框架的基础知识撸了一遍,跟着[github上的这个项目](https://github.com/codingXiaxw/seckill) ,实现了一下秒杀系统,并对这个项目中存在的问题进行了 一些小小的改进,记录一下。整个项目用到了Spring+SpringMVC+Mybatis+Redis框架,如果是刚学SSM框架希望找个小项目练手的,可以跟着这个项目来练练手,感觉涉及的知识还是很全面的。项目源码已上传到GitHub:[https://github.com/MeteorCh/SecKill](https://github.com/MeteorCh/SecKill),需要的同学自取。 # 一、项目功能及涉及知识点 项目的整体业务流程如下(感觉自己画的可能不是很标准,表达意思即可) ![抢购流程](https://img-blog.csdnimg.cn/20200325111907951.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMxNzA5MjQ5,size_16,color_FFFFFF,t_70) ## 1.数据库 项目涉及三个数据表 ### (1)用户表user ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200325114514245.png) ### (2)商品表seckill ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200325114543975.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMxNzA5MjQ5,size_16,color_FFFFFF,t_70) ### (3)秒杀成功记录表success_seckilled ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200325114706724.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMxNzA5MjQ5,size_16,color_FFFFFF,t_70) ## 2.登录模块 ### 2.1 功能描述 在这里使用了Spring的拦截对访问的网址进行拦截,如果没登录,就跳转到登录页面登录。如果输入的用户名和密码都正确,则将用户名和用户名用MD5加密的token写入Cookie,下次登录时,首先判断Cookie的登录信息正不正确,正确的话自动登录。此外,当用户访问登录页面时,如果存在cookie,则直接跳转到列表页,这些逻辑都写在**LoginInterceptor**类中,具体内容下载源码查看。登录页面如下: ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200325112707366.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMxNzA5MjQ5,size_16,color_FFFFFF,t_70) ### 2.2 涉及知识点 登录模块涉及的知识点主要有:==Cookie、Session、SpringMVC拦截、MD5验证== ## 3.秒杀商品列表模块 ### 3.1功能描述 展示所有的秒杀商品,需要注意的是,从数据库中查找商品时,应把库存数量为0的过滤掉。秒杀商品列表页面如下: ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200325113022222.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly
使用Redis实现token校验时,可以根据用户的使用情况动态调整token的过期时间。在生成token的同时,将创建token的时间戳存储在Redis中。每次请求被拦截拦截并通过token验证成功后,将当前时间与存储在Redis中的token生成时间进行比较。如果当前时间距离创建时间快要到达设置的Redis过期时间,就重新设置token的过期时间,延长过期时间。如果用户在设置的Redis过期时间内没有进行任何操作(没有发请求),则token会在Redis中过期。具体的代码实现可以在Controller层生成token信息并存入Redis中,同时与用户登录态建立联系。生成token时,可以使用UUID生成唯一的token,并将token和用户登录态存入Redis中。设置token的过期时间,例如1小时。最后,将token返回给客户端。在拦截中,通过获取客户端传过来的Authorization字段,尝试从Redis中获取对应的用户名。如果可以获取到用户名,则说明token正确;反之,说明token错误,返回鉴权失败。这样就实现了基于Redistoken校验。\[1\]\[2\]\[3\] #### 引用[.reference_title] - *1* *2* *3* [基于SpringBoot和Redis实现Token权限认证 & 基于redis实现token验证用户是否登陆](https://blog.csdn.net/weixin_38088772/article/details/111319039)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insert_down28v1,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值