Laravel - 为 WEB 艺术家创造的 PHP 框架。

PHP THAT DOESN'T HURT. CODE HAPPY & ENJOY THE FRESH AIR.

Laravel logo

Laravel新手快速入门福利2

###翻译简介 此文为 [Getting Started With Laravel 4,chapter 4] 翻译

此章为laravel新手快速入门福利1的后续篇,本章意在前章的基础上增加用户登录,权限审核的功能。由于原文中代码有很多错误,可能未一一纠正,读者按其中代码做很可能出错,在本文后续修改中会尽量纠正出现的错误,读者发现错误可以给我发送email:xsmyqf@qq.com

下面是我的源代码地址:https://github.com/xsmyqf/startlaravel.git

其中userauth分支即为本章代码。

由于篇幅较长,译者水平有限,难免会有一些错误,而且一些翻译也不地道,希望大家指正,如果在本文中有一些错误或者有一些翻译方面的建议,请发邮件到xsmyqf@qq.com。如果您在做的过程中程序出现错误,可以下载例示程序纠察自己错误。

###本章摘要

在这章中,我们通过添加一个简单身份验证机制和解决基于现在代码的一些安全问题来完善我们上一章中的应用。这样做时(In doing so),你将会学到:

1.如何配置和实用Auth类。 2.过滤器和如何将他们应用到具体路由。 3.在web应用中最普遍的安全脆弱点(vulnerabilities)。 4.Laravel如何帮你写更多的安全代码。

###验证用户 在web应用中,允许用户注册然后登录是一种极普遍的通用特性。Yet,PHP 并不强行规定(dictate in)它应该是哪种方法,也不给任何帮助器去完成它。这导致了完全不同的创造和时常不稳定的各种各样验证用户身份和限制用户到某一具体页面的方法。从这方面来看,Laravel提供给你不同的工具来使这些特性更安全容易集成(integrate)。Laravel借助于Auth类和一个我们还没有涉及的函数,route filters来实现这些特性。

###创建用户模型 首先,我们需要去定义将要展示我们应用用户的模型。Laravel已经提供给你一些朴实的默认设置在app/config/auth.php,在这你可以改变模型或者用来存储你账户的表。

它还有一个(It also comes with)已经存在的User模型在app/models/User.php。为了应用的目标,我们准备稍微简化(simplify)下它,移除一些类变量,然后增加一些新的方法让它能与Cat模型交互。


use Illuminate\Auth\UserTrait;
use Illuminate\Auth\UserInterface;
use Illuminate\Auth\Reminders\RemindableTrait;
use Illuminate\Auth\Reminders\RemindableInterface;

class User extends Eloquent implements UserInterface, RemindableInterface {

 use UserTrait, RemindableTrait;

 /**
  * The database table used by the model.
  *
  * @var string
  */
 protected $table = 'users';
protected $fillable =array('username','password','is_admin','remember_token');
 /**
  * The attributes excluded from the model's JSON form.
  *
  * @var array
  */
 protected $hidden = array('password', 'remember_token');
        public function getAuthIdentifier() {
return $this->getKey();
}
public function getAuthPassword() {
return $this->password;
}
public function cats(){
return $this->hasMany('Cat');
}
public function owns(Cat $cat){
return $this->id == $cat->owner;
}
public function canEdit(Cat $cat){
return $this->is_admin or $this->owns($cat);
}

}

首先请注意到model完成UserInterface接口。

记住一个接口并不给任何的实现细节。 它只不过是(It is nothing more than)一个约定,这个约定详细说明(specify)实现它的类应该定义的方法名,在这里(in this case),是getAuthIdentifier() 和getAuthPassword()。这些方法被Laravel内部使用当验证一个用户时。下一个方法,cats(),只是定义和Cat模型的has many(一对多)关系。最后两个方法被用来检测一个给定的Cat实例是否被拥有和能否被当前用户编辑。

###创建必要的数据库表 既然我们已经定义了User模型,我们需要为它创建数据库表然后改变已经存在的cats表去添加关于owner的信息。通过创建一个新的迁移开始:

$ php artisan migrate:make create_users

然后用需要的数据库表列来定义up()方法。

public function up(){
Schema::create('users', function($table){
$table->increments('id');
$table->string('username');
$table->string('password');
$table->string('remember_token');
$table->boolean('is_admin');
$table->timestamps();
});
Schema::table('cats', function($table){
$table->integer('user_id')->nullable()->references('id')->on('users');
});
}

