ITKeyword,专注技术干货聚合推荐

注册 | 登录

防重复提交专题系列3-3:springmvc下的基于token的防重复提交

wabiaozia 分享于 2016-07-29

推荐:Struts的Token(令牌)机制解决表单重复提交的问题

Struts的Token(令牌)机制能够很好的解决表单重复提交的问题,基本原理是:服务器端在处理到达的请求之前,会将请求中包含的令牌值与保存在当前用户会话中的令

2019阿里云全部产品优惠券(新购或升级都可以使用,强烈推荐)
领取地址https://promotion.aliyun.com/ntms/yunparter/invite.html

前言

        今天发了三篇博客,咋一看这三篇博客毫无联系,网上很多博客也多是将这三篇博客作为三篇不同的主题发表。如果你不将这三篇博客联系起来看,就不能很透彻的防重复提交这个知识点,也不能学完整这个知识体系。为什么说这三篇文章要关联看,我在第三篇博客 springmvc下的基于token的防重复提交 里会阐述原因.下面要讲的三篇博客:

3 springmvc下的基于token的防重复提交:http://blog.csdn.net/wabiaozia/article/details/52065065
2 总结 XSS 与 CSRF 两种跨站攻击:http://blog.csdn.net/wabiaozia/article/details/52064849
1 外刊IT评论:防止表单重复提交的几种策略:http://blog.csdn.net/wabiaozia/article/details/52064772

由于"springmvc下的基于token的防重复提交"网上demo很多,我就不写了,直接拷来一篇。


正文

        原文出处:http://blog.csdn.net/mylovepan/article/details/38894941

问题描述:

现在的网站在注册步骤中,由于后台要处理大量信息,造成响应变慢(测试机器性能差也是造成变慢的一个因素),在前端页面提交信息之前,等待后端响应,此时如果用户
再点一次提交按钮,后台会保存多份用户信息。为解决此问题,借鉴了struts2的token思路,在springmvc下实现token。

实现思路:

在springmvc配置文件中加入拦截器的配置,拦截两类请求,一类是到页面的,一类是提交表单的。当转到页面的请求到来时,生成token的名字和token值,一份放到redis缓存中,一份放传给页面表单的隐藏域。(注:这里之所以使用redis缓存,是因为tomcat服务器是集群部署的,要保证token的存储介质是全局线程安全的,而redis是单线程的)

当表单请求提交时,拦截器得到参数中的tokenName和token,然后到缓存中去取token值,如果能匹配上,请求就通过,不能匹配上就不通过。这里的tokenName生成时也是随机的,每次请求都不一样。而从缓存中取token值时,会立即将其删除(删与读是原子的,无线程安全问题)。


实现方式:

推荐:JavaWeb防止表单重复提交

在平时开发中,如果网速比较慢的情况下,用户提交表单后,发现服务器半天都没有响应,那么用户可能会以为是自己没有提交表单,就会再点击提交按钮重复提交表单,

TokenInterceptor.java

