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,可信息依然保留了。
本章结束