Chinaunix首页 | 论坛 | 博客
  • 博客访问: 385786
  • 博文数量: 69
  • 博客积分: 1984
  • 博客等级: 上尉
  • 技术积分: 953
  • 用 户 组: 普通用户
  • 注册时间: 2007-03-28 00:43
个人简介

学无所长,一事无成

文章分类

全部博文(69)

文章存档

2015年(19)

2014年(14)

2013年(9)

2012年(17)

2010年(10)

我的朋友

分类:

2010-08-23 16:26:46

Catalyst::Manual::Tutorial::05_Authentication
学习笔记 (2010-08-23 星期一) -- 身份认证
 
文章来源:
 
概述:
 
本章分成两部分:
  • 1) basic, cleartext authentication and
  • 2) hash-based authentication.
 
基本身份验证:
 
 
sql 脚本如下:
 

--
-- Add user and role tables, along with a many-to-many join table
--
PRAGMA foreign_keys = ON;
CREATE TABLE user (
        id INTEGER PRIMARY KEY,
        username TEXT,
        password TEXT,
        email_address TEXT,
        first_name TEXT,
        last_name TEXT,
        active INTEGER
);
CREATE TABLE role (
        id INTEGER PRIMARY KEY,
        role TEXT
);
CREATE TABLE user_role (
        user_id INTEGER REFERENCES user(id) ON DELETE CASCADE ON UPDATE CASCADE,
        role_id INTEGER REFERENCES role(id) ON DELETE CASCADE ON UPDATE CASCADE,
        PRIMARY KEY (user_id, role_id)
);
--
-- Load up some initial test data
--
INSERT INTO user VALUES (1, 'test01', 'mypass', 't01@na.com', 'Joe', 'Blow', 1);
INSERT INTO user VALUES (2, 'test02', 'mypass', 't02@na.com', 'Jane', 'Doe', 1);
INSERT INTO user VALUES (3, 'test03', 'mypass', 't03@na.com', 'No', 'Go', 0);
INSERT INTO role VALUES (1, 'user');
INSERT INTO role VALUES (2, 'admin');
INSERT INTO user_role VALUES (1, 1);
INSERT INTO user_role VALUES (1, 2);
INSERT INTO user_role VALUES (2, 1);
INSERT INTO user_role VALUES (3, 1);

执行脚本

$ sqlite3 myapp.db < myapp02.sql


Add User and Role Information to DBIC Schema

$ script/myapp_create.pl model DB DBIC::Schema MyApp::Schema \
    create=static components=TimeStamp dbi:SQLite:myapp.db \
    on_connect_do="PRAGMA foreign_keys = ON"
 exists "/root/dev/MyApp/script/../lib/MyApp/Model"
 exists "/root/dev/MyApp/script/../t"
Dumping manual schema for MyApp::Schema to directory /root/dev/MyApp/script/../lib ...
Schema dump completed.
 exists "/root/dev/MyApp/script/../lib/MyApp/Model/DB.pm"
$
$ ls lib/MyApp/Schema/Result
Author.pm BookAuthor.pm Book.pm Role.pm User.pm UserRole.pmx


运行后 lib/MyApp/Schema/Result  目录下新增三个表,已经自动生成了 has_many ,belongs_to 关系。我们需要手工添加  many_to_many 关系(参考第三章)。

这里我们编辑 lib/MyApp/Schema/Result/User.pm

# many_to_many():
# args:
# 1) Name of relationship, DBIC will create accessor with this name
# 2) Name of has_many() relationship this many_to_many() is shortcut for
# 3) Name of belongs_to() relationship in model class of has_many() above
# You must already have the has_many() defined to use a many_to_many().
__PACKAGE__->many_to_many(roles => 'user_roles', 'role');


这里我们只建立了一个 user -> roles 的单向访问链路,很多时候我们不需要双向访问。
 
 
检查一下我们的工作,启动服务,应该看到如下信息:
 

