多字段登录通用解决方案
面临的问题:
- 登录字段小于或等于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的认真评论和回复 :beers:
以下是 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 的分享 :beers:
本作品采用《CC 协议》,转载必须注明作者和本文链接
我觉得多字段很简单,封装一个 username 函数,用于判断是何种类型,ThinkSNS Plus 支持 id、email、phone、name 四个字段,前端使用 login 字段作为用户名传递。
参考:https://github.com/slimkit/thinksns-plus/b...
并不需要这么复杂的逻辑。
username
函数代码:我还有一个项目,简版的多字段登录案例:https://github.com/medz/phpwind/blob/maste...
一个输入框输入多种类型账号,后端使用 email,name等接收,无疑等于把判断做交给前端。虽然前端可以做。但是也有判断不准确的时候,随着代码的迭代,前后端分离。远没有直接一个字段提交过来后端判断来得直接。
@medz
话说你认真的看了这篇文章吗?
你说用一个字段去判断,那加入某个人注册时候的用户名就是一个手机号呢?但是他的 mobile 字段是另一个手机号,当他用用户名的那个手机号登录是不是就会登录不了呢?
让前端去判断我觉得不太实际,要么不准确,如我上面所说,要么需要大量的前后端交互……
@medz 老实说,你这种做法我考虑过,正常的思路确实没问题,但是逆向来看的话,就有 Bug……
再者,如果还有更多的字段,而这些又跟username 字段类似,没有想 email 和 mobile 那样的特性,毫无规律可言,你怎么办?
比如员工的工号……
额~认真看过,有些地方你写的我确实没明白,感觉语序不对可能理解偏差了。不过大多数系统只需要 phone、email、username 登录。我这样的做法更简单。
其次,用户名是有规则的,严格地说,用户名不应该允许用户使用纯数字和手机号格式。
其实,你是参考你的图,你的图透露的信息是你要一个输入框输入可能是 ID,可能是用户名,可能是手机号,可能是邮箱,还要你后面说的可能是工号。一个输入框,可是你传递表单为不同字段。我自然这样理解了,你可能误会我的回复了。
@medz 更新了一下,画了一个认证流程图 :beers:
先点赞。但是感觉就是这么一个小功能,搞的有点复杂了,这一个文件,那一个的。。。
我在想是否可以加2个字段,guard_type('email/phone/other') guard_value 根据用户注册的类型来选择
难道重点不是登录界面很好看么。。
你这个支持的字段超级多啊,一般有email 和 mobile 就够了,现在感觉大趋势是一个 mobile 搞定,
@839891627 是的,简单点的 medz 已经给出了比较好的方案
@我是谁 其实无论用户是用什么方式注册的,只要它补全了其他唯一标识符,那么使用其他字段都应该可以登录的。比如我微信扫码登录,平台让我绑定手机,然后我有绑定了邮箱,那么应可以用邮箱去登录的……主要还是看平台是否允许这么做 :smile:
@BradStev 对的,一般 B2C 的服务都是这样的,能表示用户唯一性的信息也就那几个。
但是对于企业内部来说比如要实现工号和用户名都能登录,而工号的规律又不那么明显,那么久这个方法就排上用场了!
其实主要还是因为我不擅长 正则表达式 :see_no_evil:
@HyanCat 英雄所见略同 :beers:
@Jinrenjie 了解了,确实是需要的这么多个字段来登录了
多种登录方式, 最好的实现还是
这样, 哪怕你一个账号支持一千种登录都没问题, 而且在数据量大后做表结构优化也好做.
上面回复的 users_login少了个 account 字段. 存储用户名或 openid 的
@NicolaBonelli 我认真的分析了一下你的设计思路,确实可行!!!(这个方法挺棒的)从横向转为纵向,对于用户登录凭证的存储和可扩展性来说确实优于单一表格的横向多字段存储。
但是不推荐这么做,这样设计带来的最大问题就是与已有的扩展包不兼容(因为你的
password
和凭证
不在一个表里)例如 Passport 认证,也要自定义UserProvider,重写retrieveByCredentials 方法,当然如果你不打算用 Laravel 自带的那套机制就另当别论了……为了给其他人提供更多的可选思路,我打算把你的方法添加到文章中,并注明来源,不知可否?
@Jinrenjie laravel 的扩展包基本都是 trait. 覆盖掉关键即可方法
最后一个问题, 当然可以. 哈哈哈
用了or 就不能触发索引了,小项目可以,大项目 就不好玩了
@waynet 嗯,这倒是,当时水平有限,考虑不到数据库索引问题!不过大项目肯定会吧认证模块做成微服务,有专门的架构肯定不会这样去做……
在文件 app/Http/Controllers/Auth/LoginController.php 中重写
attemptLogin
方法。这才是妥妥的, 要几个字段就几个,还简单!
代码如下:
@bing8u 如果是api呢?也一样吗?
@Sher guard的attempt方法 只在中间件web中可用