CGI::Session.pmでセッション管理

はじめに

perlでCGI::Session.pmを使ってセッション管理を行う.idの生成・抹消をはじめ,セッション管理に必要な様々な機能を提供してくれ,非常に便利である.

CGI::Session.pmは必要なデータをサーバに置く.すなわち,セッションidを生成した後,サーバ内にidに対応するファイル(DBを指定することも可能)を生成する.セッションで必要なデータはサーバのファイルに保管し,プログラム間を行き来するのはセッションidのみである.

CGI.pmも一緒に使うと利便性はさらに高くなる.

動作環境

動作環境は以下の通りである.Windows XP上でWindows版のApache,cygwin版のperlが動いている.

  • Windows XP Professional
  • Apache 2.0.48(Windows版)
  • cygwin 1.5.7-1
  • perl 5.8.0

インストール

Windows XP/Apache/cgywin/perlのインストールは省略する.インストーラの言う通りでインストールできる.perlもcygwinのセットアッププログラムでインストールできる.

では,何をインストールするのかといえば,CPANを使ってCGI::Session.pmのインストールを行う.CPANはperlのモジュールのインストールを行うもので,CGI::Sessionはperlのモジュールである.CPANの設定を行う必要があるが,CPAN 初級を参考にしてほしい.Googleで「perl cpan」をキーワードにして検索しても参考になるサイトはいっぱいある.

CPANの設定が終わったら,CGI::Session.pmをインストールする.手順は簡単だ.

$ perl -MCPAN -e shell
cpan> install CGI::Session
        

これでインストールしてくれる.“/cygdrive/c/program: not found”こんなエラーが出てインストールに失敗する場合は,空白のあるディレクトリが原因である.cygwinでcpanを使ってエラーを参照のこと.

CGI::Session.pm以外にCGI.pmもインストールする.一緒に使うと便利である.手順はCGI::Session.pmと同様である.

$ perl -MCPAN -e shell
cpan> install CGI
        

サンプルの説明

二つの画面からなるセッションを想定する.最初の画面を経由したアクセスのみを正常のアクセスとし,直接次の画面を呼出したり,一定期間(1分)を経過した後のアクセスはエラーとする.

プログラムは

${APACHE_HOME}/cgi-bin/
        

以下に配置する.${APACHE_HOME}はApacheをインストールしたディレクトリである.

遷移元画面:urlパラメータでセッションidを渡す

遷移元の画面を表示するプログラム(session-cgi.pl)である.遷移先へはセッションidをurlパラメータに付加して遷移する.アクセスするurlは以下の通りとなる.

http://localhost/cgi-bin/session-cgi.pl
        
#session-cgi.pl
#!c:/progra~1/cygwin/bin/perl.exe -w
use strict;
use CGI;
use CGI::Session qw/-ip_match/; 
#異なるipからのアクセスは認めない
#Tutrial.podは'-ip-match',Session.pmは'-ip_match'.多分'-ip_match'.

my $session=CGI::Session->new(undef,undef,{Directory=>'./.session'});
#セッションidの生成.ディレクトリ.sessionは予め作っておく
$session->expire('+1m'); #有効期限の設定.1分間
$session->param('name','john'); #セッション経由で引き渡す項目と値

#htmlの生成
my $cgi=CGI->new;
print $cgi->header(-charset=>'UTF-8'),
      $cgi->start_html(-lang=>'ja',
                       -encoding=>'UTF-8',
                       -title=>'CGI.pm使用/urlパラメータ'),
      $cgi->p('session id: '.$session->id.'<br/>',
              'name: '.$session->param('name').'<br/>',
              $cgi->a({href=>'http://localhost/cgi-bin/session.pl?CGISESSID='.$session->id},'next')),
      $cgi->end_html;
#end
        

セッションidの生成は以下のように記述する.

my $session=CGI::Session->new(undef,undef,{Directory=>'./.session'});
        

