Amon2 での実装例です。
使用感としては、
といったかんじ。
管理画面とかでつかってみたらよいかもしれぬ。
use strict;
use warnings;
use utf8;
use File::Spec;
use File::Basename;
use lib File::Spec->catdir(dirname(__FILE__), 'extlib', 'lib', 'perl5');
use lib File::Spec->catdir(dirname(__FILE__), 'lib');
use Amon2::Lite;
{
package Amon2::Plugin::Web::Hsegawa;
use constant {
REDIRECT => 10001,
VALIDATION_ERROR => 10002,
};
sub init {
my ($class, $c, $conf) = @_;
$c->add_trigger(
BEFORE_DISPATCH => sub {
my $c = shift;
my $method = $c->req->method;
if ($method ne 'GET' && $method ne 'HEAD') {
unless (_validate_hsegawa($c)) {
my $res = $c->render_json(
{error => 'CSRF Detected.'},
);
return $res;
}
}
},
);
Amon2::Util::add_method($c, 'post_redirect', \&post_redirect);
Amon2::Util::add_method($c, 'post_validation_failed', \&post_validation_failed);
}
sub post_redirect {
my $self = shift;
my $res = $self->redirect(@_);
return $self->render_json(
{
code => REDIRECT,
location => scalar($res->header('Location')),
}
);
}
sub post_validation_failed {
my $self = shift;
return $self->render_json(
{
code => VALIDATION_ERROR,
message => $_[0],
}
);
}
sub _validate_hsegawa {
my $c = shift;
return 0 unless defined $c->req->header('X-From');
return 1 unless defined($c->req->header('Origin'));
return 1 if _is_same_origin(scalar($c->req->header('Origin')), scalar($c->req->header('X-From')));
return 0;
}
sub _is_same_origin {
my ($a, $b) = @_;
$a = URI->new($a);
$b = URI->new($b);
return (
$a->scheme eq $b->scheme
&& $a->host eq $b->host
&& $a->port eq $b->port
);
}
}
our $VERSION = '0.01';
my @ENTRIES;
sub load_config { +{} }
get '/' => sub {
my $c = shift;
return $c->render('index.tt', {
entries => \@ENTRIES,
});
};
post '/inquiry' => sub {
my $c = shift;
my $msg = $c->req->param('msg');
if ($msg =~ /fuck/) {
return $c->post_validation_failed('Bad word');
}
warn "POSTTTTTTTTTTTTTTTTTTT: $msg";
unshift @ENTRIES, $msg;
return $c->post_redirect('/');
};
# load plugins
__PACKAGE__->load_plugin('Web::JSON');
Amon2::Plugin::Web::Hsegawa->init(__PACKAGE__);
__PACKAGE__->enable_session();
__PACKAGE__->to_app(handle_static => 1);
__DATA__
@@ index.tt
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>hsegawa::CSRFTest</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.8.0/jquery.min.js"></script>
<link rel="stylesheet" href="http://twitter.github.com/bootstrap/1.4.0/bootstrap.min.css">
<link rel="stylesheet" href="[% uri_for('/static/css/main.css') %]">
</head>
<body>
<div class="container">
<header><h1>hsegawa::CSRFTest</h1></header>
<section class="row">
<form method="post" action="/inquiry" class="csrf">
<input type="text" name="msg">
<input type="submit" value="Send" class="btn">
</form>
</section>
<div>
[% FOR e IN entries %]
[% e %]<br />
[% END %]
</div>
<footer>Powered by <a href="http://amon.64p.org/">Amon2::Lite</a></footer>
</div>
<script>
"use strict";
$.fn.hsegawa = function (opts) {
var form = $(this);
form.submit(function () {
$.ajax({
url: form.attr('action'),
type: form.attr('method'),
data: form.serialize(),
headers: {
'X-From': location.href
},
dataType: 'json'
}).success(function (res) {
if (res.code == 10001) {
opts.redirect(res);
} else if (res.code == 10002) {
opts.validation_failed(res);
}
}).error(function (res) {
opts.error(res);
});
return false;
});
};
$('form.csrf').hsegawa({
redirect: function (res) {
location.href = res.location;
},
validation_failed: function (res) {
alert(res.message);
},
error: function (res) {
alert("ERROR: " + x);
}
});
</script>
</body>
</html>
@@ /static/css/main.css
footer {
text-align: right;
}