处理流程
用户访问表单添加页面->spring防重复token拦截器拦截请求url,判断url对应的controller方法是是否注解有生成防重复token的标识->生成防重复token保存到redis中RedisUtil.getRu().setex(“formToken_” + uuid, “1”, 60 * 60);同时将本次生存的防重复token放到session中->跳转到表单页面时重token中取出放入表单->用户填写完信息提交表单->spring防重复token拦截器拦截请求url,判断提交的url对应controller方法是否注解有处理防重复提交token的标识->redis中formToken做原子减1操作RedisUtil.getRu().decr(“formToken_” + clinetToken);如果不redis中做了减1操作后值不为0,则为重复提交,返回false。
自定义注解
为何要用自定义注解
有些方法我们想要它只能被特定的用户访问到,比如用户登录之后才能访问。spring 的拦截器可以配置拦截的路由,但在 restful 风格的路由中,往往有重复的,根据 http method 来指定功能,这样子的话直接配置拦截器路由规则也不太方便。所以我们可以自定义一个注解,将它用在需要登录的方法中,然后在拦截器中判断要访问的方法是否有我们自定义的注解,如果有就判断当前用户是否登录了(判断是否携带了登录之后获取到的 token ),从而决定是否拦截。
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AnnotationToken {
/**
* 需要防重复功能的表单入口URL对应的controller方法需要添加的注解,用于生成token(默认为uuid)
* @return
*/
boolean repeat() default false;
/**
* 防重复表单提交表单到后台对应的URL的controller方法需要添加的注解,用于第一次成功提交后remove掉token
* @return
*/
boolean checkRepeat() default false;
}
拦截器相关设置
-
添加拦截器
public class TokenInterceptor extends HandlerInterceptorAdapter { private static final Logger logger = Logger.getLogger(TokenInterceptor.class); @Autowired private RedisUtils redisUtils; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler){ //如果没有映射到方法直接通过 if(!(handler instanceof HandlerMethod)){ return true; } HandlerMethod handlerMethod = (HandlerMethod)handler; Method method = handlerMethod.getMethod(); //获取注解 AnnotationToken annotation = method.getAnnotation(AnnotationToken.class); //如果没有注解,直接通过 if(annotation == null){ return true; } //获取注解repeat boolean needrepeat = annotation.repeat(); //如果需要防重发,在redis中添加计数器 if(needrepeat){ String uuid = UUID.randomUUID().toString(); String key = "token" + uuid; System.out.println("uuid:"+uuid); redisUtils.set("token_"+uuid,"1",60*60); request.getSession().setAttribute("token",uuid); logger.warn(request.getServletPath() + "---->formToken:" + uuid); } //获取注解checkRepeat,第一次提交则去掉token,并允许交易通过,重复提交则不允许交易通过 boolean needCheckRepeat = annotation.checkRepeat(); if(needCheckRepeat){ if(isRepeatSubmit(request)){ logger.warn("please don't repeat submit,url:" + request.getServletPath()); return false; } } return true; } private boolean isRepeatSubmit(HttpServletRequest request){ String token = (String)request.getSession().getAttribute("token"); //检查redis中是否存在该用户的token boolean r = redisUtils.hasKey("token"+token); if(r){ //用户token减一,如果等于0,则是第一次提交 Long count = redisUtils.decr("token"+token,1); redisUtils.del("token"+token); if(count == 0){ //删除用户token return false; } } return true; } }
-
在spring中注册拦截器
@Configuration public class WebAppConfig extends WebMvcConfigurerAdapter { @Override public void addInterceptors(InterceptorRegistry registry){ //注册自定义拦截器 registry.addInterceptor(tokenInterceptor()).addPathPatterns("/**"); // 拦截所有请求,通过判断是否有 @LoginRequired 注解 决定是否需要登录 super.addInterceptors(registry); } @Bean public TokenInterceptor tokenInterceptor(){ return new TokenInterceptor(); } }
添加Redis相关配置和工具类
-
添加redis相关配置
Resource/conf/redis.properties
redis.host=127.0.0.1 redis.port=6379 redis.timeout=10000 redis.maxIdle=300 redis.maxTotal=1000
-
添加redis工具类
public class RedisUtils { @Autowired private RedisTemplate redisTemplate; // public void setRedisTemplate(RedisTemplate<String, Object> redisTemplate) { // this.redisTemplate = redisTemplate; // } //=============================common============================ /** * 指定缓存失效时间 * @param key 键 * @param time 时间(秒) * @return */ public boolean expire(String key,long time){ try { if(time>0){ redisTemplate.expire(key, time, TimeUnit.SECONDS); } return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 根据key 获取过期时间 * @param key 键 不能为null * @return 时间(秒) 返回0代表为永久有效 */ public long getExpire(String key){ return redisTemplate.getExpire(key,TimeUnit.SECONDS); } /** * 判断key是否存在 * @param key 键 * @return true 存在 false不存在 */ public boolean hasKey(String key){ try { return redisTemplate.hasKey(key); } catch (Exception e) { e.printStackTrace(); return false; } } }
-
添加redis的配置类
@PropertySource("classpath:conf/redis.properties") @Configration public class RedisConfig { @Value("${redis.host}") private String host; @Value("${redis.port}") private Integer port; @Value("${redis.maxTotal}") private Integer maxTotal; @Value("${redis.maxIdle}") private Integer maxIdle; @Value("${redis.timeout}") private Integer timeout; @Bean public JedisPoolConfig jedisPoolConfig(){ JedisPoolConfig config = new JedisPoolConfig(); config.setMaxTotal(maxTotal); config.setMaxIdle(maxIdle); return config; } @Bean public JedisConnectionFactory jedisConnectionFactory(JedisPoolConfig config){ JedisConnectionFactory factory = new JedisConnectionFactory(config); factory.setHostName(host); factory.setPort(port); factory.setTimeout(timeout); return factory; } @Bean public RedisTemplate redisTemplate(JedisConnectionFactory factory){ RedisTemplate redisTemplate = new RedisTemplate(); redisTemplate.setKeySerializer(new StringRedisSerializer()); redisTemplate.setHashKeySerializer(new StringRedisSerializer()); redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer()); redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer()); // 开启事务 redisTemplate.setEnableTransactionSupport(true); redisTemplate.setConnectionFactory(factory); return redisTemplate; } }