第一引数のデータソースがundefなので,「ファイルに,デフォルトのシリアライザを使って,MD5のIDジェネレータでidを生成」する.第二引数もundefなので新規idを生成し,第三引数のディレクトリにidに対応するファイルを生成する.カレントディレクトリにある.sessionディレクトリ内にこのようなファイル(cgisess_セッションid)ができる.中に有効期間やセッションに格納したデータが入っている.なお,ファイルを格納するディレクトリ(今回の場合は.session)は予め作っておく必要がある.

セッションの有効期限や,セッション経由で次の画面へ引き渡すデータの設定は下記のように記述する.

$session->expire('+1m'); #有効期限の設定.1分間
$session->param('name','john'); #セッション経由で引き渡す項目と値
        

urlパラメータとして次画面へ引渡すはidのみである.有効期限やセッション経由で引渡す値は全て.sessionディレクトリ以下のファイルに入る.

詳細は,CGIアプリケーションにおける持続的なデータのセッション参照.

htmlの生成はCGI.pmを使用している.CGI.pmの使用方法は簡単なCGI(Common Gateway Interface)クラスが詳しい.

cookieでセッションidを渡す

これも遷移元の画面を表示するプログラムである.前のプログラムとは,セッションidの渡し方が異なっている.urlパラメータではなくcookieを使用している.cookieはCGI.pmの機能で操作している.アクセスするurlは以下の通りとなる.

http://localhost/cgi-bin/session-cookie.pl
        
#session-cookie.pl
#!c:/progra~1/cygwin/bin/perl.exe -w
use strict;
use CGI;
use CGI::Session qw/-ip_match/; 

my $session=CGI::Session->new(undef,undef,{Directory=>'./.session'});
$session->expire('+1m'); #有効期限は1分間
$session->param('name','john'); #セッション経由で引き渡す項目と値
my $cgi=CGI->new;
print $cgi->header(-charset=>'UTF-8',
                   -cookie=>$cgi->cookie(-name=>'CGISESSID',
                                         -value=>$session->id)),
                   #cookieにセッションidを保管
      $cgi->start_html(-lang=>'ja',
                       -encoding=>'UTF-8',
                       -title=>'CGI.pm使用/cookie'),
      $cgi->p('session id: '.$session->id.'<br/>',
              'name: '.$session->param('name').'<br/>',
              $cgi->a({href=>"http://localhost/cgi-bin/session.pl"},"next")),
              #urlパラメータにはセッションid不要
      $cgi->end_html;
#end
        

セッションidを受取る遷移先画面

遷移先の画面のプログラムである.urlパラメータとcookie両方に対応している.

#session.pl
#!c:/progra~1/cygwin/bin/perl.exe -w
use strict;
use CGI;
use CGI::Session qw/-ip_match/;

my $cgi=CGI->new;
my $sid=$cgi->cookie('CGISESSID')||$cgi->param('CGISESSID')||undef;
#1.cookieからCGISESSIDを探す
#2.cookieから取れなかったらurlパラメータを探す.
#3.どちらも取得できなかったらundef.
my $session=CGI::Session->new(undef,$sid,{Directory=>'./.session'});
#4.取得したセッションidが有効ならそのまま.無効なら別のidを発番.

print $cgi->header(-charset=>'UTF-8'),
      $cgi->start_html(-lang=>'ja',
                       -encoding=>'UTF-8',
                       -title=>'CGI.pm使用/遷移先');
if(defined $sid && $sid eq $session->id){
#cookieかurlパラメータから値を取得でき,かつ有効なid
  print $cgi->p('セッションは有効<br/>',
                'session id: '.$session->id.'<br/>',
                'name: '.$session->param('name'));
}
elsif(defined $sid && $sid ne $session->id){
#cookie,またはurlパラメータから値を取得できた.しかしidとしては無効
  print $cgi->p('セッションは無効<br/>',
                '$sid: '.$sid.'<br/>',
                '$session->id: '.$session->id);
  #不要なidはさっさと消去
  #先にcloseをしないと,deleteで
  #'(in cleanup) could not flush: Couldn't unlink .session/cgisess_CGISESSID'
  #が発生する.エラーが出てもファイルは消える.
  #closeは遅いらしい
  $session->close;
  $session->delete;
}
else{
#cookie,またはurlパラメータから値を取得できない.
  print $cgi->p('セッションは無効<br/>',
                '$sid: undefined'),
}
print $cgi->end_html;
#end
        

