3 package Catalyst::Plugin::Session;
4 use base qw/Class::Accessor::Fast/;
10 use Catalyst::Exception ();
13 use Object::Signature ();
15 our $VERSION = "0.03";
18 __PACKAGE__->mk_accessors(
24 _session_delete_reason
36 $c->check_session_plugin_requirements;
42 sub check_session_plugin_requirements {
45 unless ( $c->isa("Catalyst::Plugin::Session::State")
46 && $c->isa("Catalyst::Plugin::Session::Store") )
49 ( "The Session plugin requires both Session::State "
50 . "and Session::Store plugins to be used as well." );
53 Catalyst::Exception->throw($err);
60 my $cfg = ( $c->config->{session} ||= {} );
68 $c->NEXT::setup_session();
74 if ( $c->config->{session}{flash_to_stash}
76 and my $flash_data = $c->flash )
78 @{ $c->stash }{ keys %$flash_data } = values %$flash_data;
81 $c->NEXT::prepare_action(@_);
90 $c->NEXT::finalize(@_);
96 if ( my $sid = $c->_sessionid ) {
97 if ( my $session_data = $c->_session ) {
99 # all sessions are extended at the end of the request
101 $c->store_session_data(
102 "expires:$sid" => ( $c->config->{session}{expires} + $now ) );
104 no warnings 'uninitialized';
105 if ( Object::Signature::signature($session_data) ne
106 $c->_session_data_sig )
108 $session_data->{__updated} = $now;
109 $c->store_session_data( "session:$sid" => $session_data );
118 if ( my $sid = $c->_sessionid ) {
119 my $flash_data = $c->_flash || {};
121 delete @{$flash_data}{ @{ $c->_flash_stale_keys || [] } };
123 if (%$flash_data) { # damn 'my' declarations
124 $c->store_session_data( "flash:$sid", $flash_data );
127 $c->delete_session_data("flash:$sid");
135 if ( my $sid = $c->_sessionid ) {
136 no warnings 'uninitialized'; # ne __address
138 my $session_expires = $c->get_session_data("expires:$sid") || 0;
140 if ( $session_expires < time ) {
143 $c->log->debug("Deleting session $sid (expired)") if $c->debug;
144 $c->delete_session("session expired");
148 my $session_data = $c->get_session_data("session:$sid");
149 $c->_session($session_data);
151 if ( $c->config->{session}{verify_address}
152 && $session_data->{__address} ne $c->request->address )
155 "Deleting session $sid due to address mismatch ("
156 . $session_data->{__address} . " != "
157 . $c->request->address . ")",
159 $c->delete_session("address mismatch");
163 $c->log->debug(qq/Restored session "$sid"/) if $c->debug;
164 $c->_session_data_sig( Object::Signature::signature($session_data) );
165 $c->_expire_session_keys;
167 return $session_data;
176 if ( my $sid = $c->_sessionid ) {
177 if ( my $flash_data = $c->_flash
178 || $c->_flash( $c->get_session_data("flash:$sid") ) )
180 $c->_flash_stale_keys( [ keys %$flash_data ] );
188 sub _expire_session_keys {
189 my ( $c, $data ) = @_;
193 my $expiry = ( $data || $c->_session || {} )->{__expire_keys} || {};
194 foreach my $key ( grep { $expiry->{$_} < $now } keys %$expiry ) {
195 delete $c->_session->{$key};
196 delete $expiry->{$key};
201 my ( $c, $msg ) = @_;
203 # delete the session data
204 my $sid = $c->_sessionid || return;
205 $c->delete_session_data("${_}:${sid}") for qw/session expires flash/;
207 # reset the values in the context object
209 $c->_sessionid(undef);
210 $c->_session_delete_reason($msg);
213 sub session_delete_reason {
217 if ( $c->_sessionid && !$c->_session ); # must verify session data
219 $c->_session_delete_reason(@_);
226 if ( $c->validate_session_id( my $sid = shift ) ) {
227 $c->_sessionid($sid);
228 return unless defined wantarray;
231 my $err = "Tried to set invalid session ID '$sid'";
232 $c->log->error($err);
233 Catalyst::Exception->throw($err);
238 if ( $c->_sessionid && !$c->_session ); # must verify session data
240 return $c->_sessionid;
243 sub validate_session_id {
244 my ( $c, $sid ) = @_;
246 $sid and $sid =~ /^[a-f\d]+$/i;
252 $c->_session || $c->_load_session || do {
253 $c->create_session_id;
255 $c->initialize_session_data;
261 $c->_flash || $c->_load_flash || do {
262 $c->create_session_id;
267 sub session_expire_key {
268 my ( $c, %keys ) = @_;
271 @{ $c->session->{__expire_keys} }{ keys %keys } =
272 map { $now + $_ } values %keys;
275 sub initialize_session_data {
286 $c->config->{session}{verify_address}
287 ? ( __address => $c->request->address )
294 sub generate_session_id {
297 my $digest = $c->_find_digest();
298 $digest->add( $c->session_hash_seed() );
299 return $digest->hexdigest;
302 sub create_session_id {
305 if ( !$c->_sessionid ) {
306 my $sid = $c->generate_session_id;
308 $c->log->debug(qq/Created session "$sid"/) if $c->debug;
316 sub session_hash_seed {
319 return join( "", ++$counter, time, rand, $$, {}, overload::StrVal($c), );
324 sub _find_digest () {
326 foreach my $alg (qw/SHA-1 SHA-256 MD5/) {
327 if ( eval { Digest->new($alg) } ) {
332 Catalyst::Exception->throw(
333 "Could not find a suitable Digest module. Please install "
334 . "Digest::SHA1, Digest::SHA, or Digest::MD5" )
338 return Digest->new($usable);
345 $c->NEXT::dump_these(),
348 ? ( [ "Session ID" => $c->sessionid ], [ Session => $c->session ], )
361 Catalyst::Plugin::Session - Generic Session plugin - ties together server side
362 storage and client side state required to maintain session data.
366 # To get sessions to "just work", all you need to do is use these plugins:
370 Session::Store::FastMmap
371 Session::State::Cookie
374 # you can replace Store::FastMmap with Store::File - both have sensible
375 # default configurations (see their docs for details)
377 # more complicated backends are available for other scenarios (DBI storage,
381 # after you've loaded the plugins you can save session data
382 # For example, if you are writing a shopping cart, it could be implemented
385 sub add_item : Local {
386 my ( $self, $c ) = @_;
388 my $item_id = $c->req->param("item");
390 # $c->session is a hash ref, a bit like $c->stash
391 # the difference is that it' preserved across requests
393 push @{ $c->session->{items} }, $item_id;
395 $c->forward("MyView");
398 sub display_items : Local {
399 my ( $self, $c ) = @_;
401 # values in $c->session are restored
402 $c->stash->{items_to_display} =
403 [ map { MyModel->retrieve($_) } @{ $c->session->{items} } ];
405 $c->forward("MyView");
410 The Session plugin is the base of two related parts of functionality required
411 for session management in web applications.
413 The first part, the State, is getting the browser to repeat back a session key,
414 so that the web application can identify the client and logically string
415 several requests together into a session.
417 The second part, the Store, deals with the actual storage of information about
418 the client. This data is stored so that the it may be revived for every request
419 made by the same client.
421 This plugin links the two pieces together.
423 =head1 RECCOMENDED BACKENDS
427 =item Session::State::Cookie
429 The only really sane way to do state is using cookies.
431 =item Session::Store::File
433 A portable backend, based on Cache::File.
435 =item Session::Store::FastMmap
437 A fast and flexible backend, based on Cache::FastMmap.
447 An accessor for the session ID value.
451 Returns a hash reference that might contain unserialized values from previous
452 requests in the same session, and whose modified value will be saved for future
455 This method will automatically create a new session and session ID if none
460 This is like Ruby on Rails' flash data structure. Think of it as a stash that
461 lasts a single redirect, not only a forward.
464 my ( $self, $c ) = @_;
466 $c->flash->{beans} = 10;
467 $c->response->redirect( $c->uri_for("foo") );
471 my ( $self, $c ) = @_;
473 my $value = $c->flash->{beans};
477 $c->response->redirect( $c->uri_for("bar") );
481 my ( $self, $c ) = @_;
483 if ( exists $c->flash->{beans} ) { # false
488 =item session_delete_reason
490 This accessor contains a string with the reason a session was deleted. Possible
505 =item session_expire_key $key, $ttl
507 Mark a key to expire at a certain time (only useful when shorter than the
508 expiry time for the whole session).
512 __PACKAGE__->config->{session}{expires} = 1000000000000; # forever
516 $c->session_expire_key( __user => 3600 );
518 Will make the session data survive, but the user will still be logged out after
521 Note that these values are not auto extended.
525 =item INTERNAL METHODS
531 This method is extended to also make calls to
532 C<check_session_plugin_requirements> and C<setup_session>.
534 =item check_session_plugin_requirements
536 This method ensures that a State and a Store plugin are also in use by the
541 This method populates C<< $c->config->{session} >> with the default values
542 listed in L</CONFIGURATION>.
546 This methoid is extended.
548 It's only effect is if the (off by default) C<flash_to_stash> configuration
549 parameter is on - then it will copy the contents of the flash to the stash at
554 This method is extended and will extend the expiry time, as well as persist the
555 session data if a session exists.
557 =item delete_session REASON
559 This method is used to invalidate a session. It takes an optional parameter
560 which will be saved in C<session_delete_reason> if provided.
562 =item initialize_session_data
564 This method will initialize the internal structure of the session, and is
565 called by the C<session> method if appropriate.
567 =item create_session_id
569 Creates a new session id using C<generate_session_id> if there is no session ID
572 =item generate_session_id
574 This method will return a string that can be used as a session ID. It is
575 supposed to be a reasonably random string with enough bits to prevent
576 collision. It basically takes C<session_hash_seed> and hashes it using SHA-1,
577 MD5 or SHA-256, depending on the availibility of these modules.
579 =item session_hash_seed
581 This method is actually rather internal to generate_session_id, but should be
582 overridable in case you want to provide more random data.
584 Currently it returns a concatenated string which contains:
586 =item validate_session_id SID
588 Make sure a session ID is of the right format.
590 This currently ensures that the session ID string is any amount of case
591 insensitive hexadecimal characters.
605 One value from C<rand>.
609 The stringified value of a newly allocated hash reference
613 The stringified value of the Catalyst context object
617 In the hopes that those combined values are entropic enough for most uses. If
618 this is not the case you can replace C<session_hash_seed> with e.g.
620 sub session_hash_seed {
621 open my $fh, "<", "/dev/random";
622 read $fh, my $bytes, 20;
627 Or even more directly, replace C<generate_session_id>:
629 sub generate_session_id {
630 open my $fh, "<", "/dev/random";
631 read $fh, my $bytes, 20;
633 return unpack("H*", $bytes);
636 Also have a look at L<Crypt::Random> and the various openssl bindings - these
637 modules provide APIs for cryptographically secure random data.
641 See L<Catalyst/dump_these> - ammends the session data structure to the list of
642 dumped objects if session ID is defined.
646 =head1 USING SESSIONS DURING PREPARE
648 The earliest point in time at which you may use the session data is after
649 L<Catalyst::Plugin::Session>'s C<prepare_action> has finished.
651 State plugins must set $c->session ID before C<prepare_action>, and during
652 C<prepare_action> L<Catalyst::Plugin::Session> will actually load the data from
658 # don't touch $c->session yet!
660 $c->NEXT::prepare_action( @_ );
662 $c->session; # this is OK
663 $c->sessionid; # this is also OK
668 $c->config->{session} = {
672 All configuation parameters are provided in a hash reference under the
673 C<session> key in the configuration hash.
679 The time-to-live of each session, expressed in seconds. Defaults to 7200 (two
684 When true, C<<$c->request->address>> will be checked at prepare time. If it is
685 not the same as the address that initiated the session, the session is deleted.
689 This option makes it easier to have actions behave the same whether they were
690 forwarded to or redirected to. On prepare time it copies the contents of
691 C<flash> (if any) to the stash.
697 The hash reference returned by C<< $c->session >> contains several keys which
698 are automatically set:
704 This key no longer exists. This data is now saved elsewhere.
708 The last time a session was saved to the store.
712 The time when the session was first created.
716 The value of C<< $c->request->address >> at the time the session was created.
717 This value is only populated if C<verify_address> is true in the configuration.
723 =head2 Round the Robin Proxies
725 C<verify_address> could make your site inaccessible to users who are behind
726 load balanced proxies. Some ISPs may give a different IP to each request by the
727 same client due to this type of proxying. If addresses are verified these
728 users' sessions cannot persist.
730 To let these users access your site you can either disable address verification
731 as a whole, or provide a checkbox in the login dialog that tells the server
732 that it's OK for the address of the client to change. When the server sees that
733 this box is checked it should delete the C<__address> sepcial key from the
734 session hash when the hash is first created.
736 =head2 Race Conditions
738 In this day and age where cleaning detergents and dutch football (not the
739 american kind) teams roam the plains in great numbers, requests may happen
740 simultaneously. This means that there is some risk of session data being
741 overwritten, like this:
747 request a starts, request b starts, with the same session id
751 session data is loaded in request a
755 session data is loaded in request b
759 session data is changed in request a
763 request a finishes, session data is updated and written to store
767 request b finishes, session data is updated and written to store, overwriting
772 If this is a concern in your application, a soon to be developed locking
773 solution is the only safe way to go. This will have a bigger overhead.
775 For applications where any given user is only making one request at a time this
776 plugin should be safe enough.
784 =item Christian Hansen
786 =item Yuval Kogman, C<nothingmuch@woobling.org> (current maintainer)
788 =item Sebastian Riedel
792 And countless other contributers from #catalyst. Thanks guys!
794 =head1 COPYRIGHT & LICENSE
796 Copyright (c) 2005 the aforementioned authors. All rights
797 reserved. This program is free software; you can redistribute
798 it and/or modify it under the same terms as Perl itself.