Как разрешить только одному пользователю одновременно в этом CGI :: Application? - PullRequest
1 голос
/ 19 июля 2011

Я использовал это учебное пособие для создания веб-сайта, но мне бы хотелось, чтобы в любое время в систему мог входить только один пользователь.

Я полагаю, что это изменение должно быть сделано в Login.pm, который я включил, но я не могу понять, где ввести это ограничение.

Обновление

Основываясь на решении scorpio17, у меня теперь есть, что только один пользователь может войти в систему, если пользователь не забудет нажать кнопку «Выйти».

Теперь проблема в том, как состояние $ can_login изменяется по истечении времени сеанса.

Вот обновленные функции.

sub logout : Runmode {
    my $self = shift;
    if ($self->authen->username) {
    $self->authen->logout;
    $self->session->delete; # Delete current session
    }

    # get state of can_login file
    my $file = "lock-can_login.txt";
    open my $fh, '+<', $file or die "can't open $file in update mode: $!\n";
    flock($fh, LOCK_EX) or die "couldn't get lock: $!\n";

    # 1 means a new user can login
    my $can_login = <$fh>;
    chomp $can_login;

    # allow others to login now
    $can_login = !$can_login;

    # write
    seek $fh, 0, 0;
    print $fh "$can_login\n";
    truncate($fh, tell($fh));
    close $fh;

    return $self->redirect($self->query->url);
}

sub one_user {
    my $self = shift;

    # get state of can_login file
    my $file = "lock-can_login.txt";
    open my $fh, '+<', $file or die "can't open $file in update mode: $!\n";
    flock($fh, LOCK_EX) or die "couldn't get lock: $!\n";

    # 1 means a new user can login
    my $can_login = <$fh>;
    chomp $can_login;

    if ($self->authen->is_authenticated && $can_login) {
    # prevent others from logging in
    $can_login = !$can_login;
    } else {
    $self->authen->logout;
    #and redirect them to a page saying "there can be only one!"
    }

    # write
    seek $fh, 0, 0;
    print $fh "$can_login\n";
    truncate($fh, tell($fh));
    close $fh;
}

Может кто-нибудь понять это?

package MyLib::Login;

use strict;

#use lib '/usr/lib/perl5/vendor_perl/5.8.8/';

use base 'CGI::Application';

# shorter URLs
# extract the desired run mode from the PATH_INFO environment variable.
use CGI::Application::Plugin::AutoRunmode;

# wrapper for DBI
#use CGI::Application::Plugin::DBH(qw/dbh_config dbh/);

# a wrapper around CGI::Session.
# maintain state from one page view to the next (provides persistent data).
use CGI::Application::Plugin::Session;

# logging in and out.
# Authentication allows to identify individual users.
use CGI::Application::Plugin::Authentication;

# external redirects in CGI::Application
use CGI::Application::Plugin::Redirect;

# read parameters from config file
use CGI::Application::Plugin::ConfigAuto(qw/cfg/);

# encrypt passphrases
use Digest::MD5 qw(md5_hex);

# authenticate against NIS/LDAP server
use Authen::Simple::LDAP;


sub setup {
    my $self = shift;

    $self->mode_param(
    path_info => 1,         # tell CGI::Application to parse the PATH_INFO environment variable
    param     => 'rm',
    );
}

# most of the initialization is done here
sub cgiapp_init {
    my $self = shift;

    # read config file and store name-value pairs in %CFG
    my %CFG = $self->cfg;

    # where to look for templete files
    $self->tmpl_path(['./templates']);

    # save session data in mysql
    $self->session_config(
    # store sessions in /tmp as files
    CGI_SESSION_OPTIONS => [ "driver:File", $self->query, {Directory=>'/tmp'} ],

    DEFAULT_EXPIRY => '+10m',   # default expiration time for sessions
    );

    # configure authentication parameters
    $self->authen->config(
    DRIVER => [ 'Authen::Simple::LDAP',
            host   => 'ldaps://nms.imm.dtu.dk/dc=ldap,dc=imm,dc=dtu,dc=dk',
            basedn => 'OU=people,DC=ldap,DC=imm,DC=dtu,DC=dk',
    ],

    STORE                => 'Session',          # save login state inside a session
                                                    # If a user is not logged in, but tries to access a
                                                    # protected page, the Authentication plugin will
                                                    # automatically redirect the user to the login page.
                                                    # Once the user enters a valid username and
                                                    # passsword, they get redirected back to the
                                                    # protected page they originally requested.
    LOGOUT_RUNMODE       => 'logout',           # method to use for logging out when session expires

# uncomment the next 3 lines to enable custom build login prompt
#   LOGIN_RUNMODE        => 'login',
#   POST_LOGIN_RUNMODE   => 'okay',             # run mode that gets called after a user successfully logs in
                                                    # figures out which run mode (page) the user really wanted to
                                                    # see, then redirects the browser to that page using http
                                                    # (not https).

#   RENDER_LOGIN         => \&my_login_form,    # generate a login form. Authentication plugin comes with a default 
    );

    # define runmodes (pages) that require successful login:
    # The Login.pm module doesn't define any content - all of the actual web pages are in Simple.pm.
    # 'mustlogin' page is a place-holder. It's a dummy page that forces you to login, but immediately redirects
    # you back to the default start page (usually the index page).
    $self->authen->protected_runmodes('mustlogin');
}