セッションidの取得箇所をもう少し説明する.以下のコードである.

my $sid=$cgi->cookie('CGISESSID')||$cgi->param('CGISESSID')||undef;
    

cookieとurlパラメータからセッションidを取得しているが,cookieメソッドやparamメソッドはCGI.pmのものである.cookieを優先し,cookieでセッションidが取得できなかったらurlパラメータを探している.cookie・urlパラメータ両方にセッションidがなかった場合はundefとなる.

cookieメソッド・paramメソッドで取得したセッションidには,有効なものと無効なものがある点に注意が必要である.遷移元画面で有効期限を1分に設定している.これを越えた場合,セッションidは取得できたとしても,無効なidとなる.有効期限以外にも,セッションidを改竄した場合はセッションidは無効となる.urlパラメータもcookieも書換えることは簡単だ.セッションidの有効・無効の判定をする必要がある.それが次のコードとそれに続く条件判定である.cookie・urlパラメータから取得したidと,CGI::Sessionが生成したインスタンスのidを比べることで有効・無効を判定できる.

my $sid=$cgi->cookie('CGISESSID')||$cgi->param('CGISESSID')||undef;
my $session=CGI::Session->new(undef,$sid,{Directory=>'./.session'});
#4.取得したセッションidが有効ならそのまま.無効なら別のidを発番.
if(defined $sid && $sid eq $session->id){
#cookieかurlパラメータから値を取得でき,かつ有効なid
  <<省略>>
}
elsif(defined $sid && $sid ne $session->id){
#cookie,またはurlパラメータから値を取得できた.しかしidとしては無効
  <<省略>>
  $session->close;
  $session->delete;
}
else{
#cookie,またはurlパラメータから値を取得できない.
  <<省略>>
}
    

今回のコードでは,セッションidの有効・無効の組合せを以下の3種類に分類している.

  1. セッションidが取得可,かつ有効(正常ケース)
  2. セッションidが取得可,かつ無効(有効期限切れやidの改竄)
  3. セッションidが取得不可

「セッションidが取得可,かつ有効(正常ケース)」な場合,cookie・urlパラメータから取得したセッションid($sid)と,そのセッションidを使って生成したCGI::Sessionのインスタンスのidは同一値を持っている.

「セッションidが取得可,かつ無効(有効期限切れやidの改竄)」な場合,cookie・urlパラメータから取得したセッションid($sid)を使ってCGI::Sessionのインスタンスは生成できない.CGI::Sessionは指定した無効なidではなく,有効なidを持った新しいセッションインスタンスを生成する.したがって,cookie・urlパラメータから取得したidと新しいセッションのidは異なる.また,このケースの場合,新しいセッションidをこのプログラムの中で生成する.このidはプログラム終了後に削除したほうが安全である.セッションオブジェクトのclose/deleteメソッドを使用する.

cookie・urlパラメータ両方からセッションidが取得できなかった場合,「セッションidが取得不可」に該当する.メッセージを出して終了である.

CGI.pmを使わない場合

CGI::Session.pmはCGI.pmがなくても使用可能である.CGI.pmを使わない場合のソースを以下に挙げる.最初に結論を言うと,CGI.pmを使ったほうがはるかに楽だ.

#session-cgi.pl
#!c:/progra~1/cygwin/bin/perl.exe -w
use strict;
use CGI::Session qw/-ip_match/;

my $session=CGI::Session->new(undef,undef,{Directory=>'./.session'});
#ディレクトリ.sessionは予め作っておく
$session->expire('+1m'); #有効期限は1分間
$session->param('name','john'); #セッション経由で引き渡す項目と値
my $sid=$session->id;
my $name=$session->param('name');
print<<END
Content-Type: text/html; charset=UTF-8

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html
        PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
         "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" lang="ja" xml:lang="ja">
  <head>
    <title>CGI.pm未使用/cgiパラメータ</title>
  </head>
  <body>
    <p>
      session id: $sid<br/>
      name: $name<br/>
      <a href="http://localhost/cgi-bin/session2.pl?CGISESSID=$sid">next</a>
    </p>
  </body>
