用户访问表单添加页面->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相关配置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 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;
}
}