Freewind @ Thoughtworks scala java javascript dart 工具 编程实践 月结 math python english [comments admin] [feed]

(2013-01-01) 5. 创建验证码

广告: 云梯:翻墙vpn (省10元) 土行孙:科研用户翻墙http proxy (有优惠)

(本文译者是wersion)

创建验证码

因为任何人都可以提交一个评论到我们的博客引擎,我们应该稍微保护它避免自动垃圾邮件。保护表单防止这情况的一种简单的方法就是添加一个验证码 图像(captcha image)。

生成验证码图像

我们将开始看到如何使用Play简单地生成一个验证码图像。基本说来,我们仅仅会使用其他的行为,除了将返回一个二进制流而不是我们目前已经返回的HTML响应之外。 既然Play是一个全堆栈型的web框架,我们尝试把内置构造包含到web应用的大多数典型的需求中;生成验证码就是应用中的一种。我们可以使用play.libs.Images 辅助包简单地生成一个验证码图像,然后把它写入http响应中。

如往常一样,我们将从一个简单的实现开始。添加captcha 行为到应用控制器中:

public static void captcha() {
    Images.Captcha captcha = Images.captcha();
    renderBinary(captcha);
}

注意,我们可以直接地传递captcha对象到renderBinary()方法,因为这Images.Captcha类实现了java.io.InputStream。

### 别忘了导入 `play.libs.*`.

现在添加一个新路由到/yabe/conf/routes文件:

GET     /captcha                                Application.captcha

然后通过打开[http://localhost:9000/captcha尝试下这个验证码行为。](http://localhost:9000/captcha尝试下这个验证码行为。)

[![image](/user_images/1315-1.png "image")](/user_images/1315-1.png)

每次刷新,它将会生成一个随机文本。

### 我们如何管理它的状态?

直到现在它还是简单的,但是最复杂的部分马上就要来了。为了验证这验证码,我们需要把已写入到这个验证码图像某处的随即文本保存起来,然后在表单提交的时候再检查下。

当然,我们仅能在图像生成的时候把文本放入到用户会话中,然后等以后再获取它。但是这种方案有2个污点:

第一,Play 会话作为一个cookie被存储。依据结构体系,它解决了大量的问题,但是它还有很多的意义。写入会话cookie的数据都被签名了(因此用户就不能修改它们)但却没有被加密。假如我们把这个验证码的代码写入这会话中,那么任何人都可以通过读取会话cookie简单地解决它。

第二,记住,Play是一个无状态的框架。我们想在一种纯粹无状态的方式来管理这些事件。一般来说,假如一个用户用不同的验证码图像同时打开了2个不同的博客页面,这将会发生什么?我们对每个表单需要追踪它的验证码代码。

因此,解决这个问题我们需要2件事。我们将会把验证码密钥存储在服务端。因为这是些短暂的数据所以我们可以简单地使用这个play 缓冲数据(Play Cache)。除此之外,因为被缓冲的数据是有限的生命周期,所以它将会被加入多种安全机制(让我们说,一个验证码代码仅仅有效10mn)。接着,解决这代码之后,我们需要生成一个唯一id。这唯一id号将会作为一个隐藏域被添加到每个表单,然后隐式的指定一个已生成的验证码代码。

接着我们优雅地解决我们状态问题。

像这样修改验证码行为:

public static void captcha(String id) {
    Images.Captcha captcha = Images.captcha();
    String code = captcha.getText("#E4EAFD");
    Cache.set(id, code, "10mn");
    renderBinary(captcha);
}

注意,`getText()`方法把任何颜色作为参数。它将会使用这种颜色去绘画文本。 别忘了导入`play.cache.*`。

### 为评论表单添加验证码图像

现在,在显示一个评论表单之前,我们将会生成一个唯一ID。接着,我们将修改html表单通过使用它的id使一个验证码图像完整,然后添加这个ID到另外的隐藏域。 让我们重写这个 `Application.show `行为:

public static void show(Long id) {
    Post post = Post.findById(id);
    String randomID = Codec.UUID();
    render(post, randomID);
}

接着,在`/yable/app/views/Application/show.html`模板的表单:

…
<p>
    <label for="content">Your message: </label>
    <textarea name="content" id="content">${params.content}</textarea>
</p>
<p>
    <label for="code">Please type the code below: </label>
    <img src="@{Application.captcha(randomID)}" />
    <br />
    <input type="text" name="code" id="code" size="18" value="" />
    <input type="hidden" name="randomID" value="${randomID}" />
</p>
<p>
    <input type="submit" value="Submit your comment" />
</p>
…

好棒的开始。现在,这个评论表单有了一个验证码图像了。 

[![image](/user_images/1315-3.png "image")](/user_images/1315-3.png)

### 验证这个验证码

现在,我们仅需要去验证这个验证码。我们需要添加这个随机ID(randomID)作为一个隐藏域,对吗?因此,我们可以在postComment行为上获取它,然后从缓冲中获取有效的代码,最后,再和已提交的代码进行比较。

这并不难。让我们修改这个`postComment`行为:

public static void postComment(
        Long postId, 
        @Required(message="Author is required") String author, 
        @Required(message="A message is required") String content, 
        @Required(message="Please type the code") String code, 
        String randomID) 
{
    Post post = Post.findById(postId);
    validation.equals(
        code, Cache.get(randomID)
    ).message("Invalid code. Please type it again");
    if(validation.hasErrors()) {
        render("Application/show.html", post, randomID);
    }
    post.addComment(author, content);
    flash.success("Thanks for posting %s", author);
    Cache.delete(randomID);
    show(postId);
}

因为现在我们有了更多的错误信息,所以修改下我们在show.html模板上显示的错误方式(为是的话,我们将会显示第一个错误,而这就足够了):

.. 
#{ifErrors}
    <p class="error">
        ${errors[0]}
    </p>
#{/ifErrors}
…

一般来说,对于更加复杂的表单,错误信息都不会用这种方式进行管理,而是都被实例化到messages 文件并且每个错误别写入到相应的域中。 检验这验证码现在是功能齐全了。

image

非常棒!

comments powered by Disqus