ブラウザバックによる二重実行の注意

ヘッダー広告
スポンサードリンク

今回の記事はブラウザバックで発生するWebアプリケーションの問題について紹介する記事になります。

Webアプリケーションを開発・運用していると、ブラウザバックによって様々な問題が頻繁に発生します。
今回はそのブラウザバックによって発生する問題のうちの一つである『ブラウザバックによる二重実行』についてのご紹介をさせていただき、Webアプリケーション開発時に意識していただければと思います。

ブラウザバックによる二重実行

ブラウザバックとは

念のためブラウザバックが何かについてご説明させていただきます。

ブラウザバックとは、ブラウザ自体に付いている戻るボタンをクリックして、前の画面に遷移する行為のことを言います。(ブラウザ戻るともいいます)

このボタンをクリックすることや、右クリック戻るをクリックすることで、前の画面に戻ることが出来ます。


このボタンはWebサイトを閲覧している時には大変便利でよく使う機能だと思うのですが、Webアプリケーションを開発している場合には問題になることが多い機能です。
Webアプリケーションの開発者は戻るボタンを画面自体につけて戻らせたいのですが、ブラウザ自体に戻る機能が存在しているせいで、想定していない画面から戻られてしまい、予期しないエラーや問題となることが多々あります。

しかもブラウザごとにブラウザバックの挙動が異なったり、キャッシュ有無によって動作が変わったりと結構曖昧な動作をするので厄介です。

今後ブラウザバックについては色々と記事にしたいと考えているのですが、まず第一弾としてこのブラウザバックによる二重実行についてをご紹介させていただき、ブラウザバックの恐怖を知っていただければと思います。

二重実行再現

状況を再現するために、振込を行うための画面を作成しました。(シンプルで申し訳ないです)

振込入力画面で、振込先の名前・振込金額を入力してPOST送信をクリックすると、振込完了画面へ遷移して、transactionテーブルに入力した内容が反映されます。

1.振込入力画面

2.振込完了画面

3.振込完了後DB状態

その後、振込完了画面から別の画面(今回の場合はyahoo)へ遷移します。

4.別画面へ遷移

そして、ここでブラウザバックして振込完了画面へ戻ります。
振込完了画面へ戻るとページが存在していないというメッセージが出力されます。
ここで『最新の情報に更新』を実行すると、フォームを再送信するかというメッセージが出力されるため、『再試行』を選択します。

5.ページが存在しない画面

ここでF5などで更新を行う。

6.フォーム再送信確認

フォームの再送信確認メッセージに『再試行』を選択する。

すると、先ほどの振込完了画面へ遷移します。
しかし、ここでは単純に画面を戻っただけでなく、transactionテーブルまで先ほどと同じ内容で更新されてしまうのです。
つまり同一内容で二回実行されたことと同じ状態となってしまいます。

7.ブラウザバック後の振込完了画面

8.ブラウザバック後のDB状態

このDBの結果は佐藤太郎さんに10,000円を2回振り込んだというものが反映されています。
しかしこのブラウザの動きはWebサイトの閲覧者側からみると、単純に前の画面に戻っただけであり、同じ内容で振り込んだとは思っていないのです。
そのためWebアプリケーションの管理者側と利用者側で認識の相違が発生してしまい、問題となってしまうことがある動作となります。

これが私がお伝えしたかったブラウザバックによる二重実行の挙動です。

なぜ二重実行されてしまうのか

さてなぜブラウザバックすることで二重実行されてしまうのかです。

まずブラウザバックした時の挙動を説明したいと思います。

ブラウザバックを行うと単純に前の画面に戻るだけと考えがちなのですが、実はブラウザバックで戻る画面がキャッシュされているかどうかで挙動が異なります。

戻る画面がキャッシュされている場合には、単純に前の画面を表示するだけですが、キャッシュされていない(キャッシュ抑止されている)場合には、前の画面を呼び出すためにリクエストした内容が再度実行されます。

説明資料参考

この遷移の場合には、C画面からブラウザバックしB画面に戻る際には、リクエスト1が再度実行された結果としてB画面が表示されるという動きをします。

そのため今回ご紹介したような結果となってしまうというわけです。

しかし、HTTPリクエストメソッドによってはブラウザバック時に注意メッセージを出力してくれます。
GETメソッドは参照用の画面。POSTメソッドは更新用の画面に使われることが一般的であるため、POSTメソッドで遷移した先の画面へブラウザバックしようとした際には、ブラウザバック後の再リクエスト前に画面が存在しないメッセージが出力され、F5更新した際にもフォームの再送信確認メッセージが出力してくれます。

そのため、今回のように何か処理を実行する画面の大半はPOST実行しているはずなので、ブラウザバックしても確認メッセージが出力してくれるはずです。
ですが、Webサイトの閲覧者はWebの理解がない方がほとんどですので、このようにメッセージが出力してもそのまま実行してしまうという方が多いと思いますので、Webアプリケーションを開発している側が二重実行に対して対策を実施し、二重実行させないようにする必要があります。

二重実行の対策

二重実行の対策方法はいくつかありますが、一番効果的なのはリクエスト実行時に、DBチェックを入れて二重実行を回避するというものになります。

何か必ず一意になる値がある場合には、インサート実行前にその値を持つレコードが既存のテーブル内に存在しないかをチェックして、存在する場合にはエラーを出力するというロジックを入れるのが一番効果的な対策になります。

以下は二重実行チェックを入れた後に、先ほどの再現と同様にブラウザバックしてF5更新した後の画面になります。