</html>
END
#end

#session-cookie.pl
#!c:/progra~1/cygwin/bin/perl.exe -w
use strict;
use CGI::Session qw/-ip_match/;

my $session=CGI::Session->new(undef,undef,{Directory=>'./.session'});
$session->expire('+1m'); #有効期限は1分間
$session->param('name','john'); #セッション経由で引き渡す項目と値
my $sid=$session->id;
my $name=$session->param('name');

#cookieの有効期間を1分に設定.これをやらないと,遷移先の画面ではurlパラメータにセッションidがあっても,期限の切れたcookieのセッションidを優先する.
my($sec,$min,$hour,$day,$month,$year,$wday)=gmtime(time+60); #有効期限を1分に設定
my @week=('Sun','Mon','Tue','Wed','Thr','Fri','Sat');
my @months=('Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec');
my $expires=sprintf("%s, %02d\-%s\-%04d %02d:%02d:%02d GMT",
                    $week[$wday],$day,$months[$month],$year+1900,$hour,$min,$sec);

print<<END
Set-Cookie: CGISESSID=$sid; expires=$expires
Content-Type: text/html; charset=UTF-8

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html
        PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
         "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" lang="ja" xml:lang="ja">
  <head>
    <title>CGI.pm未使用/cookie</title>
  </head>
  <body>
    <p>
      session id: $sid<br/>
      name: $name<br/>
      <a href="http://localhost/cgi-bin/session2.pl">next</a>
    </p>
  </body>
</html>
END
#end

#session.pl
#!c:/progra~1/cygwin/bin/perl.exe -w
use strict;
use CGI::Session qw/-ip_match/;

my $sid=undef;
if(defined $ENV{'HTTP_COOKIE'}){
  ($sid)=$ENV{'HTTP_COOKIE'}=~/CGISESSID=(.+)/;
}
elsif(defined $ENV{'QUERY_STRING'}){
  ($sid)=$ENV{'QUERY_STRING'}=~/CGISESSID=(.+)/;
}
my $session=CGI::Session->new(undef,$sid,{Directory=>'./.session'});

print qq(Content-Type: text/html; charset=UTF-8

  <?xml version="1.0" encoding="UTF-8"?>
  <!DOCTYPE html
        PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
         "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
  <html xmlns="http://www.w3.org/1999/xhtml" lang="ja" xml:lang="ja">
    <head>
      <title>CGI.pm未使用/遷移先</title>
    </head>
    <body>);
if(defined $sid && $sid eq $session->id){
#cookieかcgiパラメータから値を取得でき,かつ有効なid
  my $sid=$session->id;
  my $name=$session->param('name');
  print qq(
    <p>
      セッションは有効<br/>
      session id: $sid<br/>
      name: $name
    </p>);
}
elsif(defined $sid && $sid ne $session->id){
#cookieかcgiパラメータから値を取得できたが,idは無効
  my $id=$session->id;
  print qq(
    <p>
      セッションは無効<br/>
      \$sid: $sid<br/>
      \$session->id: $id<br/>
    </p>);
  #不要なidはさっさと消去
  #先にcloseをしないとdeleteで
  #'(in cleanup) could not flush: Couldn't unlink .session/cgisess_CGISESSID'
  #が発生する.エラーが出てもファイルは消える.
  #closeは遅いらしい
  $session->close;
  $session->delete;
}
else{
#cookieかcgiパラメータから値を取得でない
  print qq(
    <p>
      セッションは無効<br/>
      \$sid: undefined
    </p>);
}
print qq(
    </body>
  </html>);
#end
        

参考

Japanized Perl Resources Project
CGI::Session.pmのマニュアルCGI.pmのマニュアルを参照できる.
ミスティーネットPerl・CGI講座ミスティーネットPerl・CGI講座
CGI.pmを使わない場合のクッキーの使い方等.
CPAN 初級
CPANの基本的な使い方の説明