Created
March 27, 2011 15:16
-
-
Save riywo/889285 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
use strict; | |
use warnings; | |
use Plack::Builder; | |
use Plack::Session; | |
use Plack::Session::State::Cookie; | |
use Plack::Session::Store::Cache; | |
use Authen::Simple::DBI; | |
use Data::Section::Simple; | |
use Text::Xslate; | |
use Cache::Memcached::Fast; | |
use DBI; | |
use Digest; | |
=pod | |
CREATE TABLE `user_data` ( | |
`user_id` int(10) unsigned NOT NULL AUTO_INCREMENT, | |
`username` varchar(20) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, | |
`password` varchar(64) NOT NULL, | |
PRIMARY KEY (`user_id`), | |
UNIQUE KEY `username` (`username`) | |
) ENGINE=InnoDB DEFAULT CHARSET=utf8; | |
CREATE TABLE `auto_login` ( | |
`token` varchar(48) NOT NULL, | |
`user_id` int(10) unsigned NOT NULL, | |
`expires` int(10) unsigned NOT NULL, | |
PRIMARY KEY (`token`), | |
KEY `i1` (`user_id`) | |
) ENGINE=InnoDB DEFAULT CHARSET=utf8; | |
=cut | |
my $fixed_salt = 'aaaaaaaaaaaaaaaaaa'; | |
my $stretch_count = 1000; | |
my $app = sub { | |
my $env = shift; | |
my $req = Plack::Request->new($env); | |
my $res = $req->new_response(200); | |
my $session = Plack::Session->new($env); | |
my $tx = Text::Xslate->new( | |
path => [ | |
Data::Section::Simple->new()->get_data_section(), | |
], | |
); | |
## login? | |
my $dbh = DBI->connect('dbi:mysql:database=test;', 'root', '', {RaiseError => 1, AutoCommit => 1}); | |
my $user_id = $session->get('user_id'); | |
my $username; | |
if ($user_id) { | |
$username = $dbh->selectrow_array( | |
'select username from user_data where user_id = ?', {}, $user_id); | |
} | |
elsif ($req->cookies->{'token'}){ | |
my $expires; | |
($user_id, $expires) = $dbh->selectrow_array( | |
'select user_id, expires from auto_login where token = ?', {}, $req->cookies->{'token'}); | |
if ($expires < time) { | |
$dbh->do('delete from auto_login where token = ?', {}, $req->cookies->{'token'}); | |
$user_id = undef; | |
} | |
else { | |
## auto login using token | |
$session->set('user_id', $user_id); | |
$username = $dbh->selectrow_array( | |
'select username from user_data where user_id = ?', {}, $user_id); | |
$dbh->do('delete from auto_login where token = ?', {}, $req->cookies->{'token'}); | |
my $token = _set_auth_token($dbh, $user_id, $expires); | |
$res->cookies->{'token'} = {value => $token, expires => $expires}; | |
} | |
} | |
my $arg = { | |
session => { | |
sid => $session->id, | |
%{$session->session}, | |
username => $username, | |
}, | |
cookies => $req->cookies, | |
}; | |
if ($req->path_info eq '/') { | |
$res->content_type('text/html'); | |
$res->body($tx->render('index.tx', $arg)); | |
$res->finalize; | |
} | |
elsif ($req->path_info eq '/mypage') { | |
if (!$user_id) { | |
$res->redirect('/'); | |
$res->finalize; | |
} | |
else { | |
my ($password) = $dbh->selectrow_array( | |
'select password from user_data where user_id = ?', {}, $user_id); | |
$arg->{'password'} = $password; | |
$res->content_type('text/html'); | |
$res->body($tx->render('mypage.tx', $arg)); | |
$res->finalize; | |
} | |
} | |
elsif ($req->path_info eq '/login') { | |
if ($user_id) { | |
## already login | |
$res->redirect('/'); | |
$res->finalize; | |
} | |
elsif ($req->method eq 'GET') { | |
$session->set('redir_to', $env->{HTTP_REFERER} || '/'); | |
$res->content_type('text/html'); | |
$res->body($tx->render('login.tx', $arg)); | |
$res->finalize; | |
} | |
elsif ($req->method eq 'POST') { | |
## check username & password | |
my $auth_result; | |
my $req_user_id; | |
eval { | |
my $authen = Authen::Simple::DBI->new( | |
dsn => 'dbi:mysql:host=localhost;database=test;', | |
username => 'root', | |
statement => 'select password from user_data where user_id = ?', | |
); | |
($req_user_id) = $dbh->selectrow_array( | |
'select user_id from user_data where username = ?', {}, $req->param('username')); | |
die if(!$req_user_id); | |
$auth_result = $authen->authenticate( | |
$req_user_id, _get_password_hash($req_user_id, $req->param('password'))); | |
}; | |
$auth_result = undef if($@); | |
if ($auth_result) { | |
## login successed | |
$req->session_options->{change_id}++; | |
$session->set('user_id', $req_user_id); | |
if ($req->param('auto_login')) { | |
my $expires = time + (24 * 60 * 60); | |
my $token = _set_auth_token($dbh, $req_user_id, $expires); | |
$res->cookies->{'token'} = {value => $token, expires => $expires}; | |
} | |
my $redir_to = $session->get('redir_to'); | |
$session->remove('redir_to'); | |
$res->redirect($redir_to); | |
$res->finalize; | |
} | |
else { | |
## login failed | |
$res->content_type('text/html'); | |
$res->body($tx->render('login_fail.tx', $arg)); | |
$res->finalize; | |
} | |
} | |
} | |
elsif ($req->path_info eq '/logout') { | |
if ($req->method eq 'POST' and $req->param('token') eq $session->id and $user_id) { | |
$res->cookies->{'token'} = {expires => 0}; | |
$dbh->do('delete from auto_login where user_id = ?', {}, $user_id); | |
$session->expire; | |
$res->redirect('/'); | |
$res->finalize; | |
} | |
} | |
else { | |
$res->status(404); | |
$res->body("Not Found"); | |
$res->finalize; | |
} | |
}; | |
sub _get_salt { | |
my $username = shift; | |
return $username . pack("H*", $fixed_salt); | |
} | |
sub _get_password_hash { | |
my ($username, $password) = @_; | |
my $salt = _get_salt($username); | |
my $hash = ''; | |
my $sha = Digest->new('SHA-256'); | |
for (1..$stretch_count) { | |
$sha->add($hash, $password, $salt); | |
$hash = $sha->hexdigest; | |
} | |
return $hash; | |
} | |
sub _set_auth_token { | |
my ($dbh, $user_id, $expires) = @_; | |
my $token; | |
my $sth = $dbh->prepare('insert into auto_login (token, user_id, expires) values (?, ?, ?)'); | |
for (1..10) { | |
$token = _get_token(); | |
eval { | |
$sth->execute($token, $user_id, $expires); | |
}; | |
if (!$@) { | |
return $token; | |
} | |
elsif ($sth->err == 1062) { | |
next; | |
} | |
} | |
die; | |
} | |
sub _get_token { | |
open my $fh, '<', '/dev/urandom'; | |
read $fh, my $buf, 24; | |
close $fh; | |
return unpack("H*", $buf); | |
} | |
builder { | |
enable 'Debug'; | |
enable 'Session', | |
store => Plack::Session::Store::Cache->new( | |
cache => Cache::Memcached::Fast->new({ | |
servers => [{address => 'localhost:11211'}], | |
}), | |
), | |
state => Plack::Session::State::Cookie->new( | |
session_key => 'sid', | |
); | |
$app; | |
}; | |
__DATA__ | |
@@ base.tx | |
<html> | |
<head><title>Session Test</title></head> | |
<body> | |
<h2>Body</h2> | |
<div> | |
<: block body {} :> | |
</div> | |
<h2>Session</h2> | |
<ul> | |
<li>HTTP Request Cookie | |
<ul> | |
: for $cookies.keys() -> $key { | |
<li><: $key :>=<: $cookies[$key] :></li> | |
: } | |
</ul> | |
</li> | |
<li>in Plack Session | |
<ul> | |
: for $session.keys() -> $key { | |
<li><: $key :>=<: $session[$key] :></li> | |
: } | |
</ul> | |
</li> | |
<li>Cookie Info (using JavaScript without http-only) | |
<ul><li><script>document.write(document.cookie.split(';').join('</li><li>'));</script></li></ul> | |
</li> | |
</ul> | |
<h2>Verified ?</h2> | |
: if $session.user_id { | |
verified username = <: $session.username :> | |
: } | |
: else { | |
not verified | |
: } | |
<h2>Link</h2> | |
<ul> | |
<li><a href="http://localhost:5000/">top</a></li> | |
<li><a href="http://localhost:5000/mypage">mypage</a></li> | |
<li><a href="http://localhost:5000/login">login</a></li> | |
<li><form action="/logout" method="POST"> | |
<input type="hidden" name="token" value="<: $session.sid :>"> | |
<input type="submit" value="logout"> | |
</form></li> | |
</ul> | |
</body> | |
</html> | |
@@ index.tx | |
: cascade base; | |
: override body -> { | |
Top Page | |
: } | |
@@ mypage.tx | |
: cascade base; | |
: override body -> { | |
Hi <: $session.username :> ! This is Your My Page! <br /> | |
Your Password Hash is <: $password :> <br /> | |
:} | |
@@ login.tx | |
: cascade base; | |
: override body -> { | |
<form action="/login" method="POST"> | |
username: <input type="text" name="username" /><br /> | |
password: <input type="password" name="password" /><br /> | |
auto login(24 hours): <input type="checkbox" name="auto_login" /><br /> | |
<input type="submit" value="login" /> | |
</form> | |
: } | |
@@ login_fail.tx | |
: cascade base; | |
: override body -> { | |
<p><em>username or password failed!</em></p> | |
<form action="/login" method="POST"> | |
username: <input type="text" name="username" /><br /> | |
password: <input type="password" name="password" /><br /> | |
auto login(24 hours): <input type="checkbox" name="auto_login" /><br /> | |
<input type="submit" value="login" /> | |
</form> | |
:} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment