Authentication 认证系统使用
Authentication
学习笔记《Laravel Auth 代码阅读》
花了两天研究 Laravel,昨天是通宵达旦搞到将近凌晨5点,基本掌握系统架构,可以说是博大精深!今天继续,主要心思放到整个 Laravel Auth 子系统中来了。
基本流程解读
HTTP本身是无状态,通常在系统交互的过程中,使用账号或者Token标识来确定认证用户,Token 一般会以 Cookie 方式保存到客户端,客户端发送请求时一并将 Token 发回服务器。如果客服端没有 Cookie,通常做法时时在 URL 中携带 Token。也可以 HTTP 头信息方式携带 Token 到服务器。Laravel 提供 Session 和 URL 中携带 token 两种方式做认证。对应配置文件中的 guards
配置的 web、api。
web 认证基于 Session 根据 SessionId 获取用户,provider 查询关联用户;api认证是基于token值交互,也采用users这个provider;
'providers' => [
'users' => [
'driver' => 'eloquent',
'model' => App\User::class,
],
// 'users' => [
// 'driver' => 'database',
// 'table' => 'users',
// ],
],
配置文件 /config/auth.php
中的 provider 是提供用户数据的接口,要标注驱动对象和目标对象,默认值 users
是一套 provider 的名字,采用 eloquent 驱动,对应模类是 App\User
。也可以使用 database 的方式。
Laravel 内置了认证模块,也可以手动安装,命令会生成相关的视图文件:
artisan make:auth
app
+-- Http
| +-- Middleware
| | +-- RedirectIfAuthenticated.php
| +-- Controllers
| +-- Auth
| +-- ForgotPasswordController.php
| +-- LoginController.php
| +-- RegisterController.php
| +-- ResetPasswordController.php
+-- Providers
| +-- AuthServiceProvider.php
+-- User.php
整个认证个模块包括相应的视图文件,一个 RedirectIfAuthenticated
中间件和四个 Controller 文件,一个 User 模型文件,它是 Authenticatable
接口类。另外还有一个 AuthServiceProvider
它是服务容器,为注入认证用户的数据提供者 UserProvider
接口准备的。密码数据是从 Authenticatable
流向 UserProvider
的,前者读取数据,后者负责校验。列如可以尝试这样覆盖 getAuthPassword()
方法来测试数据逻辑,而不是从数据库中读入数据:
public function getAuthPassword(){
return bcrypt("userpass");
}
此方法原本是在 Illuminate\Auth\Authenticatable
定义的,它是 trait Authenticatable 类,是 PHP 多继承的规范。这个方法就是简单地返回 $this->password
,即模型关联的数据表字段 password
的值,数据是通过依赖注入的。
在 app/Http/Kernel.php
注册中间件:
protected $routeMiddleware = [
// ...
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
];
Auth模块从功能上分为用户认证和权限管理两个部分;
Illuminate\Auth
是负责用户认证和权限管理的模块;
Illuminate\Foundation\Auth
提供了登录、修改密码、重置密码等一系统列具体逻辑实现;
Illuminate\Foundation\Auth\AuthenticatesUsers
负责登录视图逻辑
Illuminate\Auth\Passwords
目录下是密码重置或忘记密码处理的小模块;
config\auth.php
认证相关配置文件
RedirectIfAuthenticated 只有 handle()
处理逻辑,但 Auth 类中并没有定义 Auth::guard()
,这是经过 Facades 编程模式,通过 __callStatic()
关联到了 AuthManager
,代码注解中有提示。Facade 是一套配合 Service Container 的静态方法解决方案,是一套设计得非常优雅的机制,是 Laravel 的核心机制之一。
public function handle($request, Closure $next, $guard = null)
{
if (Auth::guard($guard)->check()) {
return redirect('/home');
}
return $next($request);
}
通过获取配置文件的 guards 设置,不同的依赖就会加载进来:
HTTP Basic: /src/Illuminate/Auth/RequestGuard.php
Session: /src/Illuminate/Auth/SessionGuard.php
Token: /src/Illuminate/Auth/TokenGuard.php
这些按标准接口定义的类都有 user()
方法,这个方法就是从数据库获取匹配的授权用户,用户数据是通过 UserProvider 接口提供的,即 /app/Providers/AuthServiceProvider.php
。通过实现此接口可以定制自己的认证逻辑,UserProvider::validateCredentials()
就是检验密码的方法。自带的 /app/User.php
模型就是实现了 Authenticatable
接口的类。
public function user()
{
if ($this->loggedOut) {
return;
}
if (! is_null($this->user)) {
return $this->user;
}
$id = $this->session->get($this->getName());
if (! is_null($id) && $this->user = $this->provider->retrieveById($id)) {
$this->fireAuthenticatedEvent($this->user);
}
if (is_null($this->user) && ! is_null($recaller = $this->recaller())) {
$this->user = $this->userFromRecaller($recaller);
if ($this->user) {
$this->updateSession($this->user->getAuthIdentifier());
$this->fireLoginEvent($this->user, true);
}
}
return $this->user;
}
在 /Illuminate/Routing/Router.php
中已经定义好和认证相关的一组路由,只需在 /routes/web.php
中添加一句 Auth::routes();
就可以注册这些路由,这些路由连接的控制器就前面安装认证模块生成的。
public function auth(array $options = [])
{
// Authentication Routes...
$this->get('login', 'Auth\LoginController@showLoginForm')->name('login');
$this->post('login', 'Auth\LoginController@login');
$this->post('logout', 'Auth\LoginController@logout')->name('logout');
// Registration Routes...
if ($options['register'] ?? true) {
$this->get('register', 'Auth\RegisterController@showRegistrationForm')->name('register');
$this->post('register', 'Auth\RegisterController@register');
}
// Password Reset Routes...
if ($options['reset'] ?? true) $this->resetPassword();
// Email Verification Routes...
if ($options['verify'] ?? false) $this->emailVerification();
}
public function resetPassword()
{
$this->get('password/reset', 'Auth\ForgotPasswordController@showLinkRequestForm')->name('password.request');
$this->post('password/email', 'Auth\ForgotPasswordController@sendResetLinkEmail')->name('password.email');
$this->get('password/reset/{token}', 'Auth\ResetPasswordController@showResetForm')->name('password.reset');
$this->post('password/reset', 'Auth\ResetPasswordController@reset')->name('password.update');
}
public function emailVerification()
{
$this->get('email/verify', 'Auth\VerificationController@show')->name('verification.notice');
$this->get('email/verify/{id}', 'Auth\VerificationController@verify')->name('verification.verify');
$this->get('email/resend', 'Auth\VerificationController@resend')->name('verification.resend');
}
来看第一条路由,是一条命名路由 name()
,执行控制器的方法是 LoginController->showLoginForm()
,这个方法会调出视图 view('auth.login')
,需要自己建立视图文件 \resources\views\auth\login.blade.php
,可以使用命令 php artisan make:auth
来生成。登录表单大概是这样,@csrf
这是默认需要添加的参数,除非在 VerifyCsrfToken.php
中间关闭了校验。
<style>
.frame { width:50%; margin:auto; padding:32px; background: #484848; border-radius: 4px; color:white; }
.filed { padding:16px; border:1px solid #4E6DB0; border-radius: 16px; background: #282828; margin:16px;}
.center { text-align: center; }
</style>
<div class="frame">
<form method="post">
{{ csrf_field() }}
<!-- @csrf -->
<div class="filed">{{ __('E-Mail Address') }}: <input type="text" name="email"></div>
<div class="filed">{{ __('Password') }}: <input type="password" name="password"></div>
<div class="filed center">
<button type="submit">{{ __('Submit') }}</button>
<input type="checkbox" name="remember" id="remember" {{ old('remember') ? 'checked' : '' }}>
<label class="form-check-label" for="remember">{{ __('Remember Me') }}</label>
</div>
</form>
</div>
所有post请求中必须包含一个 @crsf
的字段用以防止跨域攻击,只有通过验证才认为是安全的提交动作,否则会得到 419 Page Expired。打开 app\Http\Middleware\VerifyCsrfToken.php
添加排除规则即可关闭:
protected $except = [
'login',
];
只是,LoginController
类并没有定义这个方法,这个方法定义在 AuthenticatesUsers
中定义的,通过 use
关键字引入 trait Authenticatable,这相当于 PHP 的多继承用法。
use AuthenticatesUsers;
email
和 password
两个变量用于匹配用户,密码生成使用的是 bcrypt()
方法,自带的 DatabaseSeeder.php
中含有初始化数据,按需要去执行命令填充到数据库 artisan db:seed
。
还有一些其他的认证方法:
Auth::check()
判断当前用户是否已认证(是否已登录)
Auth::user()
获取当前的认证用户
Auth::id()
获取当前的认证用户的 ID(未登录情况下会报错)
Auth::attempt(['email' => $email, 'password' => $password], $remember)
尝试对用户进行认证
Auth::attempt($credentials, true)
通过传入 true 值来开启 '记住我' 功能
Auth::once($credentials)
只针对一次的请求来认证用户
Auth::login($user)
Auth::login($user, true)
// Login and "remember" the given user...
Auth::login(User::find(1), $remember)
登录一个指定用户到应用上
Auth::guard('admin')->login($user)
Auth::loginUsingId(1)
登录指定用户 ID 的用户到应用上
Auth::loginUsingId(1, true)
// Login and "remember" the given user...
Auth::logout()
使用户退出登录(清除会话)
Auth::validate($credentials)
验证用户凭证
Auth::viaRemember()
是否通过记住我登录
Auth::basic('username')
使用 HTTP Basic Auth 的基本认证方式来认证
Auth::onceBasic()
执行「HTTP Basic」登录尝试
Password::remind($credentials, function($message, $user){})
发送密码重置提示给用户
Adding Custom Guards
namespace App\Providers;
use App\Services\Auth\JwtGuard;
use Illuminate\Support\Facades\Auth;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
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']));
});
}
}
在 /config/auth.php
中配置自定义的 Guards:
'guards' => [
'api' => [
'driver' => 'jwt',
'provider' => 'users',
],
],
Closure Request Guards
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();
});
}
在 /config/auth.php
中配置自定义的 Guards:
'guards' => [
'api' => [
'driver' => 'custom-token',
],
],
Adding Custom User Providers
如果不使用传统的关系数据库,可以自定义 Provider 可以实现自己的认证逻辑,如实现一个 riak Provider:
namespace App\Providers;
use Illuminate\Support\Facades\Auth;
use App\Extensions\RiakUserProvider;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
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'));
});
}
}
在 /config/auth.php
中配置自定义的 riak:
'providers' => [
'users' => [
'driver' => 'riak',
],
],
'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'users',
],
],
The User Provider Contract
Illuminate\Contracts\Auth\UserProvider
contract 接口定义:
namespace Illuminate\Contracts\Auth;
interface UserProvider {
public function retrieveById($identifier);
public function retrieveByToken($identifier, $token);
public function retrieveByCredentials(array $credentials);
public function updateRememberToken(Authenticatable $user, $token);
public function validateCredentials(Authenticatable $user, array $credentials);
}
retrieveById()
, retrieveByToken()
, and retrieveByCredentials()
方法返回的对象需要实现 Authenticatable 接口。
The Authenticatable Contract
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();
}
Events 认证事件
认证过程中(包括注册、忘记密码),定义了相关事件,实现自己的 EventServiceProvider
进行监听:
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\Logout' => ['App\Listeners\LogSuccessfulLogout', ],
'Illuminate\Auth\Events\Lockout' => ['App\Listeners\LogLockout', ],
'Illuminate\Auth\Events\PasswordReset' => ['App\Listeners\LogPasswordReset', ],
];