介紹了使用 redirect 技術防止表單提交,可是 redirect 解決不了後退到表單頁面時重複提交表單,爲了解決這個問題,加入了 token 的機制。若是每一個 form 相關的處理方法中都寫一遍 token 的生成和校驗代碼,在實際項目中是不太能接受的,接下來介紹了使用攔截器的方式生成和校驗 token。 html
Result: ${result!}
<!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>
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"; } }
可是,若是在瀏覽器裏點擊後退按鈕後退到表單頁面,點擊 Update User,表單被再次提交了。可使用 token 防止後退的狀況下重複提交表單,訪問表單頁面的時候生成一個 token 在 form 裏而且在 Server 端存儲這個 token,提交表單的時候先檢查 Server 端有沒有這個 token,若是有則說明是第一次提交表單,而後把 token 從 Server 端刪除,處理表單,redirect 到 result 頁面,若是 Server 端沒有這個 token,則說明是重複提交的表單,不處理表單的提交。 java
Result: ${result!}
在 form 裏增長一個 input 域存放 token. web
<!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>
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("重複提交表單"); } // 正常提交表單,刪除 token session.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"; } }
思考一下,爲了給 user-form 增長 token,在處理 user-form 的方法裏新加了不少代碼,若是有 10 個 form, 100 form 都要使用 token 的機制呢?難道要去每一個 form 處理的方法裏都加上上面的那麼多代碼嗎?上面 token 使用的是 UUID,若是要改爲 static 類型的整數,每次生成時都加 1 呢? token 存儲在 session 裏,項目進行到必定的時候要決定存儲在第三方緩存如 Redis 裏呢?每次需求的變動都要修改全部 form 的處理方法? 工做量也太大了,誰遇到這樣的問題都會抓狂,難怪招聘裏着重強調:不準打項目經理! spring
什麼是 token? 簡單的說就是一次操做的標識,能夠是數字,字符串,甚至對象等,只要能把不一樣的表單提交區別開來就能夠了。申請表單的時候生成一個 token,表單提交後刪除 token。 數據庫
Result: ${result!}
在 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>
和開始的 Controller 代碼同樣,沒有 token 的相關代碼。 spring-mvc
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 用於生成和校驗 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("重複提交表單"); } // 正常提交表單,刪除 token request.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 { } }
<beans> ... <mvc:interceptors> <mvc:interceptor> <mvc:mapping path="/user-form"/> <!--須要增長 token 校驗的 form 的 URI--> <bean class="interceptor.TokenValidator"></bean> </mvc:interceptor> </mvc:interceptors> </beans>
Token 的存儲須要考慮過時時間,不然訪問 10 萬次 user-form 頁面,生成 10 萬個 token 而不提交表單,token 一直不會被刪除,會形成很大的資源浪費。 session
Token 應該寫入到 form 的隱藏域,爲了直觀,上面咱們寫入了普通的 input 中:
<input type="hidden" name="token" value="${token!}"> mvc這裏使用的是 SpringMVC 的攔截器生成和校驗 token,固然也可使用 Servlet 的 Filter 等技術實現。
重複提交表單時不該該直接把異常顯示給用戶,可使用 SpringMVC 的異常處理機制,不一樣的頁面顯示不一樣異常的友好信息