LiftでJCaptcha

Captcha(画像認証)をLiftで実装する方法。Liftでバイナリデータをレスポンスする方法と、JCaptchaの使い方で意外と悩んだのでメモ。

JavaのCaptchaライブラリのサンプルでは、画像を返すServletをサンプル実装としていることが多いですが、Liftのような独自のURLマップを持つフレームワークだといろいろ面倒。
ここでは、Liftらしい方法でCaptchaを実装します。

Liftでバイナリデータをレスポンスする方法

LiftWiki:StreamingResponse
に書いてありますが、これだけではなんのことかわからない。。

あるURLに対してあるレスポンスを返すには、
LiftRules.dispatch.appendを使います。

class Boot{
  LiftRules.dispatch.append {
    case Req( "captcha" :: Nil, _, _ ) => Captcha.challenge _
  }
}

LiftRules.dispatch.appendの引数の型は、

 PartialFunction[Req, () => Box[LiftResponse]]

URLパターンのcaseに対して、"Box[LiftResponse]を返す関数"を指定してやればよい。LiftResponseのサブタイプにはいろいろ便利ラッパがあるのでチェック。バイナリデータを応答するには、StreamingResponse(BasicResponseのサブタイプ)を使う。
(後述のCaptcha#challengeがそのサンプル)

※ちなみにこのDispatchingの仕組みを使った、RESTの実装方法がまとまっているので要チェックです。
LiftWiki:REST Web Services

Captchaの実装

JCaptchaを利用した。
JCaptcha: 5 minutes application integration tutorial

但し、バージョン1.0付属のCaptchaエンジンでは(泣けるほど)ろくな画像を生成してくれないので、バージョン2.0-alphaを使う。
JCaptcha:Simple Servlet Integration documentation
の下の「Jcaptcha 2.0 jars」からダウンロード。HotmailEngineか、GmailEngineあたりが見やすいのではないでしょうか。


上記の情報を総合して、Lift+Scalaを使った、Captchaの実装はこんな感じになります。

import com.octo.captcha.service.image._
import com.octo.captcha.service.captchastore._
import com.octo.captcha.engine.image.gimpy._
import com.sun.image.codec.jpeg._
import java.io._
import java.util.Locale._
import net.liftweb.common._
import net.liftweb.http._

object Captcha {

  lazy val service =
    new DefaultManageableImageCaptchaService(
      new FastHashMapCaptchaStore, new GmailEngine,
      180,100,75)

  def getCaptcha:Box[ Array[Byte] ] = S.session match {
   case Full(session)=>
    val jpeg = new ByteArrayOutputStream
    val id = session.uniqueId
    val challenge = service.getImageChallengeForID(id, JAPAN)
    JPEGCodec.createJPEGEncoder(jpeg).encode(challenge)
    Full( jpeg.toByteArray )
   case _ => Empty
  }

  //Captchaを生成してレスポンス
  def challenge: Box[LiftResponse] = {
   getCaptcha match{
    case Full( capture ) =>
     val headers = ("Content-type" -> "image/jpeg") ::
      ("Cache-Control" -> "no-store") ::
      ("Pragma", "no-cache") ::
      ("Expires", "0" ) ::
      ("Content-length" -> capture.length.toString) :: Nil

     Full(StreamingResponse( new java.io.ByteArrayInputStream(capture),
      () => {}, capture.length, headers, Nil, 200) )
    case _ => Empty
   }
  }

  //ユーザー入力が正しいかチェック
  def validate(response:String):Boolean = S.session match{
   case Full(session) =>
    service.validateResponseForID(session.uniqueId,response)
      .asInstanceOf[Boolean]
   case _ => false
  }

}

コメント

コメントしてください
お名前:
入力しなければ「匿名さん」。20字以内。

メール:
入力しても表示しません

URL:
入力すればリンクが貼れます


コメント: