3 package Catalyst::Plugin::Session;
4 use base qw/Class::Accessor::Fast/;
10 use Catalyst::Exception ();
14 our $VERSION = "0.02";
17 __PACKAGE__->mk_accessors(qw/_sessionid _session session_delete_reason/);
25 $c->check_session_plugin_requirements;
31 sub check_session_plugin_requirements {
34 unless ( $c->isa("Catalyst::Plugin::Session::State")
35 && $c->isa("Catalyst::Plugin::Session::Store") )
38 ( "The Session plugin requires both Session::State "
39 . "and Session::Store plugins to be used as well." );
42 Catalyst::Exception->throw($err);
49 my $cfg = ( $c->config->{session} ||= {} );
57 $c->NEXT::setup_session();
63 if ( my $session_data = $c->_session ) {
65 # all sessions are extended at the end of the request
67 @{ $session_data }{qw/__updated __expires/} =
68 ( $now, $c->config->{session}{expires} + $now );
69 delete @{ $session_data->{__flash} }{ @{ delete $session_data->{__flash_stale_keys} || [] } };
70 $c->store_session_data( $c->sessionid, $session_data );
73 $c->NEXT::finalize(@_);
81 $c->NEXT::prepare_action(@_);
87 if ( my $sid = $c->sessionid ) {
88 no warnings 'uninitialized'; # ne __address
90 my $session_data = $c->_session || $c->_session( $c->get_session_data($sid) );
91 if ( !$session_data or $session_data->{__expires} < time ) {
94 $c->log->debug("Deleting session $sid (expired)") if $c->debug;
95 $c->delete_session("session expired");
97 elsif ($c->config->{session}{verify_address}
98 && $session_data->{__address} ne $c->request->address )
101 "Deleting session $sid due to address mismatch ("
102 . $session_data->{__address} . " != "
103 . $c->request->address . ")",
105 $c->delete_session("address mismatch");
108 $c->log->debug(qq/Restored session "$sid"/) if $c->debug;
111 $c->_expire_ession_keys;
112 $session_data->{__flash_stale_keys} = [ keys %{ $session_data->{__flash} } ]
117 sub _expire_ession_keys {
118 my ( $c, $data ) = @_;
122 my $expiry = ($data || $c->_session || {})->{__expire_keys} || {};
123 foreach my $key (grep { $expiry->{$_} < $now } keys %$expiry ) {
124 delete $c->_session->{$key};
125 delete $expiry->{$key};
130 my ( $c, $msg ) = @_;
132 # delete the session data
133 my $sid = $c->sessionid;
134 $c->delete_session_data($sid);
136 # reset the values in the context object
138 $c->_sessionid(undef);
139 $c->session_delete_reason($msg);
146 if ( $c->validate_session_id( my $sid = shift ) ) {
147 return $c->_sessionid( $sid );
149 my $err = "Tried to set invalid session ID '$sid'";
150 $c->log->error( $err );
151 Catalyst::Exception->throw( $err );
155 return $c->_sessionid;
158 sub validate_session_id {
159 my ( $c, $sid ) = @_;
161 $sid =~ /^[a-f\d]+$/i;
168 my $sid = $c->generate_session_id;
171 $c->log->debug(qq/Created session "$sid"/) if $c->debug;
173 $c->initialize_session_data;
179 return $c->session->{__flash} ||= {};
182 sub session_expire_key {
183 my ( $c, %keys ) = @_;
186 @{ $c->session->{__expire_keys} }{keys %keys} = map { $now + $_ } values %keys;
189 sub initialize_session_data {
194 return $c->_session({
197 __expires => $now + $c->config->{session}{expires},
200 $c->config->{session}{verify_address}
201 ? ( __address => $c->request->address )
207 sub generate_session_id {
210 my $digest = $c->_find_digest();
211 $digest->add( $c->session_hash_seed() );
212 return $digest->hexdigest;
217 sub session_hash_seed {
220 return join( "", ++$counter, time, rand, $$, {}, overload::StrVal($c), );
225 sub _find_digest () {
227 foreach my $alg (qw/SHA-1 MD5 SHA-256/) {
229 my $obj = Digest->new($alg);
235 or Catalyst::Exception->throw(
236 "Could not find a suitable Digest module. Please install "
237 . "Digest::SHA1, Digest::SHA, or Digest::MD5" );
240 return Digest->new($usable);
247 $c->NEXT::dump_these(),
250 ? ( [ "Session ID" => $c->sessionid ], [ Session => $c->session ], )
263 Catalyst::Plugin::Session - Generic Session plugin - ties together server side
264 storage and client side state required to maintain session data.
268 # To get sessions to "just work", all you need to do is use these plugins:
272 Session::Store::FastMmap
273 Session::State::Cookie
276 # you can replace Store::FastMmap with Store::File - both have sensible
277 # default configurations (see their docs for details)
279 # more complicated backends are available for other scenarios (DBI storage,
283 # after you've loaded the plugins you can save session data
284 # For example, if you are writing a shopping cart, it could be implemented
287 sub add_item : Local {
288 my ( $self, $c ) = @_;
290 my $item_id = $c->req->param("item");
292 # $c->session is a hash ref, a bit like $c->stash
293 # the difference is that it' preserved across requests
295 push @{ $c->session->{items} }, $item_id;
297 $c->forward("MyView");
300 sub display_items : Local {
301 my ( $self, $c ) = @_;
303 # values in $c->session are restored
304 $c->stash->{items_to_display} =
305 [ map { MyModel->retrieve($_) } @{ $c->session->{items} } ];
307 $c->forward("MyView");
312 The Session plugin is the base of two related parts of functionality required
313 for session management in web applications.
315 The first part, the State, is getting the browser to repeat back a session key,
316 so that the web application can identify the client and logically string
317 several requests together into a session.
319 The second part, the Store, deals with the actual storage of information about
320 the client. This data is stored so that the it may be revived for every request
321 made by the same client.
323 This plugin links the two pieces together.
325 =head1 RECCOMENDED BACKENDS
329 =item Session::State::Cookie
331 The only really sane way to do state is using cookies.
333 =item Session::Store::File
335 A portable backend, based on Cache::File.
337 =item Session::Store::FastMmap
339 A fast and flexible backend, based on Cache::FastMmap.
349 An accessor for the session ID value.
353 Returns a hash reference that might contain unserialized values from previous
354 requests in the same session, and whose modified value will be saved for future
357 This method will automatically create a new session and session ID if none
360 =item session_delete_reason
362 This accessor contains a string with the reason a session was deleted. Possible
377 =item session_expire_key $key, $ttl
379 Mark a key to expire at a certain time (only useful when shorter than the
380 expiry time for the whole session).
384 __PACKAGE__->config->{session}{expires} = 1000000000000; # forever
388 $c->session_expire_key( __user => 3600 );
390 Will make the session data survive, but the user will still be logged out after
393 Note that these values are not auto extended.
397 =item INTERNAL METHODS
403 This method is extended to also make calls to
404 C<check_session_plugin_requirements> and C<setup_session>.
406 =item check_session_plugin_requirements
408 This method ensures that a State and a Store plugin are also in use by the
413 This method populates C<< $c->config->{session} >> with the default values
414 listed in L</CONFIGURATION>.
418 This methoid is extended, and will restore session data and check it for
419 validity if a session id is defined. It assumes that the State plugin will
420 populate the C<sessionid> key beforehand.
424 This method is extended and will extend the expiry time, as well as persist the
425 session data if a session exists.
427 =item delete_session REASON
429 This method is used to invalidate a session. It takes an optional parameter
430 which will be saved in C<session_delete_reason> if provided.
432 =item initialize_session_data
434 This method will initialize the internal structure of the session, and is
435 called by the C<session> method if appropriate.
437 =item generate_session_id
439 This method will return a string that can be used as a session ID. It is
440 supposed to be a reasonably random string with enough bits to prevent
441 collision. It basically takes C<session_hash_seed> and hashes it using SHA-1,
442 MD5 or SHA-256, depending on the availibility of these modules.
444 =item session_hash_seed
446 This method is actually rather internal to generate_session_id, but should be
447 overridable in case you want to provide more random data.
449 Currently it returns a concatenated string which contains:
451 =item validate_session_id SID
453 Make sure a session ID is of the right format.
455 This currently ensures that the session ID string is any amount of case
456 insensitive hexadecimal characters.
470 One value from C<rand>.
474 The stringified value of a newly allocated hash reference
478 The stringified value of the Catalyst context object
482 In the hopes that those combined values are entropic enough for most uses. If
483 this is not the case you can replace C<session_hash_seed> with e.g.
485 sub session_hash_seed {
486 open my $fh, "<", "/dev/random";
487 read $fh, my $bytes, 20;
492 Or even more directly, replace C<generate_session_id>:
494 sub generate_session_id {
495 open my $fh, "<", "/dev/random";
496 read $fh, my $bytes, 20;
498 return unpack("H*", $bytes);
501 Also have a look at L<Crypt::Random> and the various openssl bindings - these
502 modules provide APIs for cryptographically secure random data.
506 See L<Catalyst/dump_these> - ammends the session data structure to the list of
507 dumped objects if session ID is defined.
511 =head1 USING SESSIONS DURING PREPARE
513 The earliest point in time at which you may use the session data is after
514 L<Catalyst::Plugin::Session>'s C<prepare_action> has finished.
516 State plugins must set $c->session ID before C<prepare_action>, and during
517 C<prepare_action> L<Catalyst::Plugin::Session> will actually load the data from
523 # don't touch $c->session yet!
525 $c->NEXT::prepare_action( @_ );
527 $c->session; # this is OK
528 $c->sessionid; # this is also OK
533 $c->config->{session} = {
537 All configuation parameters are provided in a hash reference under the
538 C<session> key in the configuration hash.
544 The time-to-live of each session, expressed in seconds. Defaults to 7200 (two
549 When true, C<<$c->request->address>> will be checked at prepare time. If it is
550 not the same as the address that initiated the session, the session is deleted.
556 The hash reference returned by C<< $c->session >> contains several keys which
557 are automatically set:
563 A timestamp whose value is the last second when the session is still valid. If
564 a session is restored, and __expires is less than the current time, the session
569 The last time a session was saved. This is the value of
570 C<< $c->session->{__expires} - $c->config->session->{expires} >>.
574 The time when the session was first created.
578 The value of C<< $c->request->address >> at the time the session was created.
579 This value is only populated if C<verify_address> is true in the configuration.
585 C<verify_address> could make your site inaccessible to users who are behind
586 load balanced proxies. Some ISPs may give a different IP to each request by the
587 same client due to this type of proxying. If addresses are verified these
588 users' sessions cannot persist.
590 To let these users access your site you can either disable address verification
591 as a whole, or provide a checkbox in the login dialog that tells the server
592 that it's OK for the address of the client to change. When the server sees that
593 this box is checked it should delete the C<__address> sepcial key from the
594 session hash when the hash is first created.
602 =item Christian Hansen
604 =item Yuval Kogman, C<nothingmuch@woobling.org> (current maintainer)
606 =item Sebastian Riedel
610 And countless other contributers from #catalyst. Thanks guys!
612 =head1 COPYRIGHT & LICENSE
614 Copyright (c) 2005 the aforementioned authors. All rights
615 reserved. This program is free software; you can redistribute
616 it and/or modify it under the same terms as Perl itself.