[java]  view plain   copy   在CODE上查看代码片 派生到我的代码片
  1. package com.xxx.www.common.interceptor;  
  2.   
  3. import java.io.IOException;  
  4. import java.util.HashMap;  
  5. import java.util.Map;  
  6. import javax.servlet.http.HttpServletRequest;  
  7. import javax.servlet.http.HttpServletResponse;  
  8. import org.apache.log4j.Logger;  
  9. import org.springframework.beans.factory.annotation.Autowired;  
  10. import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;  
  11. import com.xxx.cache.redis.IRedisCacheClient;  
  12. import com.xxx.common.utility.JsonUtil;  
  13. import com.xxx.www.common.utils.TokenHelper;  
  14.   
  15. /** 
  16.  *  
  17.  * @see TokenHelper 
  18.  */  
  19. public class TokenInterceptor extends HandlerInterceptorAdapter  
  20. {  
  21.       
  22.     private static Logger log = Logger.getLogger(TokenInterceptor.class);  
  23.     private static Map<String , String> viewUrls = new HashMap<String , String>();  
  24.     private static Map<String , String> actionUrls = new HashMap<String , String>();  
  25.     private Object clock = new Object();  
  26.       
  27.     @Autowired  
  28.     private IRedisCacheClient redisCacheClient;  
  29.     static  
  30.     {  
  31.         viewUrls.put("/user/regc/brandregnamecard/", "GET");  
  32.         viewUrls.put("/user/regc/regnamecard/", "GET");  
  33.           
  34.         actionUrls.put("/user/regc/brandregnamecard/", "POST");  
  35.         actionUrls.put("/user/regc/regnamecard/", "POST");  
  36.     }  
  37.     {  
  38.         TokenHelper.setRedisCacheClient(redisCacheClient);  
  39.     }  
  40.       
  41.     /** 
  42.      * 拦截方法,添加or验证token 
  43.      */  
  44.     @Override  
  45.     public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception  
  46.     {  
  47.         String url = request.getRequestURI();  
  48.         String method = request.getMethod();  
  49.         if(viewUrls.keySet().contains(url) && ((viewUrls.get(url)) == null || viewUrls.get(url).equals(method)))  
  50.         {  
  51.             TokenHelper.setToken(request);  
  52.             return true;  
  53.         }  
  54.         else if(actionUrls.keySet().contains(url) && ((actionUrls.get(url)) == null || actionUrls.get(url).equals(method)))  
  55.         {  
  56.             log.debug("Intercepting invocation to check for valid transaction token.");  
  57.             return handleToken(request, response, handler);  
  58.         }  
  59.         return true;  
  60.     }  
  61.       
  62.     protected boolean handleToken(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception  
  63.     {  
  64.         synchronized(clock)  
  65.         {  
  66.             if(!TokenHelper.validToken(request))  
  67.             {  
  68.                 System.out.println("未通过验证...");  
  69.                 return handleInvalidToken(request, response, handler);  
  70.             }  
  71.         }  
  72.         System.out.println("通过验证...");  
  73.         return handleValidToken(request, response, handler);  
  74.     }  
  75.       
  76.     /** 
  77.      * 当出现一个非法令牌时调用 
  78.      */  
  79.     protected boolean handleInvalidToken(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception  
  80.     {  
  81.         Map<String , Object> data = new HashMap<String , Object>();  
  82.         data.put("flag", 0);  
  83.         data.put("msg", "请不要频繁操作!");  
  84.         writeMessageUtf8(response, data);  
  85.         return false;  
  86.     }  
  87.       
  88.     /** 
  89.      * 当发现一个合法令牌时调用. 
  90.      */  
  91.     protected boolean handleValidToken(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception  
  92.     {  
  93.         return true;  
  94.     }  
  95.       
  96.     private void writeMessageUtf8(HttpServletResponse response, Map<String , Object> json) throws IOException  
  97.     {  
  98.         try  
  99.         {  
  100.             response.setCharacterEncoding("UTF-8");  
  101.             response.getWriter().print(JsonUtil.toJson(json));  
  102.         }  
  103.         finally  
  104.         {  
  105.             response.getWriter().close();  
  106.         }  
  107.     }  
  108.       
  109. }  

TokenHelper.java

[java]  view plain   copy   在CODE上查看代码片 派生到我的代码片
  1. package com.xxx.www.common.utils;  
  2.   
  3. import java.math.BigInteger;  
  4. import java.util.Map;  
  5. import java.util.Random;  
  6. import javax.servlet.http.HttpServletRequest;  
  7. import org.apache.log4j.Logger;  
  8. import com.xxx.cache.redis.IRedisCacheClient;  
  9.   
  10. /** 
  11.  * TokenHelper 
  12.  *  
  13.  */  
  14. public class TokenHelper  
  15. {  
  16.       
  17.     /** 
  18.      * 保存token值的默认命名空间 
  19.      */  
  20.     public static final String TOKEN_NAMESPACE = "xxx.tokens";  
  21.       
  22.     /** 
  23.      * 持有token名称的字段名 
  24.      */  
  25.     public static final String TOKEN_NAME_FIELD = "xxx.token.name";  
  26.     private static final Logger LOG = Logger.getLogger(TokenHelper.class);  
  27.     private static final Random RANDOM = new Random();  
  28.       
  29.     private static IRedisCacheClient redisCacheClient;// 缓存调用,代替session,支持分布式  
  30.       
  31.     public static void setRedisCacheClient(IRedisCacheClient redisCacheClient)  
  32.     {  
  33.         TokenHelper.redisCacheClient = redisCacheClient;  
  34.     }  
  35.       
  36.     /** 
  37.      * 使用随机字串作为token名字保存token 
  38.      *  
  39.      * @param request 
  40.      * @return token 
  41.      */  
  42.     public static String setToken(HttpServletRequest request)  
  43.     {  
  44.         return setToken(request, generateGUID());  
  45.     }  
  46.       
  47.     /** 
  48.      * 使用给定的字串作为token名字保存token 
  49.      *  
  50.      * @param request 
  51.      * @param tokenName 
  52.      * @return token 
  53.      */  
  54.     private static String setToken(HttpServletRequest request, String tokenName)  
  55.     {  
  56.         String token = generateGUID();  
  57.         setCacheToken(request, tokenName, token);  
  58.         return token;  
  59.     }  
  60.       
  61.     /** 
  62.      * 保存一个给定名字和值的token 
  63.      *  
  64.      * @param request 
  65.      * @param tokenName 
  66.      * @param token 
  67.      */  
  68.     private static void setCacheToken(HttpServletRequest request, String tokenName, String token)  
  69.     {  
  70.         try  
  71.         {  
  72.             String tokenName0 = buildTokenCacheAttributeName(tokenName);  
  73.             redisCacheClient.listLpush(tokenName0, token);  
  74.             request.setAttribute(TOKEN_NAME_FIELD, tokenName);  
  75.             request.setAttribute(tokenName, token);  
  76.         }  
  77.         catch(IllegalStateException e)  
  78.         {  
  79.             String msg = "Error creating HttpSession due response is commited to client. You can use the CreateSessionInterceptor or create the HttpSession from your action before the result is rendered to the client: " + e.getMessage();  
  80.             LOG.error(msg, e);  
  81.             throw new IllegalArgumentException(msg);  
  82.         }  
  83.     }  
  84.       
  85.     /** 
  86.      * 构建一个基于token名字的带有命名空间为前缀的token名字 
  87.      *  
  88.      * @param tokenName 
  89.      * @return the name space prefixed session token name 
  90.      */  
  91.     public static String buildTokenCacheAttributeName(String tokenName)  
  92.     {  
  93.         return TOKEN_NAMESPACE + "." + tokenName;  
  94.     }  
  95.       
  96.     /** 
  97.      * 从请求域中获取给定token名字的token值 
  98.      *  
  99.      * @param tokenName 
  100.      * @return the token String or null, if the token could not be found 
  101.      */  
  102.     public static String getToken(HttpServletRequest request, String tokenName)  
  103.     {  
  104.         if(tokenName == null)  
  105.         {  
  106.             return null;  
  107.         }  
  108.         Map params = request.getParameterMap();  
  109.         String[] tokens = (String[]) (String[]) params.get(tokenName);  
  110.         String token;  
  111.         if((tokens == null) || (tokens.length < 1))  
  112.         {  
  113.             LOG.warn("Could not find token mapped to token name " + tokenName);  
  114.             return null;  
  115.         }  
  116.           
  117.         token = tokens[0];  
  118.         return token;  
  119.     }  
  120.       
  121.     /** 
  122.      * 从请求参数中获取token名字 
  123.      *  
  124.      * @return the token name found in the params, or null if it could not be found 
  125.      */  
  126.     public static String getTokenName(HttpServletRequest request)  
  127.     {  
  128.         Map params = request.getParameterMap();  
  129.           
  130.         if(!params.containsKey(TOKEN_NAME_FIELD))  
  131.         {  
  132.             LOG.warn("Could not find token name in params.");  
  133.             return null;  
  134.         }  
  135.           
  136.         String[] tokenNames = (String[]) params.get(TOKEN_NAME_FIELD);  
  137.         String tokenName;  
  138.           
  139.         if((tokenNames == null) || (tokenNames.length < 1))  
  140.         {  
  141.             LOG.warn("Got a null or empty token name.");  
  142.             return null;  
  143.         }  
  144.           
  145.         tokenName = tokenNames[0];  
  146.           
  147.         return tokenName;  
  148.     }  
  149.       
  150.     /** 
  151.      * 验证当前请求参数中的token是否合法,如果合法的token出现就会删除它,它不会再次成功合法的token 
  152.      *  
  153.      * @return 验证结果 
  154.      */  
  155.     public static boolean validToken(HttpServletRequest request)  
  156.     {  
  157.         String tokenName = getTokenName(request);  
  158.           
  159.         if(tokenName == null)  
  160.         {  
  161.             LOG.debug("no token name found -> Invalid token ");  
  162.             return false;  
  163.         }  
  164.           
  165.         String token = getToken(request, tokenName);  
  166.           
  167.         if(token == null)  
  168.         {  
  169.             if(LOG.isDebugEnabled())  
  170.             {  
  171.                 LOG.debug("no token found for token name " + tokenName + " -> Invalid token ");  
  172.             }  
  173.             return false;  
  174.         }  
  175.           
  176.         String tokenCacheName = buildTokenCacheAttributeName(tokenName);  
  177.         String cacheToken = redisCacheClient.listLpop(tokenCacheName);  
  178.           
  179.         if(!token.equals(cacheToken))  
  180.         {  
  181.             LOG.warn("xxx.internal.invalid.token Form token " + token + " does not match the session token " + cacheToken + ".");  
  182.             return false;  
  183.         }  
  184.           
  185.         // remove the token so it won't be used again  
  186.           
  187.         return true;  
  188.     }  
  189.       
  190.     public static String generateGUID()  
  191.     {  
  192.         return new BigInteger(165, RANDOM).toString(36).toUpperCase();  
  193.     }  
  194.       
  195. }  

spring-mvc.xml

[html]  view plain   copy   在CODE上查看代码片 派生到我的代码片
  1. <!-- token拦截器-->  
  2.     <bean id="tokenInterceptor" class="com.xxx.www.common.interceptor.TokenInterceptor"></bean>      
  3.     <bean class="org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping">      
  4.         <property name="interceptors">      
  5.             <list>      
  6.                 <ref bean="tokenInterceptor"/>      
  7.             </list>  
  8.         </property>      
  9.     </bean>  

input.jsp 在form中加如下内容:

[html]  view plain   copy   在CODE上查看代码片 派生到我的代码片
  1. <input type="hidden" name="<%=request.getAttribute("xxx.token.name") %>" value="<%=token %>"/>  
  2.   
  3. <input type="hidden" name="xxx.token.name" value="<%=request.getAttribute("xxx.token.name") %>"/>  

当前这里也可以用类似于struts2的自定义标签来做。

另:公司域名做了隐藏,用xxx替换了。


为什么用redis不用tomcat的session存token?

http://www.cnblogs.com/jiangu66/p/3241093.html

session存储和同步
由于默认tomcat使用内存管理session,在集群环境下,上述的做法将会存在不一致问题。比如用户从A服务器获取了表单和token,但是提交表单时候却往B服务器提交,这样B服务器判断用户为csrf攻击,所以,用session管理涉及道同步问题。当然,另一个做法是把cookie当session用,把csrf的token放在用户的cookie中。但是,为了避免泄漏token,需要对token进行加密,和进行http only的设置,后者避免js对cookie中的token进行访问。

-------------------------------------------------------------------------------------------------------------------------------------------

我博客所有文章目录: 点击打开链接

推荐:利用Token防止重复提交(Struts框架)

利用Token防止重复提交(Struts框架)   如果用户对一个html表单多次提交,web应用应该能够判断用户的重复提交行为,并作出相应的处理。    最常见的是新增一条数

前言         今天发了三篇博客,咋一看这三篇博客毫无联系,网上很多博客也多是将这三篇博客作为三篇不同的主题发表。如果你不将这三篇博客联系起来看,就不能很透彻的防重复提交这个知识点,也不

相关阅读排行


用户评论

游客

相关内容推荐

最新文章

×

×

请激活账号

为了能正常使用评论、编辑功能及以后陆续为用户提供的其他产品,请激活账号。

您的注册邮箱: 修改

重新发送激活邮件 进入我的邮箱

如果您没有收到激活邮件,请注意检查垃圾箱。