(本文译者是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尝试下这个验证码行为。)
[](/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>
…
好棒的开始。现在,这个评论表单有了一个验证码图像了。
[](/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 文件并且每个错误别写入到相应的域中。 检验这验证码现在是功能齐全了。
非常棒!