...
     .-------------------------------------------------------------------+----------.
    | Class | Type |
    +-------------------------------------------------------------------+----------+
    | MyApp::Controller::Books | instance |
    | MyApp::Controller::Root | instance |
    | MyApp::Model::DB | instance |
    | MyApp::Model::DB::Author | class |
    | MyApp::Model::DB::Book | class |
    | MyApp::Model::DB::BookAuthor | class |
    | MyApp::Model::DB::Role | class |
    | MyApp::Model::DB::User | class |
    | MyApp::Model::DB::UserRole | class |
    | MyApp::View::TT | instance |
    '-------------------------------------------------------------------+----------'
    ...

Include Authentication and Session Plugins

编辑 lib/MyApp.pm : 

# Load plugins

use Catalyst qw/
    -Debug
    ConfigLoader
    Static::Simple

    StackTrace

    Authentication

    Session
    Session::Store::FastMmap
    Session::State::Cookie
/;

注:不同版本的 Catalyst::Devel 加载 plugin 的方法各不相同,我们坚持这种写法。

Authentication 用于身份识别,同时使用 session 在 HTTP request 间保存状态信息。

这里我们只需要指定 Authentication 就可以了,不用理会是采用 Authentication::Store 还是 Authentication::Credential ,这是应用程序的事。

修改 Makefile.PL ,包括以上插件:

requires 'Catalyst::Plugin::Authentication';
requires 'Catalyst::Plugin::Session';
requires 'Catalyst::Plugin::Session::Store::FastMmap';
requires 'Catalyst::Plugin::Session::State::Cookie';


unix 平台建议使用:.  或

windows 平台建议使用: ,这是  的子类派生,增加了些额外选项,比如可以使用后端数据库进行存储。

配置 Authentication

有很多方法配置 . 这里我们使用 ,它会我我们自动设置一些缺省值。

编辑 lib/MyApp.pm ,在 __PACKAGE__->setup(); 语句前修改如下:

# Configure SimpleDB Authentication
__PACKAGE__->config->{'Plugin::Authentication'} = {
        default => {
            class => 'SimpleDB',
            user_model => 'DB::User',
            password_type => 'clear',
        },
    };


这些配置也可以放在 myapp.conf 里,但放在 lib/MyApp.pm 里更好,因为这些信息在程序发布时一般不需要修改。(当然,你也可以将 class 和 user_model 定义放在 lib/MyApp.pm 里,将 password_type 放到 myapp.conf 里,便于程序发布时修改口令 )。后面我们会继续使用 lib/MyApp.pm 进行配置,如果你喜欢用 myapp.conf ,可以如下配置:

<Plugin::Authentication>
    <default>
        password_type clear
        user_model DB::User
        class SimpleDB
    </default>
</Plugin::Authentication>

提示:这里有个简便方法可以将 MyApp-config 内容 dump 成 myapp.conf 使用的 格式:

$ CATALYST_DEBUG=0 perl -Ilib -e 'use MyApp; use Config::General;
    Config::General->new->save_file("myapp.conf", MyApp->config);'

当然,一旦你是用以上方法,要注意删除 myapp.conf 中的重复指令,否则程序会出错。

这里我们使用了一个 SimpleDB ,它有很多默认的东西,所以我们不需要指定 username 或者 password 的表字段名,更多可参考:Catalyst::Authentication::Realm::SimpleDB|Catalyst::Authentication::Realm::SimpleDB

Add Login and Logout Controllers

运行指令,创建两个 controller :

$ script/myapp_create.pl controller Login
$ script/myapp_create.pl controller Logout


当然你也可以只创建一个 controller user,让他包含 action login 和 logout。Catalyst 是很灵活的,很多东西你自己都可以定制。
 
编辑 lib/MyApp/Controller/Login.pm ,找到方法 sub index :Path :Args(0) 进行修改(老版本的可能是 sub index : Private ):

sub index :Path :Args(0) {
    my ($self, $c) = @_;
    # Get the username and password from form
    my $username = $c->request->params->{username};
    my $password = $c->request->params->{password};
    # If the username and password values were found in form
    if ($username && $password) {
        # Attempt to log the user in
        if ($c->authenticate({ username => $username,
                               password => $password } )) {
            # If successful, then let them use the application
            $c->response->redirect($c->uri_for(
                $c->controller('Books')->action_for('list')));
            return;
        } else {
            # Set an error message
            $c->stash(error_msg => "Bad username or password.");
        }
    } else {
        # Set an error message
        $c->stash(error_msg => "Empty username or password.");
    }
    # If either of above don't work out, send to the login page
    $c->stash(template => 'login.tt2');
}

