開発のきっかけ
先日、視覚的な対人確認ソリューションに関する2つの記事を読んだ。1つは、『DDJ』(Dr. Dobb's Journal)2003年11月号に掲載されたPaul Tremblettの「Java 2D & Web Pages」という記事だ。この記事では、動的データを簡単に提供するための方法として、サーバーサイドJava 2DをWebアプリケーションコンポーネントに組み込むという手法を紹介していた。もう1つは、『Scientific American』2003年11月号に掲載されたLee Brunoの「Baffling the Bots」という記事である。私はこれらの記事を読んで、Webフォームを自動登録やスパムボットから保護するための.NETプラットフォーム用サンプルアプリケーションを開発しようと思い立った。
次に紹介するのは、ボットによる自動投稿を防止するための画像生成プログラムである。Webアプリケーションに対してスパム、自動登録、フォーラムへの自動投稿、案内広告の自動投稿などが頻繁に行われる場合は、人間だけが認識できるような視覚的なCAPTCHAをWebフォームに組み込むとよい。
独自CAPTCHAアプリケーションの開発
ASP.NETには、画像の生成・操作を行うための機能が含まれている。以前のASP 3.0とは異なり、ASP.NETは.NET Frameworkを最大限に活用することができ、外部ライブラリを使わずに動的な画像処理を行うことができる。System.Drawingクラスを使用すると、メモリ内で画像を生成し、その画像を出力ストリームを通じてブラウザ上に描画できる(このプロセスについては、次の分離コードファイル(OntheFly.aspx.vb)を参照のこと)。
このサンプルの目的は、クエリ文字列を通じて渡されたパラメータに基づいて、画像を動的に生成することである。ビットマップ画像を描画するには、HTTPContextを使用する。これは、System.Drawing.Bitmapオブジェクトをインスタンス化するためのブラウザのストリームハンドラである。BitmapコンストラクタはpixelFormatをパラメータとして受け取り、110×50ピクセルの描画用キャンバスを返す。このプロセスはすべてメモリ内で行われ、後でpageContextがフラッシュされるときにブラウザ上に描画されるという点に注意してほしい。次に、ラスタ(つまりビットマップメモリマップ)への描画を行うグラフィックオブジェクトが必要になる。このグラフィックオブジェクトのインスタンスはFillRectangleとDrawStringを呼び出して、キャンバス上に矩形とパラメータテキストを描画する。このキャンバスは、出力ストリームに対してgif MIMEタイプとしてフラッシュされる。実際に出力した画像の例を次の図に示す。

'// Filename: onthefly.aspx.vb '// Author: Adnan Masood '// Last updated: 12/07/03 '// Importing the libraries for drawing operations Imports System Imports System.Drawing '// Class OntheFly declaration PublicClass OntheFly : Inherits System.Web.UI.Page '//Page_Load event overriding PrivateSub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs)HandlesMyBase.Load '//Getting HTTPContext Dim pageContext As System.Web.HttpContext pageContext = System.Web.HttpContext.Current '//Reading the parameter Dim strText AsString = Request("param") '//Create the memory map Dim raster As Bitmap Dim pixFormat As Imaging.PixelFormat pixFormat = Imaging.PixelFormat.Format32bppArgb '// Create a memory image with dimensions 110x50 raster = New Bitmap(110, 50, pixFormat) Dim graphicsObject As Graphics graphicsObject = Graphics.FromImage(raster) '// Instantiate object of brush Dim objBrush As SolidBrush = New SolidBrush(Color.Yellow) '//Creating a font object with Arial size 10 and bold '//public Font(string, float, FontStyle) Dim objFont As Font = New Font("Arial", 10, FontStyle.Bold) '//Set the background half rectangle blue '//Method Signature: FillRectangle (const Brush *brush, INT x, INT y, INT width, INT height); graphicsObject.FillRectangle(New SolidBrush(Color.Blue), 0, 0, 55, 50) '//Writing the string on image graphicsObject.DrawString(strText, objFont, objBrush, 0, 0) '//Flushing the graphics object graphicsObject.Flush() '//Setting the response header content MIME type pageContext.Response.ContentType = "image/gif" '//Saving the file to output stream to be displayed in browser raster.Save(pageContext.Response.OutputStream, Imaging.ImageFormat.Gif) '//Flushing to the Response pageContext.Response.Flush() EndSub EndClass
OntheFly.aspx.vbは、OntheFly.aspxの分離コードファイルである。OntheFly.aspxには次の行が含まれている。
<%@ Page Language="vb" Src="OntheFly.aspx.vb" Inherits="OntheFly"%>
図1.20は、出力した画像の例である。パラメータに何か文字列を指定すると、このコードによって画像が生成され、ブラウザ上に描画される。バイナリファイルの中に画像として描画されたテキストは、HTMLコードとは違って簡単に読み取ることができないため、スパムボットで何が書かれているかを判読するためには、特殊なテクニックが必要になる。この単純なサンプルを応用すれば、フォーラムやブログなどの電子メールアドレス保護機能を実現できる。キャッシュ処理を工夫すれば、パフォーマンスが大きく低下することもない。

辞書ベースのCAPTCHAの実装
今度のサンプルは、本格的なCAPTCHAプログラムの機能を備えているため、先ほどのものより少し長く複雑になっている。このプログラムでは、辞書ベースのCAPTCHAを動的に生成し、画像内のテキストの入力を求めて、相手が人間かどうかの確認を行う。
このプログラムの流れは次のとおりである。ある架空のフォーラムのログインフォームで、ユーザーが人間であるかどうかを確認するために、フォーム下部の画像内のテキストを入力するようユーザーに要求する。プログラムは、用意されているさまざまな画像の中から背景用画像を動的に選択し、「OGDEN's Basic English」辞書から選択した任意の単語と組み合わせる。OCRでの認識を難しくするために、字体も1文字ごとに変更する。以降では、このプログラムの動作を簡単に説明し、完全なコードリストと辞書データベースファイルを紹介する。

ユーザーが間違った単語を入力すると、次の画面に進むことができず、エラーメッセージが表示される。

ボットが再試行しようとすると、今度は別の背景と別の単語を使用した新しい画像が表示される。
アプリケーション設計者は、再試行を何回まで認めるかを検討しなければならない。ユーザーが再試行した回数を記録しておき、このプログラムがセキュリティ対策ではなく一般ユーザーに対する障壁になっていないかどうかを確認する必要がある。また、障害者のために、代替策(音声を使用するなど)も用意しておいた方がよい。

ユーザーが正しい単語を入力した場合、このプログラムはユーザーのアクセスを許可する。その前に別の処理を挿入することもできる。

それでは、このアプリケーションの内容を詳しく見ていくことにしよう。
次の図は、OGDENのBasic English辞書をデータベース化したものである。各列に同じアルファベットから始まる単語が収録されており、ここからCAPTCHA画像に組み込む単語をランダムに選択できる。

このアプリケーションには次の3つのメインファイルがある。
captchaForm.aspx
このファイルには、辞書内の単語をランダムに選択してcaptcha.aspxに渡すためのコードが含まれている。captcha.aspxの分離コードファイルであるcaptcha.aspx.vbは、渡された単語を処理して、ブラウザ上に画像を描画する。このファイルには、フォームを処理するコードと、ユーザーが入力したCAPTCHAの値を検証するコードも含まれている。さらに、dictionary.mdbへの接続、ランダムな単語の読み取り、セッションの処理を行うコードも含まれている。
captcha.aspx.vb
これはcaptcha.aspxの分離コードファイルである。画像生成の処理はこのファイルが担当する。このファイルはbackgroundsフォルダ内の背景パターンをランダムに選択し、captchaform.aspxから渡されたテキスト文字列を書き込む。さらに、機械による読み取りを難しくするために、字体とサイズをランダムに選択する。
dictionary.mdb
このAccessデータベースリポジトリには、OGDENのBasic English辞書の項目が格納されている。
各所のコメントを追っていけば、このコードでどんな処理をしているかはすぐにわかるだろう。
<!-- '// Filename: captchaForm.aspx '// Author: Adnan Masood '// Last updated: 12/08/03 --> <%@ Import Namespace="System" %> <%@ Import Namespace="System.Data" %> <%@ Import Namespace="System.Data.OleDb" %> <%@ Page Language="C#" debug=true%> <HTML> <head> <script runat="server"> // Pageload event void Page_Load(object Source, EventArgs E) { // If form is not posted than assign the selectWord return to session if (!IsPostBack) Session["param"] =selectWord(); // Else check if session's value is same as that of Captcha's entered value // in the form and assign appropriate message to innertHtml of label else { if (String.Compare(Convert.ToString(Session["param"]),Convert.ToString(Request.Form["CAPTCHA"]))==0) lblstatus.InnerHtml= "Thanks for Registeration with our Forums"; else lblstatus.InnerHtml= "Error Occured! Value of Image is not correct, " + "please <a href=captchaform.aspx> re-enter </a>"; } } // Select word method // Return type String of random word from dictionary // Dictionary is based on OGDEN's BASIC ENGLISH // http://ogden.basic-english.org/basiceng.html public String selectWord () { // The Connection string referencing the MDB file String ConnectionString = "Provider=Microsoft.Jet.OleDb.4.0;Data Source="+ Server.MapPath("dictionary.mdb") + ";"; // Datareader object OleDbDataReader objReader; // Creating an array of 26 characters (alphabets in dictionary database as columns) char[] columns = newchar[26]; // Adding the column names in the array // uses the ASCII character conversion for selecting values // from A- Z for (int a=65; a<65+26; a++) columns[a-65] = (char)a; // Query String for selecting a random column from spelling list database String QuerySQL = "SELECT " + columns[(new Random().Next(26))] + " FROM spellList"; // Opening the connection OleDbConnection objConn = new OleDbConnection(ConnectionString); // Creating new command object OleDbCommand objCmd = new OleDbCommand(); // Assigning command text objCmd.CommandText = QuerySQL; // Assigning the connection to command object connection attribute objCmd.Connection = objConn; // Instantiating a random class object Random randomSeed = new Random(); // Creating a random seed selector int randomSeedSelector=0; // An string character with maximum capacity for dictionary column String[] selectedIndex = new String[700]; String str = ""; // This code segment opens the connection and read the dictionary try { objConn.Open(); objReader = objCmd.ExecuteReader(); while (objReader.Read()) { str = objReader.GetValue(0).ToString(); if (str.Length != 0) { selectedIndex[randomSeedSelector] =str; randomSeedSelector++; } }// Ends While str = selectedIndex[randomSeed.Next(randomSeedSelector)]; } // Ends Try catch (Exception Err) { // The Error Catching operations } finally { objConn.Close(); } // Returns the selected string return (str); }// ends function </script> </head> <body> <% //Checking for postback for initial processing of form. if (!IsPostBack) { %> <form runat="server"> <P align="left"><STRONG><FONT size="5">Signup for Forums</FONT></STRONG></P> <P> <TABLE id="Table1" height="200" cellSpacing="1" cellPadding="1" width="704" border="1"> <TR> <TD> <P>Name</P> </TD> <TD><INPUT id="Text1" type="text" name="name"></TD> </TR> <TR> <TD>Email Address</TD> <TD><INPUT id="Text2" type="text" name="email"></TD> </TR> <TR> <TD> <P><IMG src="http://localhost/captcha/example2/captcha.aspx"></P> <P>Enter the text above into the box on right.</P> </TD> <TD><INPUT id="Text3" type="text" name="CAPTCHA"></TD> </TR> <TR> <TD> <INPUT id="Submit1" type="submit" value="Submit" name="Submit1"> <INPUT id="Reset1" type="reset" value="Reset" name="Reset1"> </TD> <TD></TD> </TR> </TABLE> </P> </form> <% } //Setting lable's status accordingly else { %> <td id="lblstatus" runat="server"></td> <% }%> </body> </HTML>
'// Filename: captcha.aspx.vb '// Author: Adnan Masood '// Last updated: 12/08/03 Imports System Imports System.Drawing Imports System.String Imports System.Drawing.Image namespace com.axisebusiness.OnlineImage '// Importing the libraries for drawing operations '// Class OntheFly declaration PublicClass CAPTCHA : Inherits System.Web.UI.Page '//Page_Load event overriding PrivateSub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs)HandlesMyBase.Load '//Getting HTTPContext Dim pageContext As System.Web.HttpContext pageContext = System.Web.HttpContext.Current '//Reading the parameter from session this time Dim strText AsString = Session("param") '//Create the memory map Dim raster As Bitmap Dim pixFormat As Imaging.PixelFormat pixFormat = Imaging.PixelFormat.Format32bppArgb '// Select an memory image from file of 290x80px '// in the backgrounds folder named backX.jpg Dim graphicsObject As Graphics Dim imageObject as System.Drawing.Image =
System.Drawing.Image.FromFile(Server.Mappath("backgrounds\\back" &
new Random().Next(5) & ".jpg")) '// Creating the raster image object raster = new Bitmap (imageObject) '//Creating graphics object graphicsObject = Graphics.FromImage(raster) '// Instantiate object of brush with black color Dim objBrush As SolidBrush = New SolidBrush(Color.Black) Dim objFont As Font dim a asInteger, myFont asString, str asString '//Creating an array for most readable yet cryptic fonts for OCR's '// This is entirely up to developer's discretion dim crypticFonts(11) crypticFonts (0) = "Arial" crypticFonts (1) = "Verdana" crypticFonts (2) = "Comic Sans MS" crypticFonts (3) = "Impact" crypticFonts (4) = "Haettenschweiler" crypticFonts (5) = "Lucida Sans Unicode" crypticFonts (6) = "Garamond" crypticFonts (7) = "Courier New" crypticFonts (8) = "Book Antiqua" crypticFonts (9) = "Arial Narrow" crypticFonts (10) = "Estrangelo Edessa" '//Loop to write the characters on image '// with different fonts. For a=0 to strText.Length-1 myFont = crypticFonts(new Random().Next(a)) objFont = New Font(myFont, 20, new FontStyle().Bold) str = strText.substring(a, 1) graphicsObject.DrawString(str, objFont, objBrush, a*20, 35) graphicsObject.Flush() next '//Flushing the graphics object graphicsObject.Flush() '//Setting the response header content MIME type pageContext.Response.ContentType = "image/gif" '//Saving the file to output stream to be displayed in browser raster.Save(pageContext.Response.OutputStream, Imaging.ImageFormat.Gif) '//Flushing to the Response pageContext.Response.Flush() EndSub EndClass Endnamespace
Captcha.aspxファイルには次のように記述されている。
<%@ Page Language="C#" Src="Captcha.aspx.vb" Inherits="com.axisebusiness.OnlineImage.Captcha"%>
お気付きのとおり、このコードは1つのプログラム内で複数の言語(この場合はC#とVB.NET)を使用する方法のサンプルでもある。このプログラムで生成したCAPTCHAは絶対に破られないとは言えないが、通常のスパムボットへの対策としてはかなり有効である。

HandlesMyBase.Load