开发者

Validating Captcha after server round-trip and subsequent re-generation of captcha

I'm implementing CAPTCHA in my form submission as per Sanderson's book Pro ASP.NET MVC Framework.

The view fields are generated with:

<%= Html.Captcha("testCaptcha")%>
<%= Html.TextBox("attemptCaptcha")%>

The VerifyAndExpireSolution helper is not开发者_StackOverflow working as his solution is implemented.

I'm adding validation and when it fails I add a ModelState error message and send the user back to the view as stated in the book:

return ModelState.IsValid ? View("Completed", appt) : View();

But, doing so, generates a new GUID which generates new CAPTCHA text.

The problem is, however, that the CAPTCHA hidden field value and the CAPTCHA image url both retain the original GUID. So, you'll never be able to enter the correct value. You basically only have one shot to get it right.

I'm new to all of this, but it has something to do with the view retaining the values from the first page load.

Captcha is generated with:

public static string Captcha(this HtmlHelper html, string name)
{
    // Pick a GUID to represent this challenge
    string challengeGuid = Guid.NewGuid().ToString();
    // Generate and store a random solution text
    var session = html.ViewContext.HttpContext.Session;
    session[SessionKeyPrefix + challengeGuid] = MakeRandomSolution();

    // Render an <IMG> tag for the distorted text,
    // plus a hidden field to contain the challenge GUID
    var urlHelper = new UrlHelper(html.ViewContext.RequestContext);
    string url = urlHelper.Action("Render", "CaptchaImage", new{challengeGuid});
    return string.Format(ImgFormat, url) + html.Hidden(name, challengeGuid);
}

And then I try to validate it with:

public static bool VerifyAndExpireSolution(HttpContextBase context,
                                       string challengeGuid,
                                       string attemptedSolution)
{
    // Immediately remove the solution from Session to prevent replay attacks
    string solution = (string)context.Session[SessionKeyPrefix + challengeGuid];
    context.Session.Remove(SessionKeyPrefix + challengeGuid);

    return ((solution != null) && (attemptedSolution == solution));
}

What about re-building the target field names with the guid? Then, each field is unique and won't retain the previous form generations' value?

Or do I just need a different CAPTCHA implementation?


I had the same problem using the captcha example from Sanderson's book. The problem is that the page gets cached by the browser and doesn't refresh after the captcha test fails. So it always shows the same image, even though a new captcha has been generated and stored for testing.

One solution is to force the browser to refresh the page when reloading after a failed attempt; this won't happen if you just return View(). You can do this using RedirectToAction("SubmitEssay") which will hit the action method accepting HttpVerbs.Get.

Of course, you lose the ability to use ViewData to notify your user of the error, but you can just include this in the query string, then just check the query string to display your message.

So, following the book's example,

if (!CaptchaHelper.VerifyAndExpireSolution(HttpContext, captcha, captchaAttempt) 
{
    RedirectToAction("SubmitEssay", new { fail = 1 });
}

Then just check if the QueryString collection contains 'fail' to deliver your error message.


So, I decided to implement reCaptcha. And I've customized my view likewise:

<div id="recaptcha_image"></div>&nbsp;
     <a href="#" onclick="Recaptcha.reload();">
            generate a new image
     </a><br />
<input type="text" name="recaptcha_response_field" 
           id="recaptcha_response_field" />
           &nbsp;<%= Html.ValidationMessage("attemptCaptcha")%>
<script type="text/javascript" 
     src="http://api.recaptcha.net/challenge?k=[my public key]"></script>

This creates two captchas- one in my image container, and another created by the script. So, I added css to hide the auto-generated one:

<style type="text/css">
    #recaptcha_widget_div {display:none;}
</style>

Then, in my controller, I merely have to test for captchaValid:

[CaptchaValidator]
[AcceptVerbs(HttpVerbs.Post)]
public ViewResult SubmitEssay(Essay essay, bool acceptsTerms, bool captchaValid)
{
    if (!acceptsTerms)
        ModelState.AddModelError("acceptsTerms", 
                     "You must accept the terms and conditions.");
    else
    {
       try
       {
            // save/validate the essay
            var errors = essay.GetRuleViolations(captchaValid);
            if (errors.Count > 0)
                throw new RuleException(errors);

        }
        catch (RuleException ex)
        {
            ex.CopyToModelState(ModelState, "essay");
        }
    }
    return ModelState.IsValid ? View("Completed", essay) : View();
}

public NameValueCollection GetRuleViolations(bool captchaValid)
{
    var errors = new NameValueCollection();
    if (!captchaValid)
        errors.Add("attemptCaptcha", 
             "Please enter the correct verification text before submitting.");
    // continue with other fields....
}

And all of this assumes that you've implemented the Action Filter attribute and the view helper as detailed at recaptcha.net:

public class CaptchaValidatorAttribute : ActionFilterAttribute
{
    private const string CHALLENGE_FIELD_KEY = "recaptcha_challenge_field";
    private const string RESPONSE_FIELD_KEY = "recaptcha_response_field";

    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        var captchaChallengeValue = 
             filterContext.HttpContext.Request.Form[CHALLENGE_FIELD_KEY];
        var captchaResponseValue = 
             filterContext.HttpContext.Request.Form[RESPONSE_FIELD_KEY];
        var captchaValidtor = new Recaptcha.RecaptchaValidator
          {
              PrivateKey = "[my private key]",
              RemoteIP = filterContext.HttpContext.Request.UserHostAddress,
              Challenge = captchaChallengeValue,
              Response = captchaResponseValue
          };

        var recaptchaResponse = captchaValidtor.Validate();

    // this will push the result value into a parameter in our Action
        filterContext.ActionParameters["captchaValid"] = recaptchaResponse.IsValid;

        base.OnActionExecuting(filterContext);
    }
}

html helper:

public static class Captcha
{
    public static string GenerateCaptcha( this HtmlHelper helper )
    {  
    var captchaControl = new Recaptcha.RecaptchaControl
        {
            ID = "recaptcha",
            Theme = "clean",
            PublicKey = "[my public key]",
            PrivateKey = "[ my private key ]"
        };
    var htmlWriter = new HtmlTextWriter( new StringWriter() );
        captchaControl.RenderControl(htmlWriter);
    return htmlWriter.InnerWriter.ToString();
    }
}

Hope this helps someone who got stuck with the implementation in the book.

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