要确保删除 sub index 中的这一行:$c->response->body('Matched MyApp::Controller::Login in Login.');

这个 controller 从登录表格读取 username 和 password,成功则跳转到 books list 页面,失败则停留在登录窗口,并显示错误信息。

你可能注意到 "sub default :Path",我们一般建议你只在 MyApp::Controller::Root 中使用 default ,它一般是用来生成 404 页面的。

使用语法 "sub somename :Path :Args(0) {...}" 来匹配 /login。 Path 指定 action 匹配所在 controller 的名字空间。当然 Path 也可以带参数,这样就可以匹配绝对或者相对路径,这里我们使用不带参数的 Path 用来匹配 controller 自身名字空间。至于 index 是我们用来指定匹配 /login ,而非 /login/somethingelse。

修改文件 lib/MyApp/Controller/Logout.pm ,如下: 

sub index :Path :Args(0) {
    my ($self, $c) = @_;

    # Clear the user's state

    $c->logout;

    # Send the user to the starting point

    $c->response->redirect($c->uri_for('/'));
}

类似 login ,注意要删除 sub index 中的: $c->response->body('Matched MyApp::Controller::Logout in Logout.'); 语句。

Add a Login Form TT Template Page

编辑 root/src/login.tt2

[% META title = 'Login' %]

<!-- Login form -->
<form method="post" action="[% c.uri_for('/login') %]">
  <table>
    <tr>
      <td>Username:</td>
      <td><input type="text" name="username" size="40" /></td>
    </tr>
    <tr>
      <td>Password:</td>
      <td><input type="password" name="password" size="40" /></td>
    </tr>
    <tr>
      <td colspan="2"><input type="submit" name="submit" value="Submit" /></td>
    </tr>
  </table>
</form>


Add Valid User Check

我们需要一种强行验证机制,用于当用户未作有效登录时,阻止其访问其他页面。这里我们通过 lib/MyApp/Controller/Root.pm 中的  auto action 来实现。

编辑 lib/MyApp/Controller/Root.pm ,添加:

# Note that 'auto' runs after 'begin' but before your actions and that
# 'auto's "chain" (all from application path to most specific class are run)
# See the 'Actions' section of 'Catalyst::Manual::Intro' for more info.
sub auto :Private {
    my ($self, $c) = @_;
    # Allow unauthenticated users to reach the login page. This
    # allows unauthenticated users to reach any action in the Login
    # controller. To lock it down to a single action, we could use:
    # if ($c->action eq $c->controller('Login')->action_for('index'))
    # to only allow unauthenticated access to the 'index' action we
    # added above.
    if ($c->controller eq $c->controller('Login')) {
        return 1;
    }
    # If a user doesn't exist, force login
    if (!$c->user_exists) {
        # Dump a log message to the development server debug output
        $c->log->debug('***Root::auto User not found, forwarding to /login');
        # Redirect the user to the login page
        $c->response->redirect($c->uri_for('/login'));
        # Return 0 to cancel 'post-auto' processing and prevent use of application
        return 0;
    }
    # User found, so return 1 to continue with processing after this 'auto'
    return 1;
}

正如这篇文章所述 : , application/root 下定义的 auto 会向下传递至每个 controller,所以我们将身份验证的代码放在 lib/MyApp/Controller/Root.pm (或是 lib/MyApp.pm )下的 auto 里面。

Displaying Content Only to Authenticated Users

我们来定义 login 页面上根据用于登录与否显示的不同提示信息,编辑 root/src/login.tt2:

...
<p>
[%
   # This code illustrates how certain parts of the TT
   # template will only be shown to users who have logged in
%]
[% IF c.user_exists %]
    Please Note: You are already logged in as '[% c.user.username %]'.
    You can <a href="[% c.uri_for('/logout') %]">logout</a> here.
[% ELSE %]
    You need to log in to use this application.
