csrf是什么
CSRF全称为跨站请求伪造(Cross-site request forgery),是一种网络攻击方式,也被称为 one-click attack 或者 session riding。
CSRF攻击利用网站对于用户网页浏览器的信任,挟持用户当前已登陆的Web应用程序,去执行并非用户本意的操作。
csrf验证演示
创建控制器:
$ php artisan make:controller CsrfController
app/Http/Controllers/CsrfController.php 创建两个方法,一个显示表单,一个提交表单:
class CsrfController extends Controller
{
public function form()
{
return view('csrf.form');
}
public function post(Request $request)
{
dd($request->post());
}
}
在 routes/web.php 中定义路由:
Route::get('form', 'CsrfController@form')->name('csrf.form');
Route::post('post', 'CsrfController@post')->name('csrf.post');
创建表单视图:resources/views/csrf/form.blade.php
<div class="container mt-5">
<form method="post" action="{{ route('csrf.post') }}">
<div class="form-group">
<label for="name">用户名</label>
<input type="text" class="form-control" name="name" id="name" placeholder="输入用户名">
</div>
<div class="form-group">
<label for="pwd">密码</label>
<input type="password" class="form-control" name="password" id="pwd" placeholder="输入密码">
</div>
<button type="submit" class="btn btn-primary">提交</button>
</form>
</div>
提交表单,会报419错误:
Request URL: http://127.0.0.1:8000/post
Request Method: POST
Status Code: 419 unknown status
Remote Address: 127.0.0.1:8000
Referrer Policy: no-referrer-when-downgrade
因为表单没有携带csrf验证所需要的token,修改form表单:
第一种写法:
<form method="post" action="{{ route('csrf.post') }}">
{{ csrf_field()}}
<!-- ... -->
</form>
第二种写法:
<form method="post" action="{{ route('csrf.post') }}">
<input type="hidden" value="{{csrf_token()}}" name="_token">
<!-- ... -->
</form>
在vendor/laravel/framework/src/Illuminate/Foundation/helpers.php中:
if (! function_exists('csrf_field')) {
/**
* Generate a CSRF token form field.
*
* @return \Illuminate\Support\HtmlString
*/
function csrf_field()
{
return new HtmlString('<input type="hidden" name="_token" value="'.csrf_token().'">');
}
}
if (! function_exists('csrf_token')) {
/**
* Get the CSRF token value.
*
* @return string
*
* @throws \RuntimeException
*/
function csrf_token()
{
$session = app('session');
if (isset($session)) {
return $session->token();
}
throw new RuntimeException('Application session store not set.');
}
}
刷新页面,解析为以下代码:
<form method="post" action="http://127.0.0.1:8000/post">
<input type="hidden" name="_token" value="TiOLPHQAnStZu0uffo5K8YMbuhrT4jgxbULH6x55">
<div class="form-group">
<label for="name">用户名</label>
<input type="text" class="form-control" name="name" id="name" placeholder="输入用户名">
</div>
<div class="form-group">
<label for="pwd">密码</label>
<input type="password" class="form-control" name="password" id="pwd" placeholder="输入密码">
</div>
<button type="submit" class="btn btn-primary">提交</button>
</form>
再次提交表单,便可打印出表单数据:
array:3 [▼
"_token" => "TiOLPHQAnStZu0uffo5K8YMbuhrT4jgxbULH6x55"
"name" => "123"
"password" => "123"
]
忽略csrf验证
在app/Http/Kernel.php的$middlewareGroups将其屏蔽:
protected $middlewareGroups = [
'web' => [
\App\Http\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class,
// \Illuminate\Session\Middleware\AuthenticateSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
//\App\Http\Middleware\VerifyCsrfToken::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],
'api' => [
'throttle:60,1',
'bindings',
],
];
在app/Http/Middleware/VerifyCsrfToken.php中间件的$except属性中添加要过滤的路由:
protected $except = [
'/post'
];
Illuminate\Foundation\Http\Middleware\VerifyCsrfToken.php
protected function inExceptArray($request)
{
foreach ($this->except as $except) {
if ($except !== '/') {
$except = trim($except, '/');
}
if ($request->fullUrlIs($except) || $request->is($except)) {
return true;
}
}
return false;
}
此时将form表单中的csrf删除,再提交表单,并不会触发419错误:
array:2 [▼
"name" => "123"
"password" => "123"
]
疑问一:VerifyCsrfToken是如何被调用的呢?
打开RouteServiceProvider.php,此文件中加载了routes中的路由文件,针对web的代码如下 :
public function map()
{
$this->mapApiRoutes();
$this->mapWebRoutes();
//
}
/**
* Define the "web" routes for the application.
*
* These routes all receive session state, CSRF protection, etc.
*
* @return void
*/
protected function mapWebRoutes()
{
// 在访问这个这些路由的时候,就会执行中间件组 web 所对应的中间件:Kernel.php
Route::middleware('web')
->namespace($this->namespace)
->group(base_path('routes/web.php'));
}
疑问二:为什么post需要校验csrf,而get不需要?
在Illuminate\Foundation\Http\Middleware\VerifyCsrfToken.php中可看到如下代码:
public function handle($request, Closure $next)
{
if (
$this->isReading($request) ||
$this->runningUnitTests() || $this->inExceptArray($request) ||
$this->tokensMatch($request)
) {
return $this->addCookieToResponse($request, $next($request));
}
throw new TokenMismatchException;
}
/**
* Determine if the HTTP request uses a ‘read’ verb.
*
* @param \Illuminate\Http\Request $request
* @return bool
*/
protected function isReading($request)
{
//此处已判断请求放方式
return in_array($request->method(), ['HEAD', 'GET', 'OPTIONS']);
}
疑问三:前后端分离如何实现?
在Illuminate\Foundation\Http\Middleware\VerifyCsrfToken.php中可看到如下代码:
protected function tokensMatch($request)
{
$token = $this->getTokenFromRequest($request);
return is_string($request->session()->token()) &&
is_string($token) &&
hash_equals($request->session()->token(), $token);
}
/**
* Get the CSRF token from the request.
*
* @param \Illuminate\Http\Request $request
* @return string
*/
protected function getTokenFromRequest($request)
{
$token = $request->input('_token') ?: $request->header('X-CSRF-TOKEN');
if (! $token && $header = $request->header('X-XSRF-TOKEN')) {
$token = $this->encrypter->decrypt($header, static::serialized());
}
return $token;
}
通过上面代码可看到token可从表单中获取亦可从header中获取。前后端分离基本上都是通过在header中追加X-XSRF-TOKEN。
网友评论