通过上面的(preceding)代码,我们为我们应用中的用户创建了一个新的表,这个表有一个用户名,一个密码和一个显示用户是否是管理员的标志。我修改了一下之前的cats表,增加了一列存储猫所属者id的字段(user_id)。这次,我们也参考和使用了用来在表中创建外键约束的方法。外键帮你保证数据的一致性(比如,你将不能把cat分配到一个不存在的用户)。

去逆向迁移的代码只是简单的去移除外键约束和相关列,然后删掉users表:

public function down(){
Schema::table('cats', function($table){
$table->dropForeign('cats_user_id_foreign');
$table->dropColumn('user_id');
});
Schema::drop('users');
}

接下来,我们准备一个数据库填充去为我们的应用创建两个用户,其中的一个将是管理员。

class UsersTableSeeder extends Seeder {
public function run(){
User::create(array(
'username' =>'admin',
'password' => Hash::make('hunter2'),
    'is_admin' => true
        ));
User::create(array(
'username' =>'scott', 'password' => Hash::make('tiger'),
'is_admin' => false)
);
}
}

一旦你已经在一个名为app/database/seeds/UsersTableSeeder.php新文件中保存了这些代码,不要忘记在主DatabaseSeeder类中去调用。

小提示:Laravel会用Hash::make帮助器(使用Bcrypt去创建一个复杂的混编)将所有的密码散列混编,你不能用明文的方式去存储密码或者用弱的运算规则去混编他们,比如md5或者sha1。

去执行这个迁移然后同时填充数据库,请输入:

$ php artisan migrate && php artisan db:seed

###验证用户的路由和视图 让我们来看看新的路由和视图。我们将修改一下(making some amends to)the master 布局(app/views/master.blade.php)去显示给访客的登录连接和已经登陆的用户的登出链接。我们用Auth::check()方法去检测一个用户是否登录:

<div class="container">
<div class="page-header">
<div class="text-right">
@if(Auth::check())
Logged in as
<strong>{{{Auth::user()->username}}}</strong>
{{link_to('logout', 'Log Out')}}
@else
{{link_to('login', 'Log In')}}
@endif
</div>
@yield('header')
</div>
@if(Session::has('message'))
<div class="alert alert-success">
{{Session::get('message')}}
</div>
@endif
@if(Session::has('error'))
<div class="alert alert-warning">
{{Session::get('error')}}
</div>
@endif
@yield('content')
</div>

这段代码替换掉了之前模板文件中的

内容,一个在header下面显示错误的区域也被包含进来。

显示登录表格的路由不能更简单了:

Route::get('login', function(){
return View::make('login');
});

小提示:如果你对为什么Laravel使用在各种地方使用make好奇,它只是去保持php 5.3的兼容,不再直接支持访问实例类成员,因此,并不让你写return new View('login');

处理登录尝试的路由会只传username和password输入值给Auth::atempt方法。当这个方法返回true,我们只是简单的将访问者重定向到预期的位置。如果这失败,我们通过有输入值和错误信息的Redirect::back()重定向用户回到他来的位置。

Route::post('login', function(){
if(Auth::attempt(Input::only('username', 'password'))) {
return Redirect::intended('/');
} else {
return Redirect::back()
->withInput()
->with('error', "Invalid credentials");
}
});

但是Laravel如何知道我们预期位置是什么?如果打开app/filters.php然后看auth过滤器,你将会看到它使用Redirect::guest()方法重定向访客到登录路由。这个方法存储需要的路径在一个session变量(这个变量稍后会被intended()方法使用)里,如果没有任何请求路径在session的信息中,传到这个方法里的参数是用户应该被重定向的回滚路由。

小提示:注意那有个叫guest的过滤器和auth相反,你可以在登录路由使用他们如果你希望阻止登陆进的用户进入它。如果这些过滤器正确使用(in place),已经登录的用户会被重定向到主页。你可以通过改变app/filters.php来改变行为。

这个登录视图,在app/views/login.blade.php里,是一个简单的表:

@extends('master')
@section('header')<h2>Please Log In</h2>@stop
@section('content')
{{Form::open()}}
<div class="form-group">
{{Form::label('Username')}} {{Form::text('username')}}
</div>
<div class="form-group">
{{Form::label('Password')}} {{Form::password('password')}}
</div>
{{Form::submit()}}
{{Form::close()}}
@stop

下面是登录表的效果图,你可以到书里查看。

我们需要创建最后一个可以处理登出的路由。所有它需要做的是调用Auth::logout(),然后重定向用户到主页同时发送条消息:

Route::get('logout', function(){
Auth::logout();
return Redirect::to('/')
->with('message', 'You are now logged out');
});

然后,我们需要包裹需要身份验证的路由在一个路由组里,如下:

Route::group(array('before'=>'auth'), function(){
Route::get('cats/create', function(){...});
Route::post('cats', function(){...});
...
});

每一个到这些路由的请求都必须先执行auth过滤器。我们需要去保护这个PUT和POST路由通过添加一个检查当前登入用户是否被允许编辑页面的条件:

Route::put('cats/{cat}', function(Cat $cat) {
if(Auth::user()->canEdit($cat)){
$cat->update(Input::all());
return Redirect::to('cats/' . $cat->id)
->with('message', 'Successfully updated profile!');
} else {
return Redirect::to('cats/' . $cat->id)
->with('error', "Unauthorized operation");
}
});

在这个视图里,我们可以用如下条件去决定一个用户是否可以看到编辑和删除的链接:

@if(Auth::check() and Auth::user()->canEdit($cat))
Edit link | Delete link
@endif

最后,我们需要去改变POST/cats路由来确保当一个新的Cat实例被创建的时候我们保存了当前用户的标识符:

Route::post('cats', function(){
$cat = Cat::create(Input::all());
$cat->user_id = Auth::user()->id;
if($cat->save()){
return Redirect::to('cats/' . $cat->id)
->with('message', 'Successfully created profile!');
} else {
return Redirect::back()
->with('error', 'Could not create profile');
}
});

$cat->save()的调用结果如果当对象被插入到数据库中会返回true,如果哪有一个错误,以至于我们用它相应地去重定向用户,就会返回false。

###验证用户输入 我们的应用还有一个重大的缺点:它并不在用户提交的数据上做任何有效验证。当你会到处用一系列有正则表达式的条件在原生php中时,Laravel提供了更直接(straightforward)和强壮(robust)方法来是现它。

验证通过传递一个带有输入数据的数组和带验证规则的数组给Validator::make($data, $rules)方法被执行。在我们应用的情况里,如下是我们可以写的规则:

$rules = array(
'name' => 'required|min:3', // Required, > 3 characters
'date_of_birth' => array('required, 'date') // Must be a date
)

多重验证规则被'|'分离,但是他们也可以被传递到一个数组中。Laravel提供了超过30种不同的验证规则,然后他们的文档在: http://laravel.com/docs/validation#available-validation-rules

如下是我们对form表提交的数据检查这些规则:

$validation_result = Validator::make( Input::all(),$rules);

你可以让你的应用起作用基于$validation_ result->fails()的输出。如果调用这个方法返回true,使用 $validation_result->messages(),你会检索出一个包含错误信息的对象,然后这个对象被附到将表格发回给用户的重路由上。

return Redirect::back()
->with('messages', $validation_result->messages());

因为每个字段会有0或者更多验证错误,你将用一个条件和一个循环遵循按照如下方法去显示这些信息:

if($messages->has('name')){    
foreach ($messages->get('name') as $message){
echo $message;
}
}

你也可以使用一个叫Ardent的工具,它继承了Eloquent,并且允许你直接在模型里去写验证法则:

https://github.com/laravelbook/ardent

###保护你的应用 在不友好(hostile)的环境中(这样的环境中充满了无情的自动程序和存心不良的用户)部署你的应用前,有一堆必须注意和考虑的安全问题。在这部分,我们将讨论web应用的几个普遍攻击点,然后学习Laravel如何在保护你的应用不受这些攻击侵扰。因为一个框架不能保护你不受所有攻击侵害,我们将看如何阻止几个普遍的陷阱(pitfall)。

###伪造跨网站请求 Cross-site request forgery(CSRF)攻击是通过一个攻击有意外结果的URL(就是那些执行一个动作而不仅仅显示一些信息的页面)来引导的。我们已经在一定程度减轻CSRF攻击通过阻止Get作为有永久效果的路由(比如DELETE/cats/1)的使用,因为我们并不能通过一个简单的链接或者嵌入在< iframe>元素到达这个路由。然而,如果一个攻击者有能力给他要攻击的受害者发送一个他控制的页面,他可以轻松地使这个受害者提交表到一个特定服务器。如果受害者已经登入了目标服务器,应用将没有办法验证请求的可靠性。

最有效的对策(countermeasure)是当一个表被显示时发行一个令牌(token),然后当表格被提交时检测令牌的有效性。Form::open()和Form::model都自动插入一个隐藏的令牌输入元素。

在我们应用当前表中有几个易受攻击的(vulnerable)要点。首先,所有处理用户输入的URLs并不检查这个CSRF令牌。为了解决这个,我们将POST,PUT和DELETE路由在他们自己的路由组中用一个csrf过滤器分组:

Route::group(array('before'=>'csrf'), function(){ ...} );

我们也可以通过使用 csrf_token()函数在URL中添加的令牌来保护个别的GET路由。

<a href="{{URL::to('logout?_token='.csrf_token())}}">Log out</a>

为了将一个过滤器添加到一个个别的路由上,只需将route的第二个参数转变成一个数组,然后添加过滤器的名字:

Route::get('logout', array('before'=>'csrf', function(){...}));

多重过滤器可以被一个被很多“|”隔开的字符串传递。

Route::get('foo', array('before'=>'auth|csrf', function(){...});

###使内容远离跨网站脚本——XSS XSS攻击经常发生在攻击者有能力把JavaScript代码放到用户浏览的页面时。在我们的应用中,假定猫名没有漏掉,如果我们进入如下这小段代码并将值作为名字,只要在猫名出现的地方,每个访问者都会看到一个alert对话框信息。

Evil Cat <script>alert('Meow!')</script>

尽管这并不是一个非常有害的脚本,它很容易将一个更长的脚本或者链接插入到一个偷取session或者cookie值的外部脚本。去阻止这类攻击,你应该不要信任任何用户提交的数据信息然后避开危险的字符。为了达到这样,你只用将你Blade模板中的变量包含在3个curly braces中就行。

{{{$cat->name}}}

不再执行脚本了,这次< script>标签在页面上被显示因为已经规避尖括号,并让在他们在HTML中以实体展示(&lt;和&gt;)。

###防止SQL注入 在一个SQL查询中当一个应用插入随意的和未过滤的用户输入时,会导致SQL注入。这个用户输入可以来自cookies,server变量或者更频繁的,来自GET或者POST输入变量。这些攻击导致可以获得和修改一般正常情况下无效的数据,有时候也会打断应用的正常函数导致无法运行。

默认地,Laravel将会保护你不受这种攻击侵扰,因为query builder和Eloquent在后台用PHP的数据对象查询(PDO)。PDO用准备好的声明,将会使你安全的传递任何参数不用逃避和审查他们。

在一些情况下,你可能想写更复杂的或者具体数据库的SQl查询。这时可以用DB::raw方法。当用这种方法时,你必须非常小心不要创建任何像如下易受攻击的查询:

Route::get('sql-injection-vulnerable', function(){
$name = "'Bobby' OR 1=1";
return DB::select(
DB::raw("SELECT * FROM cats WHERE name = $name") );
});

为了防止这个查询被SQL注入,你需要重写查询语句,用问号标记来替换参数在查询中,然后将数组里的值作为第二个参数传递给raw方法:

Route::get('sql-injection-not-vulnerable', function(){
$name = "'Bobby' OR 1=1";
return DB::select(
DB::raw("SELECT * FROM cats WHERE name = ?", array($name)));
});

###用mass-assignment时小心 在上章中,我们用mass-assignment,一个方便的特性,它让我们去创造基于表格输入的模型而不用管如何单独地分配每个值。

这个特性应该不管在什么时候都应该小心使用,一个心怀不轨的用户可以在客户端改变表然后添加一个新的输入给他:

<input name="admin" value="1" >

然后,当这个表被提交时,我们可以尝试创建新的模型:

Cat::create(Input::all())

幸好有$fillable数组,它定义了一个可以使用mass assignment 的白名单,调用这个方法可以扔出一个mass-assignment异常。

当然我们也可以用$guarded属性相反地定义一个黑名单。然而,这个选项可能有潜在的危险因为你可能会忘了去更新它在你添加新的字段到模型的时候。

###cookies——默认就是安全的 Laravel通过Cookie类让它很容易被创建,读取,失效。

当你知道所有的cookies会自动地被签名和加密时肯定也会很高兴。这也意味着你将不用Javascript去从客户端读他们。

###在交换敏感数据的时候强制使用https 如果你用HTTP的格式访问你的应用,你最好记住每个被交换信息的字节,包括密码,都是用明文发送的。一个在同网络的攻击者因此可以拦截个人信息,比如session变量,然后假扮你登入。我们阻止这种情况的唯一办法是使用HTTPS。如果已经有一个SSl安装在你的服务器上,Laravel提供了几个帮助器去在http://https://之间切换,然后限制特定路由的进入权限。例如(for instance),你可以定义一个将用户重定向到安全路由的https过滤器,如下:

Route::filter('https', function() {
if (!Request::secure())
return Redirect::secure(URI::current());
});

###总结 在这章中,我们学习了如何利用很多Laravel的工具去添加用户验证特性到一个网站,验证数据,然后阻止常见的安全问题。在下一章中,我们会涉及现代网站的重要方面:测试,Laravel涉及的另一个领域。

关于作者 谢帅
八两半斤,崇尚技术