[% END %]
[%#
   Note that this whole block is a comment because the "#" appears
   immediate after the "[%" (with no spaces in between).  Although it
   can be a handy way to temporarily "comment out" a whole block of
   TT code, it's probably a little too subtle for use in "normal"
   comments.
%]

   


 
启动服务,访问  试一下,用户名 test01 密码 mypass。
 
重要提示:如果 IE 上身份验证有问题,可能是时间同步问题,你最好同步一下 client 跟 server 的时间。
debian 上可以这样做:
  • sudo aptitude -y install ntpdate
  • sudo ntpdate-debian 或 sudo ntpdate-debian -u
编辑 root/src/books/list.tt2 ,在 标签后添加:
 

...
<p>
  <a href="[% c.uri_for('/login') %]">Login</a>
  <a href="[% c.uri_for(c.controller.action_for('form_create')) %]">Create</a>
</p>

USING PASSWORD HASHES 

本小节,我们引入 SHA-1 算法对明文密码进行加密,其强度足可以抵御密码字典和彩虹表攻击。另外你可以使用 ssl 增进浏览器同服务端的安全,需要安装 Catalyst::Plugin:RequireSSL。

注:本小节可以跳过,不影响以后章节的学习。

Re-Run the DBIC::Schema Model Helper to Include DBIx::Class::EncodedColumn

引入  ,对 column 加密。

$ script/myapp_create.pl model DB DBIC::Schema MyApp::Schema \
    create=static components=TimeStamp,EncodedColumn dbi:SQLite:myapp.db \
    on_connect_do="PRAGMA foreign_keys = ON"


看下文件 lib/MyApp/Schema/Result/User.pm ,有如下内容:

__PACKAGE__->load_components("InflateColumn::DateTime", "TimeStamp", "EncodedColumn");

Modify the "password" Column to Use EncodedColumn

编辑 lib/MyApp/Schema/Result/User.pm ,在语句 "# DO NOT MODIFY THIS OR ANYTHING ABOVE!" 后添加内容: 

# Have the 'password' column use a SHA-1 hash and 10-character salt
# with hex encoding; Generate the 'check_password" method
__PACKAGE__->add_columns(
    'password' => {
        data_type => "TEXT",
        size => undef,
        encode_column => 1,
        encode_class => 'Digest',
        encode_args => {salt_length => 10},
        encode_check_method => 'check_password',
    },
);


以上语句对 password 字段进行了重新定义。encode_class 可以设置为 Digest (使用  ) 或者 Crypt::Eksblowfish::Bcrypt (使用 )。encode_args 用来定义 Digest 类型,这里我们只是指定了 salt 的位数,其实我还可以指定加密算法(缺省的是使用 'SHA-256')以及使用的格式(缺省为'base64',还可以使用 'hex' 和 'binary'),比如类似如下:
 

encode_args => {algorithm => 'SHA-1',
                        format => 'hex',
                        salt_length => 10},


 
编辑文件 set_hashed_passwords.pl :

#!/usr/bin/perl
use strict;
use warnings;
use MyApp::Schema;

my $schema = MyApp::Schema->connect('dbi:SQLite:myapp.db');
my @users = $schema->resultset('User')->all;
foreach my $user (@users) {
    $user->password('mypass');
    $user->update;
}


由于引入了 EncodedColumn ,我们可以使用 $user-check_password($password) 来校验用户口令,使用 $user-update($new_password) 来更新用户口令。
 

$ DBIC_TRACE=1 perl -Ilib set_hashed_passwords.pl

这里使用 -Ilib 参数告诉 perl 到 lib 目录下查找 MyApp::Schema model。

DBIC_TRACE 的输出信息如下:

$ DBIC_TRACE=1 perl -Ilib set_hashed_passwords.pl
SELECT me.id, me.username, me.password, me.email_address,
me.first_name, me.last_name, me.active FROM user me:
UPDATE user SET password = ? WHERE ( id = ? ):
'oXiyAcGOjowz7ISUhpIm1IrS8AxSZ9r4jNjpX9VnVeQmN6GRtRKTz', '1'
UPDATE user SET password = ? WHERE ( id = ? ):
'PmyEPrkB8EGwvaF/DvJm7LIfxoZARjv8ygFIR7pc1gEA1OfwHGNzs', '2'
UPDATE user SET password = ? WHERE ( id = ? ):
'h7CS1Fm9UCs4hjcbu2im0HumaHCJUq4Uriac+SQgdUMUfFSoOrz3c', '3'


