表单防重复提交
2019獨角獸企業重金招聘Python工程師標準>>>
防止表單重復提交
介紹了使用 redirect 技術防止表單提交,但是 redirect 解決不了后退到表單頁面時重復提交表單,為了解決這個問題,加入了 token 的機制。如果每個 form 相關的處理方法中都寫一遍 token 的生成和校驗代碼,在實際項目中是不太能接受的,接下來介紹了使用攔截器的方式生成和校驗 token。
1. 常規防止表單重復提交流程:
result.htm
Result: ${result!}user-form.htm
<!DOCTYPE html> <html> <head><title>Update User</title> </head> <body> <form action="/user-form" method="post">Username: <input type="text" name="username"><br>Password: <input type="text" name="password"><br><button type="submit">Update User</button> </form> </body> </html>
ParameterController
package controller;import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.servlet.mvc.support.RedirectAttributes;@Controller public class ParameterController {// 顯示表單@RequestMapping(value = "/user-form", method= RequestMethod.GET)public String showUserForm() {return "user-form.htm";}// 更新 User,把操作結果保存到 redirectAttributes,// redirect 到 result 頁面顯示操作結果@RequestMapping(value = "/user-form", method= RequestMethod.POST)public String handleUserForm(@RequestParam String username,@RequestParam String password,final RedirectAttributes redirectAttributes) {// Update user in database...System.out.println("Username: " + username + ", Password: " + password);// 操作結果顯示給用戶redirectAttributes.addFlashAttribute("result", "The user is already successfully updated");return "redirect:/result"; // URI instead of viewName}// 顯示表單處理結果@RequestMapping("/result")public String result() {return "result.htm";} }
測試一
2. 使用 token 進一步加強防止表單重復提交
但是,如果在瀏覽器里點擊后退按鈕后退到表單頁面,點擊?Update User,表單被再次提交了。可以使用 token 防止后退的情況下重復提交表單,訪問表單頁面的時候生成一個 token 在 form 里并且在 Server 端存儲這個 token,提交表單的時候先檢查 Server 端有沒有這個 token,如果有則說明是第一次提交表單,然后把 token 從 Server 端刪除,處理表單,redirect 到 result 頁面,如果 Server 端沒有這個 token,則說明是重復提交的表單,不處理表單的提交。
result.htm
Result: ${result!}user-form.htm
在 form 里增加一個 input 域存放 token.
<!DOCTYPE html> <html> <head><title>Update User</title> </head> <body> <form action="/user-form" method="post"><input type="input" name="token" value="${token!}"><br>Username: <input type="text" name="username"><br>Password: <input type="text" name="password"><br><button type="submit">Update User</button> </form> </body> </html>
ParameterController
package controller;import org.springframework.stereotype.Controller; import org.springframework.ui.ModelMap; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.servlet.mvc.support.RedirectAttributes;import javax.servlet.http.HttpSession; import java.util.UUID;@Controller public class ParameterController {// 顯示表單@RequestMapping(value = "/user-form", method= RequestMethod.GET)public String showUserForm(ModelMap model, HttpSession session) {String token = UUID.randomUUID().toString().toUpperCase().replaceAll("-", "");model.addAttribute("token", token);session.setAttribute(token, token);return "user-form.htm";}// 更新 User,把操作結果保存到 redirectAttributes,// redirect 到 result 頁面顯示操作結果@RequestMapping(value = "/user-form", method= RequestMethod.POST)public String handleUserForm(@RequestParam String username,@RequestParam String password,@RequestParam String token,HttpSession session,RedirectAttributes redirectAttributes) {// 處理表單前,查看 token 是否有效if (token == null || token.isEmpty() || !token.equals(session.getAttribute(token))) {throw new RuntimeException("重復提交表單");}// 正常提交表單,刪除 tokensession.removeAttribute(token);// Update user in database...System.out.println("Username: " + username + ", Password: " + password);// 操作結果顯示給用戶redirectAttributes.addFlashAttribute("result", "The user is already successfully updated");return "redirect:result";}// 顯示表單處理結果@RequestMapping("/result")public String result() {return "result.htm";} }
測試二
3. 使用 SpringMVC 攔截器生成和驗證 token
思考一下,為了給 user-form 增加 token,在處理 user-form 的方法里新加了很多代碼,如果有 10 個 form, 100 form 都要使用 token 的機制呢?難道要去每個 form 處理的方法里都加上上面的那么多代碼嗎?上面 token 使用的是 UUID,如果要改成 static 類型的整數,每次生成時都加 1 呢? token 存儲在 session 里,項目進行到一定的時候要決定存儲在第三方緩存如 Redis 里呢?每次需求的變更都要修改所有 form 的處理方法? 工作量也太大了,誰遇到這樣的問題都會抓狂,難怪招聘里著重強調:不許打項目經理!
幸好 SpringMVC 提供了攔截器的機制,能夠很簡單的給 form 增加 token 的機制
- 當訪問 user-form 頁面時,在攔截器的 postHandle() 里生成 token 存放在 ModelAndView 和 session 里。
- 提交表單時,在攔截器的 preHandle() 里校驗 token,如果 token 無效則禁止表單的提交。
- 需要增加 token 機制的 form 在攔截器的配置里加上 form 的 URI。
- 如果 form 不需要 token 機制,從攔截器的配置里把它的 URI 刪除即可。
- 不需要修改 Controller 中的代碼。
什么是 token? 簡單的說就是一次操作的標識,可以是數字,字符串,甚至對象等,只要能把不同的表單提交區別開來就可以了。申請表單的時候生成一個 token,表單提交后刪除 token。
result.htm
Result: ${result!}user-form.htm
在 form 里增加一個 input 域存放 token.
<!DOCTYPE html> <html> <head><title>Update User</title> </head> <body> <form action="/user-form" method="post"><input type="input" name="token" value="${token!}"><br>Username: <input type="text" name="username"><br>Password: <input type="text" name="password"><br><button type="submit">Update User</button> </form> </body> </html>
ParameterController
和開始的 Controller 代碼一樣,沒有 token 的相關代碼。
package controller;import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.servlet.mvc.support.RedirectAttributes;@Controller public class ParameterController {// 顯示表單@RequestMapping(value = "/user-form", method= RequestMethod.GET)public String showUserForm() {return "user-form.htm";}// 更新 User,把操作結果保存到 redirectAttributes,// redirect 到 result 頁面顯示操作結果@RequestMapping(value = "/user-form", method= RequestMethod.POST)public String handleUserForm(@RequestParam String username,@RequestParam String password,final RedirectAttributes redirectAttributes) {// Update user in database...System.out.println("Username: " + username + ", Password: " + password);// 操作結果顯示給用戶redirectAttributes.addFlashAttribute("result", "The user is already successfully updated");return "redirect:result";}// 顯示表單處理結果@RequestMapping("/result")public String result() {return "result.htm";} }
TokenValidator
攔截器 TokenValidator 用于生成和校驗 token。
package interceptor;import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.ModelAndView;import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.util.UUID;public class TokenValidator implements HandlerInterceptor {public boolean preHandle(HttpServletRequest request,HttpServletResponse response,Object handler) throws Exception {// POST, PUT, DELETE 請求都有可能是表單提交if (!"GET".equalsIgnoreCase(request.getMethod())) {String clientToken = request.getParameter("token");String serverToken = (String) request.getSession().getAttribute(clientToken);if (clientToken == null || clientToken.isEmpty() || !clientToken.equals(serverToken)) {throw new RuntimeException("重復提交表單");}// 正常提交表單,刪除 tokenrequest.getSession().removeAttribute(clientToken);}return true;}public void postHandle(HttpServletRequest request,HttpServletResponse response,Object handler,ModelAndView modelAndView) throws Exception {// GET 請求訪問表單頁面if (!"GET".equalsIgnoreCase(request.getMethod())) {return;}// 生成 token 存儲到 session 里,并且保存到 form 的 input 域String token = UUID.randomUUID().toString().replaceAll("-", "").toUpperCase();modelAndView.addObject("token", token);request.getSession().setAttribute(token, token);}public void afterCompletion(HttpServletRequest request,HttpServletResponse response,Object handler,Exception ex) throws Exception {} }
spring-mvc.xml 里配置攔截器
<beans>...<mvc:interceptors><mvc:interceptor><mvc:mapping path="/user-form"/> <!--需要增加 token 校驗的 form 的 URI--><bean class="interceptor.TokenValidator"></bean></mvc:interceptor></mvc:interceptors> </beans>
測試三
通過 SpringMVC 攔截器增加 token 的機制,
- 想改變 token 生成策略? 修改 TokenValidator
- 想改變 token 的存儲策略? 修改 TokenValidator
- 想給 form 增加 token 校驗? 修改 spring-mvc.xml 攔截器的配置
- 想把 form 的 token 校驗刪除? 修改 spring-mvc.xml 攔截器的配置
- 不需要修改任何 form 處理的方法,泰山崩于前而色不變,風波驟起而泰然處之,項目經理好像也沒那么可恨了
提示:
Token 的存儲需要考慮過期時間,否則訪問 10 萬次 user-form 頁面,生成 10 萬個 token 而不提交表單,token 一直不會被刪除,會造成很大的資源浪費。
Token 應該寫入到 form 的隱藏域,為了直觀,上面我們寫入了普通的 input 中:
<input type="hidden" name="token" value="${token!}">
這里使用的是 SpringMVC 的攔截器生成和校驗 token,當然也可以使用 Servlet 的 Filter 等技術實現。
重復提交表單時不應該直接把異常顯示給用戶,可以使用 SpringMVC 的異常處理機制,不同的頁面顯示不同異常的友好信息
轉載于:https://my.oschina.net/stonezing/blog/501583
總結
- 上一篇: Lotus中关于字符串处理的函数汇总
- 下一篇: C. Diverse Permutati