# define mustlogin runmode
sub mustlogin : Runmode {
    my $self = shift;
    my $url = $self->query->url;
    return $self->redirect($url);
}


# switch from https to http. It assumes that the target run mode is stored in a cgi parameter named
# 'destination', but if for some reason this is not the case, it will default back to the index page.
sub okay : Runmode {
    my $self = shift;

    my $url = $self->query->url;
#  my $user = $self->authen->username;
    my $dest = $self->query->param('destination') || 'index';

    if ($url =~ /^https/) {
    $url =~ s/^https/http/;
    }

    return $self->redirect("$url/$dest");
}

# displays the login form
# But first, it checks to make sure you're not already logged in, and second, it makes sure you're connecting with https. If you try to access the login page with http, it will automatically redirect you using https.
sub login : Runmode {
    my $self = shift;
    my $url = $self->query->url;

    my $user = $self->authen->username;
    # is user logged in?
    if ($user) {
    my $message = "User $user is already logged in!";
    my $template = $self->load_tmpl('default.html');
    $template->param(MESSAGE => $message);
    $template->param(MYURL => $url);
    return $template->output;
    } else {
    my $url = $self->query->self_url;
    unless ($url =~ /^https/) {
            $url =~ s/^http/https/;
        return $self->redirect($url);
    }
    return $self->my_login_form;
    }
}