数据库里再确认一下:

$ sqlite3 myapp.db "select * from user"
1|test01|38d3974fa9e9263099f7bc2574284b2f55473a9bM=fwpX2NR8|t01@na.com|Joe|Blow|1
2|test02|6ed8586587e53e0d7509b1cfed5df08feadc68cbMJlnPyPt0I|t02@na.com|Jane|Doe|1
3|test03|af929a151340c6aed4d54d7e2651795d1ad2e2f7UW8dHoGv9z|t03@na.com|No|Go|0


可以看到密码强度很高,即便是同一密码,由于引入了 salt ,每回数据库存储的内容也不同。

Enable Hashed and Salted Passwords

编辑 lib/MyApp.pm ,修改如下

# Configure SimpleDB Authentication
__PACKAGE__->config->{'Plugin::Authentication'} = {
        default => {
            class => 'SimpleDB',
            user_model => 'DB::User',
            password_type => 'self_check',
        },
    };

这里设置 self_check ,当访问 password 字段时 Catalyst::Plugin::Authentication::Store::DBIC 会调用 check_password 方法。

Try Out the Hashed Passwords

访问 ,点击 logout 可以退出。或者访问 。

USING THE SESSION FOR FLASH

前面章节我们接触过 flash ,功能同 stash 类似,主要增强在于可以在多次 request 间保持数据。flash 中的信息一旦被读取后,将被清除,不再存在。flash 本身不具备身份验证功能,主要供其他插件使用。可以回顾下 章节。

编辑 lib/MyApp/Controller/Books.pm

sub delete :Chained('object') :PathPart('delete') :Args(0) {
    my ($self, $c) = @_;
    # Use the book object saved by 'object' and delete it along
    # with related 'book_authors' entries
    $c->stash->{object}->delete;
    # Use 'flash' to save information across requests until it's read
    $c->flash->{status_msg} = "Book deleted";
    # Redirect the user back to the list page
    $c->response->redirect($c->uri_for($self->action_for('list')));
}

编辑 root/src/wrapper.tt2

...
<div id="content">
    [%# Status and error messages %]
    <span class="message">[% status_msg || c.flash.status_msg %]</span>
    <span class="error">[% error_msg %]</span>
    [%# This is where TT will stick all of your template's contents. -%]
    [% content %]
</div><!-- end content -->
...

以上只变动 content div 部分,其他保持不变。唯一的修改就是将  一行中的 "|| c.request.params.status_msg" 变为 "c.flash.status_msg" 。

Try Out Flash

通过 login 登录,访问 ,点击 "Return to list" ,删除添加的 Test,注意查看 "Book deleted" 信息,flash 在 redirect 后依然保存了信息。

注意:flash 中的信息在第一次读取后将消失,大多数情况下,这样工作很好,有时却不是你想要的。更多参考信息: 。

Switch To Flash-To-Stash

flash 工作的很好,status_msg || c.flash.status_msg 这种写法就未免丑陋了些。我们使用 flash_to_stash 来自动将 flash 内容复制到 stash中。这样不管我们是直接访问,还是 forward,redirect ,都可以很好工作。启用 flash_to_stash ,可以通过设置lib/MyApp.pm 中的 __PACKAGE__->config 来实现:

__PACKAGE__->config(
        name => 'MyApp',
        # Disable deprecated behavior needed by old applications
        disable_component_resolution_regex_fallback => 1,
        session => { flash_to_stash => 1 },
    );

或者将如下内容添加到 myapp.conf 中:

<session>
    flash_to_stash 1
</session>

本例通过 __PACKAGE__->config 方式修改可能更好。

编辑 root/src/wrapper.tt2 ,修改 status_msg 一行:

<span class="message">[% status_msg %]</span>

测试一下 ,删除一行数据,我们会看到效果依旧,尽管我们没有直接访问 c.flash,可信息依然保留了。

 

本章结束


 

 

阅读(926) | 评论(0) | 转发(2) |
给主人留下些什么吧!~~