Laravel 用户认证
简介
注:想要快速上手?需要在新安装的 Laravel 应用下通过 Composer 安装 laravel/ui 扩展包,然后运行 php artisan ui vue --auth 初始化前端脚手架代码,再运行
npm install && npm run dev
安装前端依赖并编译前端资源,最后运行php artisan migrate
执行数据库迁移生成用户表,此时,功能完备的用户注册登录功能已经完成,在浏览器中访问http://your-app.test/register
即可看到用户注册界面。
在 Laravel 中实现登录认证非常简单。实际上,几乎所有东西 Laravel 都已经为你配置好了。配置文件位于 config/auth.php,其中包含了用于调整认证服务行为的、文档友好的选项配置。
在底层代码中,Laravel 的认证组件由 "guards" 和 "providers" 组成,Guard 定义了用户在每个请求中如何实现认证,例如,Laravel 通过 session guard 来维护 Session 存储的状态和 Cookie。
Provider 定义了如何从持久化存储中获取用户信息,Laravel 底层支持通过 Eloquent 和数据库查询构建器两种方式来获取用户,如果需要的话,我们还可以定义额外的 Provider。
如果看到这些名词觉得不明所以,大可不必太过担心,因为对绝大多数应用而言,只需使用默认认证配置即可,不需要做什么改动。
数据库考量
默认情况下,Laravel 在 app
目录下包含了一个 Eloquent 模型 App\User
,这个模型可以和默认的 Eloquent 认证驱动一起使用。如果应用不使用 Eloquent,可以使用 database 认证驱动,该驱动使用 Laravel 查询构建器与数据库交互。
为 App\User
模型构建数据库表结构的时候,确保 password 字段长度至少有60位。保持默认字符串长度(255)是个不错的选择。
还有,我们需要验证 users 表包含了 remember_token,该字段是个可以为空的字符串类型,字段长度为100,用于在登录时存储应用维护的 "记住我" Session 令牌。
快速入门
路由
Laravel的laravel/ui
软件包提供了一种快速的方法,可以使用一些简单的命令来构架进行身份验证所需的所有路由和视图:
$ composer require laravel/ui:^2.4
$ php artisan ui vue --auth
此命令应用于新应用程序,并将安装布局视图、注册和登录视图,以及所有身份验证端点的路由。生成一个HomeController
还来处理对应用程序面板的登录后请求。
laravel/ui
提供了几个预置的认证控制器,位于 App\Http\Controllers\Auth
命名空间下
RegisterController
用于处理新用户注册;-
LoginController
用于处理用户登录认证; -
ForgotPasswordController
用于处理重置密码邮件链接; -
ResetPasswordController
包含重置密码逻辑。
每个控制器都使用 trait
来引入它们需要的方法。对很多应用而言,我们根本不需要修改这些控制器:
注:如果你的应用不需要注册,可以通过移除新创建的 RegisterController 控制器并编辑路由定义来禁止注册:Auth::routes(['register' => false]);。
创建包含认证代码的应用
如果我们在初始化全新应用的时候就想包含所有认证脚手架代码,可以在创建新应用的时候使用 --auth 指令,这样一来,我们就不需要做上面这些认证初始化工作了:
$ laravel new blog --auth
视图
正如上面所提到的,laravel/ui
包提供的 php artisan ui vue --auth
命令会在 resources/views/auth
目录下创建所有认证需要的视图。
ui
命令还创建了 resources/views/layouts 目录,该目录下包含了应用的基础布局文件。所有这些视图都使用了 Bootstrap CSS 框架,我们也可以根据需要对其进行自定义。
认证
现在我们已经为自带的认证控制器设置好了路由和视图,接下来我们来实现新用户注册和登录认证。可以在浏览器中访问定义好的路由,认证控制器默认已经包含了注册及登录逻辑(通过trait)。
自定义路径
我们已经知道,当一个用户成功进行登录认证后,默认将会跳转到 /home
,可以通过在 RouteServiceProvider
中使用 HOME
常量来自定义认证后的重定向路径:
public const HOME = '/home';
如果需要完全自定义用户认证后返回的响应,可以通过实现 Laravel 提供的空方法 authenticated(Request $request, $user) 来完成:
/**
* The user has been authenticated.
* @param \Illuminate\Http\Request $request
* @param mixed $user
* @return mixed
*/
protected function authenticated(Request $request, $user)
{
return response([
//
]);
}
自定义用户名
默认情况下,Laravel 使用 email 字段进行认证,如果你想要自定义认证字段,可以在 LoginController 中定义 username 方法:
public function username()
{
return 'username';
}
自定义 Guard
我们还可以自定义用于实现用户注册登录的“guard”,要实现这一功能,需要在 LoginController
、RegisterController
和 ResetPasswordController
中定义 guard
方法,该方法将会返回一个 guard 实例:
use Illuminate\Support\Facades\Auth;
protected function guard()
{
return Auth::guard('guard-name');
}
需要注意的是,guard
名称需要在配置文件 config/auth.php 中配置过:
'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'users',
],
'api' => [
'driver' => 'token',
'provider' => 'users',
'hash' => false,
],
],
自定义验证/存储
要修改新用户注册所必需的表单字段,或者自定义新用户字段如何存储到数据库,你可以修改 RegisterController 类。该类负责为应用验证输入参数和创建新用户。
RegisterController
的 validator
方法包含了新用户注册的验证规则,可以按需要自定义该方法。
RegisterController
的 create
方法负责使用 Eloquent ORM
在数据库中创建新的 App\User
记录。当然,也可以基于自己的需要自定义该方法。
获取登录用户
我们可以通过 Auth Facade 访问认证用户:
use Illuminate\Support\Facades\Auth;
// Get the currently authenticated user...
$user = Auth::user();
// Get the currently authenticated user's ID...
$id = Auth::id();
此外,用户通过认证后,还可以通过 Illuminate\Http\Request
实例访问认证用户(类型提示类会通过依赖注入自动注入到控制器方法中):
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class ProfileController extends Controller
{
/**
* Update the user's profile.
* @param Request $request
* @return Response
*/
public function update(Request $request)
{
// $request->user() 返回认证用户实例...
}
}
判断当前用户是否通过认证
要判断某个用户是否登录到应用,可以使用 Auth Facade 的 check 方法,如果用户通过认证则返回 true:
use Illuminate\Support\Facades\Auth;
if (Auth::check()) {
// The user is logged in...
}
注:尽管我们可以使用
check
方法判断用户是否通过认证,但是我们通常的做法是在用户访问特定路由/控制器之前使用中间件来验证用户是否通过认证,想要了解更多,可以查看下面的路由保护。
路由保护
路由中间件可用于只允许通过认证的用户访问给定路由。Laravel 通过定义在 Illuminate\Auth\Middleware\Authenticate
中的 auth
中间件来实现这一功能。由于该中间件已经在 HTTP kernel 中注册,你所要做的仅仅是将该中间件加到相应的路由定义中:
Route::get('profile', function() {
// Only authenticated users may enter...
})->middleware('auth');
当然,如果也可以在控制器的构造方法中调用 middleware
方法而不是在路由器中直接定义实现同样的功能:
public function __construct(){
$this->middleware('auth');
}
重定向未认证用户
当 auth
中间件判定某个用户未认证,会返回一个 JSON 401 响应,或者,如果不是 Ajax 请求的话,将用户重定向到 login 命名路由(也就是登录页面)。
你可以通过更新 app/Http/Middleware/Authenticate.php
文件中的 redirectTo 函数来改变这一行为:
/**
* Get the path the user should be redirected to.
* @param \Illuminate\Http\Request $request
* @return string
*/
protected function redirectTo($request)
{
return route('login');
}
指定一个 Guard
添加 auth
中间件到路由后,还可以指定使用哪个 guard
来实现认证, 指定的 guard
对应配置文件 config/auth.php
中 guards 数组的某个键 :
public function __construct()
{
$this->middleware('auth:api');
}
如果没有指定的话,默认 guard
是 web,这也是配置文件中配置的:
'defaults' => [
'guard' => 'web',
'passwords' => 'users',
],
密码确认
有时候,我们可能想要要求用户在使用应用特定功能之前先确认密码。例如,在用户修改账单设置或者其他敏感信息时通常需要这么做。
为了实现这个功能,Laravel 提供了 password.confirm
中间件,添加这个中间件到某个路由后,当用户访问该路由时,会先将用户重定向到确认密码页面,只有输入了正确的密码才能继续:
Route::get('/settings/security', function () {
// Users must confirm their password before continuing...
})->middleware(['auth', 'password.confirm']);
用户确认密码成功后,会被重定向到之前要访问的路由 /settings/security
。默认情况下,确认密码之后,用户三小时内访问应用 password.confirm
中间件的路由都不需要再次确认密码。可以通过配置项 auth.password_timeout
自定义这个时长:
登录失败次数限制
如果你使用了 Laravel 自带的 LoginController 类, 就已经启用了内置的 Illuminate\Foundation\Auth\ThrottlesLogins trait 来限制用户登录失败次数。默认情况下,用户在几次登录失败后将在一分钟内不能登录,这种限制基于用户的用户名/邮箱地址+IP地址作为唯一键。
手动认证用户
当然,我们也可以不使用 Laravel 自带的认证控制器。如果你选择移除这些控制器,需要直接使用 Laravel 认证类来管理用户认证。别担心,这很简单!
我们可以通过 Auth
Facade来访问认证服务,因此我们需要确保在类的顶部导入了 Auth
Facade,接下来,让我们看看如何通过 attempt
方法实现登录认证:
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class LoginController extends Controller
{
/**
* Handle an authentication attempt.
*
* @param \Illuminate\Http\Request $request
*
* @return Response
*/
public function authenticate(Request $request)
{
$credentials = $request->only('email', 'password');
if (Auth::attempt($credentials)) {
// Authentication passed...
return redirect()->intended('dashboard');
}
}
}
attempt
方法接收键/值对作为第一个参数,数组中的值被用于从数据表中查找对应用户。在上面的例子中,将会通过 email 的值作为查询条件去数据库获取对应用户,如果用户被找到,经哈希运算后存储在数据库中的密码将会和传递过来的经哈希运算处理的密码值进行比较。如果两个经哈希运算的密码相匹配,那么将会为这个用户设置一个认证 Session,标识该用户登录成功。
如果认证成功的话 attempt 方法将会返回 true。否则,返回 false。
重定向器上的 intended 方法将用户重定向到登录之前用户想要访问的 URL,在目标 URL 无效的情况下回退 URI 将会传递给该方法。
指定额外条件
如果需要的话,除了用户邮件和密码之外还可以在认证查询时添加额外的条件,例如,我们可以验证被标记为有效的用户:
if (Auth::attempt(['email' => $email, 'password' => $password, 'active' => 1])) {
// The user is active, not suspended, and exists.
}
注:在这些例子中,并不仅仅限于使用 email 进行登录认证,这里只是作为演示示例,你可以将其修改为数据库中任何其他可用作「username」的字段。
访问指定 Guard 实例
我们可以使用 Auth Facade的 guard
方法指定想要使用的 guard
实例,这种机制允许你在同一个应用中对不同的认证模型或用户表实现完全独立的用户认证。
传递给 guard 方法的 guard 名称对应配置文件 auth.php 中 guards 配置的 admin 键:
if (Auth::guard('admin')->attempt($credentials)) {
//
}
退出
要退出应用,可以使用 Auth Facade的 logout
方法,这将会清除用户 Session 中的认证信息:
Auth::logout();
记住用户
如果想要在应用中提供“记住我”的功能,可以传递一个值为 true 的布尔值作为第二个参数到 attempt 方法(不传的话默认是 false),这样用户登录认证状态就会一直保持直到他们手动退出。当然,你的 users 表必须包含 remember_token 字段,该字段用于存储「记住我」令牌。
if (Auth::attempt(['email' => $email, 'password' => $password], $remember)) {
// The user is being remembered...
}
注:如果你在使用自带的 LoginController 控制器,相应的记住用户逻辑已经通过控制器使用的 trait 实现了。
如果我们在使用"记住"用户功能,可以使用 viaRemember
方法来判断用户是否通过“记住我”Cookie进行认证:
if (Auth::viaRemember()) {
//
}
其它认证方法
认证一个用户实例
如果我们需要将一个已存在的用户实例直接登录到应用,可以调用 Auth 门面的 login 方法并传入用户实例,传入实例必须是 Illuminate\Contracts\Auth\Authenticatable
接口的实现,当然,Laravel 自带的 App\User
模型已经实现了该接口:
Auth::login($user);
// Login and "remember" the given user...
Auth::login($user, true);
当然,可以指定想要使用的 guard 实例:
Auth::guard('admin')->login($user);
通过 ID 认证用户
要通过用户ID登录到应用,可以使用 loginUsingId 方法,该方法接收你想要认证用户的主键作为参数:
Auth::loginUsingId(1);
// Login and "remember" the given user...
Auth::loginUsingId(1, true);
一次性认证用户
我们可以使用 once
方法只在单个请求中将用户登录到应用,而不存储任何 Session 和 Cookie,这在构建无状态的 API 时很有用:
if (Auth::once($credentials)) {
//
}
基于 HTTP 的基本认证
HTTP 基本认证能够帮助用户快速实现登录认证而不用设置专门的登录页面,首先要在路由中加上 auth.basic 中间件。该中间件是 Laravel 自带的,所以不需要自己定义:
Route::get('profile', function() {
// Only authenticated users may enter...
})->middleware('auth.basic');
中间件加到路由中后,当在浏览器中访问该路由时,会自动提示需要认证信息,默认情况下,auth.basic 中间件使用用户记录上的 email 字段作为「用户名」。
FastCGI 上的注意点
如果你使用 PHP FastCGI,HTTP 基本认证将不能正常工作,需要在 .htaccess 文件加入如下内容:
RewriteCond %{HTTP:Authorization} ^(.+)$
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
无状态的 HTTP 基本认证
你也可以在使用 HTTP 基本认证时不在 Session 中设置用户标识 Cookie,这在 API 认证中非常有用。要实现这个功能,需要定义一个调用 onceBasic 方法的中间件。如果该方法没有返回任何响应,那么请求会继续走下去:
<?php
namespace App\Http\Middleware;
use Illuminate\Support\Facades\Auth;
class AuthenticateOnceWithBasicAuth
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle($request, $next)
{
return Auth::onceBasic() ?: $next($request);
}
}
接下来,将 AuthenticateOnceWithBasicAuth
注册到路由中间件并在路由中使用它:
Route::get('api/user', function () {
// Only authenticated users may enter...
})->middleware('auth.basic.once');
退出
如果要手动将用户退出应用,可以使用 Auth Facade上的 logout 方法,该方法会清空用户 Session 中的认证信息:
use Illuminate\Support\Facades\Auth;
Auth::logout();
让其他设备上的 Session 失效
Laravel 还提供了让用户 Session 在除当前设备之外的其他登录设备上失效的机制,这个功能常用在用户修改或者更新密码这种场景,修改密码之后其他设备需要重新认证从而提高安全性。
使用这个功能之前,需要确保 Illuminate\Session\Middleware\AuthenticateSession
中间件在 app/Http/Kernel.php
类的 web 中间件组中存在且没有被注释:
'web' => [
// ...
\Illuminate\Session\Middleware\AuthenticateSession::class,
// ...
],
然后,我们可以使用 Auth Facade 上的 logoutOtherDevices
方法实现在其他设备"退出",该方法要求用户提供登录密码:
use Illuminate\Support\Facades\Auth;
Auth::logoutOtherDevices($password);
当 logoutOtherDevices
方法被调用时,用户在其他设备的 Session 会完全失效,表现在用户界面上就是退出登录了。
注:当结合使用 AuthenticateSession 中间件和 login 路由的自定义路由名称时,必须重写应用异常处理器上的 unauthenticated 方法,以便用户可以被正常重定向到登录页面。
添加自定义 Guard 驱动
我们可以通过 Auth Facade的 extend
方法定义自己的认证 guard
驱动,该功能需要在某个服务提供者的 boot
方法中实现,由于 Laravel 已经自带了一个 AuthServiceProvider
,所以我们将代码放到这个服务提供者中:
<?php
namespace App\Providers;
use App\Services\Auth\JwtGuard;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Auth;
class AuthServiceProvider extends ServiceProvider
{
/**
* Register any application authentication / authorization services.
*
* @return void
*/
public function boot()
{
$this->registerPolicies();
Auth::extend('jwt', function ($app, $name, array $config) {
// Return an instance of Illuminate\Contracts\Auth\Guard...
return new JwtGuard(Auth::createUserProvider($config['provider']));
});
}
}
正如在上面例子中所看到的,传递给 extend
方法的闭包回调需要返回 Illuminate\Contracts\Auth\Guard
的实现实例,该接口包含了自定义认证 guard 驱动需要的一些方法。定义好自己的认证 guard 驱动之后,就可以在配置文件 auth.php 的 guards 配置中使用这个新的 guard 驱动:
'guards' => [
'api' => [
'driver' => 'jwt',
'provider' => 'users',
],
],
闭包请求 Guard
实现一个自定义的、基于 HTTP 请求的认证系统最简单的方式就是使用 Auth:viaRequest 方法,该方法允许你通过单个闭包快速定义认证流程。
首先我们需要在 AuthServiceProvider 的 boot 方法中调用 Auth::viaRequest,viaRequest 方法接收一个 guard 名称作为第一个参数,这个名称可以是任意描述自定义 guard 的字符串,传递到该方法第二个参数应该是一个闭包,该闭包接收 HTTP 请求并返回一个用户实例,如果认证失败的话,则返回 null:
use App\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
/**
* Register any application authentication / authorization services.
*
* @return void
*/
public function boot()
{
$this->registerPolicies();
Auth::viaRequest('custom-token', function ($request) {
return User::where('token', $request->token)->first();
});
}
定义好自定义 guard 后,就可以在 auth.php 配置文件的配置项 guards 中使用这个 guard 了:
'guards' => [
'api' => [
'driver' => 'custom-token',
],
],
添加自定义用户提供者
如果我们没有使用传统的关系型数据库存储用户信息,则需要使用自己的认证用户提供者来扩展 Laravel。我们使用 Auth Facade 上的 provider
方法定义自定义该提供者:
<?php
namespace App\Providers;
use App\Extensions\RiakUserProvider;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Auth;
class AuthServiceProvider extends ServiceProvider
{
/**
* Register any application authentication / authorization services.
*
* @return void
*/
public function boot()
{
$this->registerPolicies();
Auth::provider('riak', function ($app, array $config) {
// Return an instance of Illuminate\Contracts\Auth\UserProvider...
return new RiakUserProvider($app->make('riak.connection'));
});
}
}
通过 provider
方法注册用户提供者后,你可以在配置文件 config/auth.php
中切换到新的用户提供者。首先,定义一个使用新驱动的 provider
:
'providers' => [
'users' => [
'driver' => 'riak',
],
],
然后,可以在我们的 guards 配置中使用这个provider:
'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'users',
],
],
User Provider 接口
Illuminate\Contracts\Auth\UserProvider
实现只负责从持久化存储系统中获取 Illuminate\Contracts\Auth\Authenticatable
实现,例如 MySQL、Riak 等等。这两个接口允许 Laravel 认证机制继续起作用而不管用户数据如何存储或者何种类来展现。
让我们先看看 Illuminate\Contracts\Auth\UserProvider
契约:
<?php
namespace Illuminate\Contracts\Auth;
interface UserProvider
{
public function retrieveById($identifier);
public function retrieveByToken($identifier, $token);
public function updateRememberToken(Authenticatable $user, $token);
public function retrieveByCredentials(array $credentials);
public function validateCredentials(Authenticatable $user, array $credentials);
}
retrieveById
方法通常获取一个代表用户的键,例如 MySQL 数据中的自增ID。该方法获取并返回匹配该ID的 Authenticatable
实现。
retrieveByToken
函数通过唯一标识和存储在 remember_token 字段中的“记住我”令牌获取用户。和上一个方法一样,该方法也返回 Authenticatable 实现。
updateRememberToken
方法使用新的 $token 更新 $user 的 remember_token 字段,新令牌可以是新生成的令牌(在登录是选择“记住我”被成功赋值)或者null(用户退出)。
retrieveByCredentials
方法在尝试登录系统时获取传递给 Auth::attempt 方法的认证信息数组。该方法接下来去底层持久化存储系统查询与认证信息匹配的用户,通常,该方法运行一个带“where”条件($credentials['username'])的查询。然后该方法返回 Authenticatable 的实现。这个方法不应该做任何密码校验和认证。
validateCredentials
方法比较给定 $user
和 $credentials
来认证用户。例如,这个方法比较 $user->getAuthPassword() 字符串和经 Hash::check 处理的 $credentials['password']。这个方法根据密码是否有效返回布尔值 true 或 false。
Authenticatable 接口
既然我们已经探索了 UserProvider
上的每一个方法,接下来让我们看看 Authenticatable
。记住,provider 需要从 retrieveById
和 retrieveByCredentials
方法中返回接口实现:
<?php
namespace Illuminate\Contracts\Auth;
interface Authenticatable
{
public function getAuthIdentifierName();
public function getAuthIdentifier();
public function getAuthPassword();
public function getRememberToken();
public function setRememberToken($value);
public function getRememberTokenName();
}
这个接口很简单, getAuthIdentifierName
方法会返回用户的主键字段名称,getAuthIdentifier 方法返回用户“主键”,在后端 MySQL 中这将是自增ID,getAuthPassword 返回经哈希处理的用户密码,这个接口允许认证系统处理任何用户类,不管是你使用的是 ORM 还是存储抽象层。默认情况下,Laravel app 目录下的 User 类实现了这个接口,所以你可以将这个类作为实现例子。
事件
Laravel 支持在认证过程中触发多种事件,你可以在自己的 EventServiceProvider 中监听这些事件:
/**
* The event listener mappings for the application.
*
* @var array
*/
protected $listen = [
'Illuminate\Auth\Events\Registered' => [
'App\Listeners\LogRegisteredUser',
],
'Illuminate\Auth\Events\Attempting' => [
'App\Listeners\LogAuthenticationAttempt',
],
'Illuminate\Auth\Events\Authenticated' => [
'App\Listeners\LogAuthenticated',
],
'Illuminate\Auth\Events\Login' => [
'App\Listeners\LogSuccessfulLogin',
],
'Illuminate\Auth\Events\Failed' => [
'App\Listeners\LogFailedLogin',
],
'Illuminate\Auth\Events\Validated' => [
'App\Listeners\LogValidated',
],
'Illuminate\Auth\Events\Verified' => [
'App\Listeners\LogVerified',
],
'Illuminate\Auth\Events\Logout' => [
'App\Listeners\LogSuccessfulLogout',
],
'Illuminate\Auth\Events\CurrentDeviceLogout' => [
'App\Listeners\LogCurrentDeviceLogout',
],
'Illuminate\Auth\Events\OtherDeviceLogout' => [
'App\Listeners\LogOtherDeviceLogout',
],
'Illuminate\Auth\Events\Lockout' => [
'App\Listeners\LogLockout',
],
'Illuminate\Auth\Events\PasswordReset' => [
'App\Listeners\LogPasswordReset',
],
];