表單防重複提交

防止表單重複提交

介紹了使用 redirect 技術防止表單提交,可是 redirect 解決不了後退到表單頁面時重複提交表單,爲了解決這個問題,加入了 token 的機制。若是每一個 form 相關的處理方法中都寫一遍 token 的生成和校驗代碼,在實際項目中是不太能接受的,接下來介紹了使用攔截器的方式生成和校驗 token。 html

1. 常規防止表單重複提交流程:

  1. GET 訪問表單頁面
  2. 填寫表單
  3. POST 提交表單
  4. Server 端處理表單數據,例如把數據寫入數據庫
  5. 重定向到另外一個頁面,防止用戶刷新頁面重複提交表單


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";
    }
}




測試一

  1. 訪問 http://localhost/user-form
  2. 點擊 Update User,表單成功提交後被重定向到 result 頁面
  3. 刷新 result 頁面,表單沒有被重複提交,實現了防止表單重複提交的功能

2. 使用 token 進一步增強防止表單重複提交

可是,若是在瀏覽器裏點擊後退按鈕後退到表單頁面,點擊 Update User,表單被再次提交了。可使用 token 防止後退的狀況下重複提交表單,訪問表單頁面的時候生成一個 token 在 form 裏而且在 Server 端存儲這個 token,提交表單的時候先檢查 Server 端有沒有這個 token,若是有則說明是第一次提交表單,而後把 token 從 Server 端刪除,處理表單,redirect 到 result 頁面,若是 Server 端沒有這個 token,則說明是重複提交的表單,不處理表單的提交。 java


result.htm

Result: ${result!}

user-form.htm

在 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>




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("重複提交表單");
        }

        // 正常提交表單,刪除 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";
    }
}




測試二

  1. 訪問 http://localhost/user-form
  2. 點擊 Update User,表單成功提交後被重定向到 result 頁面
  3. 刷新 result 頁面,表單沒有被重複提交,實現了防止表單重複提交的功能
  4. 點擊後退按鈕回到表單頁面,點擊 Update User,由於 token 不存在了,程序拋出異常,防止了表單的重複提交(顯示異常頁面不是最好的辦法,更友好的作法是顯示一個表單重複提交提示頁面)

3. 使用 SpringMVC 攔截器生成和驗證 token

思考一下,爲了給 user-form 增長 token,在處理 user-form 的方法裏新加了不少代碼,若是有 10 個 form, 100 form 都要使用 token 的機制呢?難道要去每一個 form 處理的方法裏都加上上面的那麼多代碼嗎?上面 token 使用的是 UUID,若是要改爲 static 類型的整數,每次生成時都加 1 呢? token 存儲在 session 裏,項目進行到必定的時候要決定存儲在第三方緩存如 Redis 裏呢?每次需求的變動都要修改全部 form 的處理方法? 工做量也太大了,誰遇到這樣的問題都會抓狂,難怪招聘裏着重強調:不準打項目經理! spring

幸虧 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 的相關代碼。 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

攔截器 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 {

    }
}




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>




測試三

  1. 訪問 http://localhost/user-form
  2. 點擊 Update User,表單成功提交後被重定向到 result 頁面
  3. 刷新 result 頁面,表單沒有被重複提交,實現了防止表單重複提交的功能
  4. 點擊後退按鈕回到表單頁面,點擊 Update User,由於 token 不存在了,程序拋出異常,防止了表單的重複提交(顯示異常頁面不是最好的辦法,更友好的作法是顯示一個表單重複提交提示頁面)
經過 SpringMVC 攔截器增長 token 的機制,
  • 想改變 token 生成策略? 修改 TokenValidator
  • 想改變 token 的存儲策略? 修改 TokenValidator
  • 想給 form 增長 token 校驗? 修改 spring-mvc.xml 攔截器的配置
  • 想把 form 的 token 校驗刪除? 修改 spring-mvc.xml 攔截器的配置
  • 不須要修改任何 form 處理的方法,泰山崩於前而色不變,風波驟起而泰然處之,項目經理好像也沒那麼可恨了
提示:

Token 的存儲須要考慮過時時間,不然訪問 10 萬次 user-form 頁面,生成 10 萬個 token 而不提交表單,token 一直不會被刪除,會形成很大的資源浪費。 session

Token 應該寫入到 form 的隱藏域,爲了直觀,上面咱們寫入了普通的 input 中:
<input type="hidden" name="token" value="${token!}"> mvc

這裏使用的是 SpringMVC 的攔截器生成和校驗 token,固然也可使用 Servlet 的 Filter 等技術實現。

重複提交表單時不該該直接把異常顯示給用戶,可使用 SpringMVC 的異常處理機制,不一樣的頁面顯示不一樣異常的友好信息

相關文章
相關標籤/搜索