# generate custom login. See templates/login_form.html
sub my_login_form {
    my $self = shift;
    my $template = $self->load_tmpl('login_form.html');

    (undef, my $info) = split(/\//, $ENV{'PATH_INFO'});
    my $url = $self->query->url;

    # 'destination' contains the URL of the page to go to once the user has successfully logged in

    # try to get a value for 'destination' from the CGI query object (in case it was passed as a hidden variable)
    my $destination = $self->query->param('destination');

    # If failed to get from CGI query object, try get destination from PATH_INFO environment variable
    # in case it's being passed as part of the URL
    unless ($destination) {
    if ($info) {
        $destination = $info;
    } else {
        # default to index page
        $destination = "index";
    }
    }

    my $error = $self->authen->login_attempts;

    # insert values into the template parameters
    $template->param(MYURL => $url);
    $template->param(ERROR => $error);
    $template->param(DESTINATION => $destination);

    # generate final html
    return $template->output;
}
# logout method
sub logout : Runmode {
    my $self = shift;
    if ($self->authen->username) {
    $self->authen->logout;
    $self->session->delete; # Delete current session
    }
    return $self->redirect($self->query->url);
}

# error runmode / page
sub myerror : ErrorRunmode {
    my $self = shift;
    my $error = shift;
    my $template = $self->load_tmpl("default.html");
    $template->param(NAME => 'ERROR');
    $template->param(MESSAGE => $error);
    $template->param(MYURL => $self->query->url);
    return $template->output;
}

# called if non-existant runmode/page is accessed. Gives a nicer error message, when typing a wrong url
sub AUTOLOAD : Runmode {
    my $self = shift;
    my $rm = shift;
    my $template = $self->load_tmpl("default.html");
    $template->param(NAME => 'AUTOLOAD');
    $template->param(MESSAGE => "<p>Error: could not find run mode \'$rm\'<br>\n");
    $template->param(MYURL => $self->query->url);
    return $template->output;
}

1;

Обновление 2

Теперь я получаю, что $self->authen->username всегда установлен на undef, когда вызывается режим запуска mustLogin. Это означает, что несколько пользователей могут войти.

Я вставил

  open F, ">/tmp/debug";
  print F Dumper $self->authen->username;
  close F;

там, где возникает проблема.

$self->cfg('SESSIONS_DIR') возвращает правильный путь.

Есть идеи, почему $self->authen->username установлен на undef, когда mustLogin запущен?

sub teardown {
  my $self = shift;

  $self->param('found_a_user', 0);

  CGI::Session->find(
      "driver:File;serializer:yaml",
      sub { my_subroutine($self, @_)},
      {Directory => $self->cfg('SESSIONS_DIR'), UMask => 0600,},
      );

  open F, ">/tmp/debug";
  print F Dumper $self->authen->username;
  close F;

  # get state of can_login file
  open my $fh, '+<', 'can_login.yaml';
  flock($fh, LOCK_EX) or die "couldn't get lock: $!\n";
  my $c = YAML::Syck::LoadFile($fh);

  if ( $self->param('found_a_user') ) {
      # found a logged in user with an unexpired session
      $c->{can_login} = 0;
  } else {
      # did NOT find any logged in users
      $c->{can_login} = 1;
  }

  # write
  my $yaml = YAML::Syck::Dump($c);
  $YAML::Syck::ImplicitUnicode = 1;
  seek $fh,0, SEEK_SET;   # seek back to the beginning of file
  print $fh $yaml . "---\n";
  close $fh;
}

sub my_subroutine {
  my $self = shift;
  my ($session) = @_;  # I don't actually need this for anything here

  if ($self->authen->username) {
    $self->param('found_a_user', 1);
  }

}

1 Ответ

2 голосов
/ 20 июля 2011

Только один пользователь за раз? Это довольно странное требование. Я никогда не делал ничего подобного раньше. Вот один из способов: вам понадобится двоичная переменная состояния, которая называется что-то вроде 'can_login'. Вы можете сохранить это в файле или в базе данных. Инициализируйте его как «true», а затем, как только пользователь успешно войдет в систему, переключите его в «false». Как это сделать? Внутри $ self-> authen-> config () определите значение для POST_LOGIN_CALLBACK. Для этого нужно указать ссылку на код, чтобы проверить значение can_login. Если пользователь аутентифицирован, AND 'can_login' - true, переключите 'can_login' в false, чтобы предотвратить другие входы в систему. Если пользователь аутентифицирован, а 'can_login' имеет значение false, вызовите $ self-> authen-> logout, чтобы выйти из него, и перенаправьте их на страницу, говорящую "может быть только один!" или что угодно. Кроме того - убедитесь, что вы создали значение тайм-аута в сеансе, содержащем информацию для входа в систему, чтобы случайно не заблокировать всех, потому что кто-то забыл выйти из системы. Если вы сохраните значение can_login в файле, вам нужно будет внедрить блокировку файла, чтобы избежать условий гонки. Если вы храните его в базе данных, вам придется выяснить, как передать правильный дескриптор базы данных в код обратного вызова ref (возможно, вставьте его в глобальную переменную в методе setup ()). Ваш режим запуска выхода из системы должен, как обычно, выйти из системы, ПЛЮС переключает значение can_login обратно в true. Код, который обрабатывает устаревшие старые сессии, должен также это делать. Вы можете даже сделать это в отдельном процессе (например, задание cron, которое запускается каждые 5 минут) - истечь старые сеансы, проверить наличие активных входов в систему и, если их нет, убедиться, что 'can_login' истинно.

UPDATE

Что касается обработки устаревших сессий, давайте предположим, что у вас есть что-то вроде этого внутри вашего метода cgiapp_init ():

$self->session_config(
  CGI_SESSION_OPTIONS => [
    "driver:File;serializer:yaml",
    $self->query,
    {Directory => $self->cfg('SESSIONS_DIR'), UMask => 0600,},
  ],

  DEFAULT_EXPIRY => '+1h',
  COOKIE_PARAMS => {
    -path     => '/',
    -httponly => 1,        # help avoid XSS attacks
  },
);

Тогда обычно вам может потребоваться такой метод разрыва:

sub teardown {
  my $self = shift;

  # purge old sessions
  CGI::Session->find(
    "driver:File;serializer:yaml",
    sub {},
    {Directory => $self->cfg('SESSIONS_DIR'), UMask => 0600,},
  );
}

Метод teardown () вызывается в конце каждого режима выполнения. В этом случае все, что он делает, это истекает старые сеансы (см. Документацию по CGI :: Session для более подробной информации - смотрите раздел под методом "find"). Общая форма такова:

find($dsn, \&code, \%dsn_args);

$ dsn и \% dsn_args должны совпадать с тем, что есть в вашей конфигурации сеанса - вот почему я показал мой, например. Coderef здесь не должен ничего делать, потому что find () автоматически вызывает load () для каждого сеанса, и он автоматически удаляет все, что уже истекло - так как это все, что я хочу, ничего больше не требуется. Но вы можете использовать это для проверки входа в систему пользователя. Вам придется сделать что-то вроде этого:

sub teardown {
  my $self = shift;

  $self->param('found_a_user',0);

  CGI::Session->find(
    "driver:File;serializer:yaml",
    sub { my_subroutine( $self, @_ ) },
    {Directory => $self->cfg('SESSIONS_DIR'), UMask => 0600,},
  );

  if ( $self->param('found_a_user') ) {
    # found a logged in user with an unexpired session: set $can_login=0 here
  } else {
    # did NOT find any logged in users - set $can_login=1 here
  }

}

sub my_subroutine {
  my $self = shift;
  my ($session) = @_;  # I don't actually need this for anything here

  if ($self->authen->username) {
    $self->param('found_a_user',1);
  }

}

Обратите внимание, что my_subroutine не является методом, поэтому я должен передать $ self в качестве дополнительного аргумента. Мне нужно, чтобы получить доступ к authen-> username, что будет истинно, только если мы вошли в систему пользователя с не истекшим сеансом. Обратите внимание, что это будет вызываться для каждого сеанса, удаляя старые сеансы по ходу. Если для параметра 'found_a_user' установлено значение 1, то мы знаем, что нашли хотя бы одного активного пользователя и можем при необходимости обновить переменную $ can_login.

...