多字段登录通用解决方案
面临的问题:#
- 登录字段小于或等于 2 个的
- 登录字段大于或等于 2 个的
登录字段不超过两个#
我在网上看到一种相对简单解决方案,但是不能解决所有两个字段的验证:
filter_var($request->input('username'), FILTER_VALIDATE_EMAIL) ? 'email' : 'name'
过滤请求中的表单内容,实现区分 username。弊端显而易见,如果另一个不是 email 就抓瞎了……,下面是另一种通用的解决方案,在 LoginController 中重写 login 方法:
public function login(Requests $request) {
//假设字段是 email
if ($this->guard()->attempt(['email' =>$request->only('username'), 'password' => $request->only('password')]))) {
return $this->sendLoginResponse($request);
}
//假设字段是 mobile
if ($this->guard()->attempt(['mobile' =>$request->only('username'), 'password' => $request->only('password')])) {
return $this->sendLoginResponse($request);
}
//假设字段是 username
if ($this->guard()->attempt(['username' =>$request->only('username'), 'password' => $request->only('password')])) {
return $this->sendLoginResponse($request);
}
return $this->sendFailedLoginResponse($request);
}
可以看到虽然能解决问题,但是显然有悖于 Laravel 的优雅风格!你也可以参照 medz 在 1 楼回复的 方案。卖了这么多关子,下面跟大家分享一下我的解决方案。
登录字段大于或等于三个的(相对复杂一些)#
为了方便理解我画了个大致的流程,只画了我认为重要的部分
-
首先需要自己实现一个
Illuminate\Contracts\Auth\UserProvider
的实现,具体可以参考 添加自定义用户提供器 但是我喜欢偷懒,就直接继承了EloquentUserProvider
,并重写了retrieveByCredentials
方法:public function retrieveByCredentials(array $credentials) { if (empty($credentials)) { return; } $query = $this->createModel()->newQuery(); foreach ($credentials as $key => $value) { if (! Str::contains($key, 'password')) { $query->orWhere($key, $value); } } return $query->first(); }
注意
: 框架源生的是$query->where($key, $value);
,多个字段同时验证,也就是说字段之间是and 的关系
;将$query->where($key, $value);
改为$query->orWhere($key, $value);
就可以了! -
紧接着需要注册自定义的 UserProvider:
class AuthServiceProvider extends ServiceProvider { /** * 注册任何应用认证/授权服务。 * * @return void */ public function boot() { $this->registerPolicies(); Auth::provider('custom', function ($app, array $config) { // 返回 Illuminate\Contracts\Auth\UserProvider 实例... return new CustomUserProvider(new BcryptHasher(), config('auth.providers.custom.model')); }); } }
-
最后我们修改一下 auth.php 的配置就搞定了:
'guards' => [ 'web' => [ 'driver' => 'session', 'provider' => 'custom', ], 'api' => [ 'driver' => 'passport', 'provider' => 'users', ], ], 'providers' => [ 'users' => [ 'driver' => 'eloquent', 'model' => App\Models\User::class, ], 'custom' => [ 'driver' => 'custom', 'model' => App\Models\User::class, ], ],
-
最后看一下 LoginController 的代码:
public function login(LoginRequest $request)
{
$username = $request->get('username');
$result = $this->guard()->attempt([
'username' => $username,
'email' => $username,
'mobile' => $username,
'password' => $request->get('password')]);
if ($result) {
return $this->sendLoginResponse($request);
}
$this->incrementLoginAttempts($request);
return $this->sendFailedLoginResponse($request);
}
现在哪怕你有在多个字段都妥妥的……??
最后得得承认一下,在与 medz 讨论的过程中有些上头了
,在这里表示歉意。
后来我重新审视了一遍,觉得确实存在许多问题,于是绘制了认证流程图,还请多多指点??
感谢 medz 的认真评论和回复
以下是 NicolaBonelli 给出的解决方案#
我这里按照他的思路只实现了 login,其他细节还需自己根据实际需求进行修改,更多讨论请看这里 20 楼
- 首先需要在已有的
users
表上移除用户凭证的字段,如 email、name 等等,然后再创建另一个用于保存用户凭证的 Certificate 模型和表,大致结构如下图所示:users
表定义如下:Schema::create('users', function (Blueprint $table) { $table->increments('id'); //用户昵称 $table->string('nickname')->nullable(); $table->string('password'); //用户状态 $table->boolean('status')->default(true); $table->rememberToken(); $table->timestamps(); });
certificates
表定义如下:
Schema::create('certificates', function (Blueprint $table) {
$table->increments('id');
//关联用户表
$table->integer('user_id')->unsigned();
//用户验证凭据
$table->string('account')->unique();
//凭据类型
$table->string('type');
$table->timestamps();
});
- 修改
config/auth.php
内容如下:
'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'certificate',
],
],
'providers' => [
'certificate' => [
'driver' => 'eloquent',
'model' => App\Certificate::class,
],
],
- 紧接着修改
Certificate
模型,让他继承Illuminate\Foundation\Auth\User
这个类,并重写getAuthPassword
方法:
/**
* 定义模型关联
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function user()
{
return $this->belongsTo(User::class, 'user_id');
}
/**
* 获取用户凭证所对应的密码
* @return string
* @throws AuthenticationException
*/
public function getAuthPassword()
{
if ($this->user()) {
return $this->user->password;
}
throw new AuthenticationException();
}
到这里多字段登录功能算是实现了,但是
注销
时会因为certificates
表中没有定义remember_token
字段而导致抛出异常。要解决这个问题,我们还要重写logout
方法,甚至重新实现一个自定义Guard
…… 这里就不做分析了,有兴趣的可以自行 ReviewIlluminate\Auth
的源码!
最后感谢 NicolaBonelli 的分享
本作品采用《CC 协议》,转载必须注明作者和本文链接
推荐文章: