我的个人博客:逐步前行STEP
目前开发小程序,按需求要实现3种登陆方式:
1、微信授权登陆
2、账户密码登陆
3、手机号、验证码登陆
我使用laravel自带的Auth认证机制,通过attempt方法进行账户验证,但是默认的认证机制必须包含password字段,而我的第1、3种登陆方式都没有password字段,所以需要深入源码了解认证机制的实现,然后再进行修改。
首先,看看自带的Auth功能的LoginController怎么实现的:
class LoginController extends Controller
{
...
use AuthenticatesUsers;
...
}
使用了trait:AuthenticatesUsers,AuthenticatesUsers中有一个login方法就是实现默认的登陆方式的方法:
public function login(Request $request)
{
//这里是对登陆参数做表单验证
$this->validateLogin($request);
//这里是防止暴力破解,对同一个IP的接口调用次数做限制
if ($this->hasTooManyLoginAttempts($request)) {
$this->fireLockoutEvent($request);//限制访问
return $this->sendLockoutResponse($request);//发回限制访问的响应
}
//验证登陆
if ($this->attemptLogin($request)) {
return $this->sendLoginResponse($request);//返回登陆成功的响应
}
//登录失败,失败次数++,防止暴力破解
$this->incrementLoginAttempts($request);
// 返回登陆失败的响应
return $this->sendFailedLoginResponse($request);
}
这里的重点在于:attemptLogin方法的调用,这才是关键的一步:登陆验证
protected function attemptLogin(Request $request)
{
return $this->guard()->attempt(
$this->credentials($request), $request->filled('remember')
);
}
再看guard函数:
/**
* Get the guard to be used during authentication.
*
* @return \Illuminate\Contracts\Auth\StatefulGuard
*/
protected function guard()
{
return Auth::guard();
}
注释说明返回
\Illuminate\Contracts\Auth\StatefulGuard
,找到该文件发现这是一个接口文件,定义 了attempt方法,直接搜索
implements StatefulGuard
看哪个类实现了该接口,找到了
Illuminate\Auth\SessionGuard
以及其中的attempt方法:
/**
* Attempt to authenticate a user using the given credentials.
*
* @param array $credentials
* @param bool $remember
* @return bool
*/
public function attempt(array $credentials = [], $remember = false)
{
$this->fireAttemptEvent($credentials, $remember);
//这里获取了用户信息
$this->lastAttempted = $user = $this->provider->retrieveByCredentials($credentials);
//校验用户密码
if ($this->hasValidCredentials($user, $credentials)) {
$this->login($user, $remember);
return true;
}
$this->fireFailedEvent($user, $credentials);
return false;
}
获取用户信息:
$user = $this->provider->retrieveByCredentials($credentials);
和校验用户密码:
$this->hasValidCredentials($user, $credentials)
就是Auth认证的核心了,首先看怎么获取用户信息:
从
Illuminate\Auth\SessionGuard
的构造函数可见在实例化SessionGuard的时候传入了UserProvider $provider:
public function __construct($name,
UserProvider $provider,
Session $session,
Request $request = null)
{
$this->name = $name;
$this->session = $session;
$this->request = $request;
$this->provider = $provider;
}
直接搜索
new SessionGuard
找到
Illuminate\Auth\AuthManager
中的:
/**
* Create a session based authentication guard.
*
* @param string $name
* @param array $config
* @return \Illuminate\Auth\SessionGuard
*/
public function createSessionDriver($name, $config)
{
//看这里,通过$config['provider']创建了provider
$provider = $this->createUserProvider($config['provider'] ?? null);
$guard = new SessionGuard($name, $provider, $this->app['session.store']);
if (method_exists($guard, 'setCookieJar')) {
$guard->setCookieJar($this->app['cookie']);
}
if (method_exists($guard, 'setDispatcher')) {
$guard->setDispatcher($this->app['events']);
}
if (method_exists($guard, 'setRequest')) {
$guard->setRequest($this->app->refresh('request', $guard, 'setRequest'));
}
return $guard;
}
继续跟踪到Illuminate\Auth\AuthManager使用的trait:Illuminate\Auth\CreatesUserProviders中的createUserProvider:
public function createUserProvider($provider = null)
{
if (is_null($config = $this->getProviderConfiguration($provider))) {
return;
}
if (isset($this->customProviderCreators[$driver = ($config['driver'] ?? null)])) {
return call_user_func(
$this->customProviderCreators[$driver], $this->app, $config
);
}
switch ($driver) {
case 'database':
return $this->createDatabaseProvider($config);
case 'eloquent':
return $this->createEloquentProvider($config);
default:
throw new InvalidArgumentException(
"Authentication user provider [{$driver}] is not defined."
);
}
}
对照config/auth.php中的provider驱动配置,默认是eloquent,也就是会执行:
return $this->createEloquentProvider($config);
跟棕到该方法:
protected function createEloquentProvider($config)
{
return new EloquentUserProvider($this->app['hash'], $config['model']);
}
可以确定在 Illuminate\Auth\SessionGuard的attempt函数中的provider就是Illuminate\Auth\EloquentUserProvider,找到retrieveByCredentials函数:
public function retrieveByCredentials(array $credentials)
{
if (empty($credentials) ||
(count($credentials) === 1 &&
array_key_exists('password', $credentials))) {
return;
}
$query = $this->createModel()->newQuery();
foreach ($credentials as $key => $value) {
if (Str::contains($key, 'password')) {
continue;
}
if (is_array($value) || $value instanceof Arrayable) {
$query->whereIn($key, $value);
} else {
$query->where($key, $value);
}
}
return $query->first();
}
在这里根据除密码之外的其它参数查询出了用户数据。
回到 Illuminate\Auth\SessionGuard,再看:
/**
* Determine if the user matches the credentials.
*
* @param mixed $user
* @param array $credentials
* @return bool
*/
protected function hasValidCredentials($user, $credentials)
{
return ! is_null($user) && $this->provider->validateCredentials($user, $credentials);
}
调用了Illuminate\Auth\EloquentUserProvider的validateCredentials方法:
/**
* Validate a user against the given credentials.
*
* @param \Illuminate\Contracts\Auth\Authenticatable $user
* @param array $credentials
* @return bool
*/
public function validateCredentials(UserContract $user, array $credentials)
{
$plain = $credentials['password'];
//对比加密后的密码是否和数据库中的相同
return $this->hasher->check($plain, $user->getAuthPassword());
}
最终,我们确认只要在EloquentProvider中的validateCredentials修改为自己的验证方式就可以实现需求了,可是直接修改源码还是不安全,可能会导致其它不可预测的问题,毕竟没有深入研究,还是保险一点,增加一个provider,写一个新的validateCredentials方法,会是更好的选择。
新建一个NewEloquentUserProvider继承EloquentUserProvider,重写validateCredentials:
public function validateCredentials(Authenticatable $user, array $credentials)
{
if(array_key_exists('openid',$credentials)){
//openid登陆
$openid = $credentials['openid'];
if($user->getAuthOpenid() == $openid) return true;
}elseif(array_key_exists('password',$credentials)){
//Phone、password登陆
$plain = $credentials['password'];
return $this->hasher->check($plain, $user->getAuthPassword());
}else{
//Phone、code登陆
$authCode = Cache::get("login_verification_code_".$credentials['code']);
if($authCode && $authCode == $credentials['code']) return true;
}
return false;
}
实现三种方式的登陆验证,然后在 trait:Illuminate\Auth\CreatesUserProviders中的createUserProvider函数的switch分支里新增一个case,并返回NewEloquentUserProvider的实例,再将config/auth.php中的providers.users.driver配置改为该case的值即可。