データの重複が発生しました。というメッセージが出力され、完了画面へは遷移しない。

今回ご紹介した再現の状況では、TransNoを主キーとしていたのですが、POST送信をしたときにクリックした日時を採番してDBにインサートしていましたが、最初に振込入力画面を表示した際にTransNoを採番し、ブラウザバックして再度実行した場合には、TransNoが必ず同じになるため、二重実行チェックにかかってエラーが出力されるというロジックに変更しました。

このように何かDBにインサートしたり、更新したりする際には二重実行チェックを入れることを忘れずに意識していただければと思います。
この問題はWebアプリケーションを開発して運用している人は発生する頻度の高い問題だと言えますので、気にしてください。

今回の記事は以上となりますが、最後に今回のソースコードも載せておきます。
※対策実行後のコードです。ただし二重重複チェックはインサート時にエラーが発生したらになっていますが、本来はSELECTでチェックする方が正しい姿です。

今回のコード(参考)

jsp

振込入力画面:Transfer.jsp
<%@ page language="java" contentType="text/html;charset=Windows-31J" %>
<%@ page pageEncoding="Windows-31J" %>
<%@ page import="java.text.SimpleDateFormat" %>
<%@ page import="java.util.Calendar" %>

<HTML>

<HEAD>

<TITLE>振込</TITLE>

<style type="text/css">
    .block1 {
	display: inline-block;
	width: 5em
    }
</style>

</HEAD>

<BODY>

<%

    String ErrMsg = (String)request.getAttribute("ErrMsg");

    if (ErrMsg != null) {
%>
<strong><font color=#ff0000><%= ErrMsg %></font></strong>
<%
    }
%>

<%
    Calendar date = Calendar.getInstance();

    SimpleDateFormat sdf = new SimpleDateFormat("MMddHHmmss");
    String TransNo = sdf.format(date.getTime());
%>


<h1>振込</h1>

<form Action="TransferServlet" method="post">
    <input type="hidden" name ="TransNo" value= <%= TransNo%> />
    <span class="block1">名前:</span><input type="text" name ="userName" /><br>
    <span class="block1">振込金額:</span><input type="text" name ="Amount" /><br>
    <input type="submit" name="submit" value="POST送信" />
</form>

</BODY>

</HTML>

振込完了画面:Transfer_Finish.jsp
<%@ page language="java" contentType="text/html;charset=Windows-31J" %>
<%@ page pageEncoding="Windows-31J" %>

<HTML>

<HEAD>

<TITLE>振込完了</TITLE>

<style type="text/css">
    .block1 {
	display: inline-block;
	width: 5em
    }
</style>

<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Cache-Control" content="no-cache">
<meta http-equiv="Expires" content="0">

</HEAD>

<BODY>

<%
  String userName = request.getParameter("userName");
  String Amount = request.getParameter("Amount");
%>


<h1>振込完了</h1>

以下の内容で振込しました。<br>
<span class="block1">名前:</span><%= userName %><br>
<span class="block1">振込金額:</span><%= Amount %><br>

</BODY>

</HTML>

javaサーブレット

メインのコード:TransferServlet.java
package webapp;

import java.io.IOException;

import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import dao.TransactionDao;

public class TransferServlet extends HttpServlet {
  @Override

    protected void doPost( HttpServletRequest req, HttpServletResponse res ) throws ServletException, IOException{

	  	int SqlResult;

        res.setContentType("text/html; charset=Windows-31J");

	  	TransactionDao Dao1 = new TransactionDao();

	  	Dao1.setTransNo(Integer.parseInt(req.getParameter("TransNo")));
	  	Dao1.setName(req.getParameter("userName"));
	  	Dao1.setAmount(Long.valueOf(req.getParameter("Amount")));

	  	SqlResult = Dao1.insert001();
	  	String disp;

	  	if (SqlResult == 0) {
	  		req.setAttribute("ErrMsg","データの重複が発生しました。");
	  		disp = "/Transfer.jsp";
	  	}else{
	  		disp = "/Transfer_Finish.jsp";
	  	}

	    RequestDispatcher dispatch = req.getRequestDispatcher(disp);
	    dispatch.include(req, res);

    }
}

DB操作:TransactionDao.java
package dao;

import java.sql.SQLException;

public class TransactionDao  {

	private int TransNo = 0;
	private String Name = "";
	private long Amount = 0;

	private static final String insert001 = ""
			+ "insert into "
			+ "  transaction "
			+ "values"
			+ "  ("
			+ "    TRANS_NO,"
			+ "    'TRANS_NAME',"
			+ "    TRANS_AMOUNT"
			+ "  );";
    public int insert001() {

	    String sql = insert001;
	    sql = sql.replaceAll("TRANS_NO",Integer.toString(TransNo));
	    sql = sql.replaceAll("TRANS_NAME",Name);
	    sql = sql.replaceAll("TRANS_AMOUNT",Long.toString(Amount));

	    try {
	    	return DBManager.simpleUpdate(sql);
	    }catch (SQLException e) {
	    	return 0;
	    }
    }

    public int getTransNo() {
    	return TransNo;
    }
    public void setTransNo(int TransNo) {
    	this.TransNo = TransNo;
    }

    public String getName() {
    	return Name;
    }
    public void setName(String Name) {
    	this.Name = Name;
    }

    public Long getAmount() {
    	return Amount;
    }
    public void setAmount(Long Amount) {
    	this.Amount = Amount;
    }

}

本日はここまで!
フッター広告

スポンサードリンク



シェアする

  • このエントリーをはてなブックマークに追加

フォローする