2015-06-02

以 SimpleCaptcha 實做驗證碼

驗證碼?

就是防止機器人送出表單的驗證圖片,這是目前辨識真假人最常見的作法。

驗證流程

  1. 使用 <img src="..."/> 產生驗證圖片,注意 src 是指向 Java Servlet,用以動態產生驗驗證圖片,而不是一張靜態的圖片。
  2. 在 web.xml 設定產生驗證圖片的 <servlet/> 與 <servlet-mapping/>
  3. 撰寫 Java Servlet,每次呼叫 Java Servlet 時,除產生驗證圖片塞到 response 以外,還要將答案寫到 session 裡,供事後比對。
  4. Form submit 到後台時,左手拿使用者輸入的驗證碼,右手拿 session 裡的答案,兩相做比較即可。

貼心的功能

  1. 在網頁上加上「重新產生驗證碼」功能,只要使用 Javascript 讓 img 重新載入圖片就可以,若擔心 cache,可以每次重新整理時在網址上加上時間戳記。
  2. 在網頁輸入時,使用 Javascript 將輸入的值自動轉大寫,當然驗證值一開始就得是大寫,這可以減低傷害使用者的耐心。
  3. 送到後台比對時,先將兩邊(使用者輸入與從 session 取得的答案)值的英文字母「歐」都換成數字「零」,以及英文字母「唉」換成數字「壹」,這是避免 O / 0 與 I / 1 的誤會。

下載 SimpleCaptcha

請到官網下載,目前版本是 1.2.1,Maven repository 上的還是 1.1.1,Maven Repository 上還有個版本超前官網的 1.2.2 的同名 library,別誤會了,這可是強國人製作的,原因不明。

然後在 web.xml 加上以下設定,重起湯姆貓,就可以在 http://localhost/stickyImg 看到驗證圖片了,重新整理也可以看到每次驗證碼都不同。
<servlet>
 <servlet-name>StickyCaptcha</servlet-name>
 <servlet-class>nl.captcha.servlet.StickyCaptchaServlet</servlet-class>
</servlet>
<servlet-mapping>
 <servlet-name>StickyCaptcha</servlet-name>
 <url-pattern>/stickyImg</url-pattern>
</servlet-mapping>
很簡單?但是預設的文字/圖片真的有點不美觀。

客制 SimpleCaptcha

就是改寫 nl.captcha.servlet.SimpleCaptchaServlet,客制文字字型與大小,以及背景圖。
public class EasySimpleCaptchaServlet extends SimpleCaptchaServlet {

  // 以下四個參數相互影響,圖大則字型大,字多字大則圖大
  private int width = 60;
  private int height = 25;
  private int length = 4;
  private int fontSize = 20;

  // 背景圖,放在 classpath 裡
  private String backgroundImg = "CaptchaBackground.jpg";

  @Override
  public void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

    // 建立 Captcha 物件
    Captcha captcha = this.buildCaptcha();

    // 將 Captcha 物件裡的圖片寫入 HttpServletResponse
    CaptchaServletUtil.writeImage(resp, captcha.getImage());

    // form submit 後再用 Captcha.NAME 取得 Captcha 物件,然後呼叫 getAnswer() 來比值
    req.getSession().setAttribute(Captcha.NAME, captcha);
  }

  private Captcha buildCaptcha() {

    // 文字顏色
    List<Color> colorList = new ArrayList<Color>();
    colorList.add(Color.BLACK);

    // 文字字型
    List<Font> fontList = new ArrayList<Font>();
    fontList.add(new Font(Font.SERIF, Font.BOLD, this.fontSize));

    // 設定長寬、設定文字產生器、設定文字顏色與字型、設定背景
    Captcha captcha = new Captcha.Builder(this.width, this.height).addText(new EasyTextProducer(this.length),
        new DefaultWordRenderer(colorList, fontList)).addBackground(new EasyBackgroundProducer()).build();

    return captcha;
  }

  /**
   * 設定背景 - 以一張圖片作為背景,並隨機使用背景圖片的某一位置
   */
  private final class EasyBackgroundProducer implements BackgroundProducer {

    /**
     * 不知道作用,不用改
     */
    @Override
    public BufferedImage addBackground(BufferedImage bi) {
      int width = bi.getWidth();
      int height = bi.getHeight();
      return this.getBackground(width, height);
    }

    @Override
    public BufferedImage getBackground(int width, int height) {
      try {

        // 讀入背景圖
        Image image = ImageIO.read(this.getClass().getClassLoader().getResourceAsStream(backgroundImg));
        BufferedImage buffered = (BufferedImage) image;

        // 可用的 x 與 y 值
        int x = buffered.getWidth() - width;
        int y = buffered.getHeight() - height;

        // 隨機產生
        x = (int) (Math.random() * x);
        y = (int) (Math.random() * y);

        // 防呆
        x = Math.max(x, 0);
        y = Math.max(y, 0);

        // 切出需要的背景圖
        return buffered.getSubimage(x, y, width, height);
      }
      catch (IOException e) {
        e.printStackTrace();
      }
      // 若出意外,挺多沒有背景圖
      return null;
    }
  }

  /**
   * 文字產生器 - 只使用 數字與大寫英文字母
   */
  private final class EasyTextProducer implements TextProducer {

    private int length;

    public EasyTextProducer(int length) {
      // 設定驗證碼長度
      this.length = length;
    }

    @Override
    public String getText() {
      StringBuilder sb = new StringBuilder();
      for (int i = 0; i < this.length; i++) {
        int n = (int) (Math.random() * 36);
        if (n <= 9) {
          sb.append(String.valueOf(n));
        }
        else {
          n -= 10;
          n += 97;
          sb.append((char) n);
        }
      }
      return sb.toString().toUpperCase();
    }
  }

  public static void main(String[] args) throws IOException {
    Captcha captcha = new EasySimpleCaptchaServlet().buildCaptcha();
    File outputfile = new File("image_" + new Date().getTime() + ".jpg");
    ImageIO.write(captcha.getImage(), "jpg", outputfile);
  }
}
不解釋,看起來好多了。
// 重新產生驗證圖片
$('#stickyImg').attr('src', 'stickyImg?_v=' + new Date().getTime()); 

// 自動轉大寫
$('input[name=verifyCode]').keyup(function(){
 $(this).val($(this).val().toUpperCase());
});
---
---
---

沒有留言:

張貼留言