2018年10月

我的个人博客:逐步前行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();
    }

注释说明返回

IlluminateContractsAuthStatefulGuard

,找到该文件发现这是一个接口文件,定义 了attempt方法,直接搜索

implements StatefulGuard

看哪个类实现了该接口,找到了

IlluminateAuthSessionGuard

以及其中的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认证的核心了,首先看怎么获取用户信息:

IlluminateAuthSessionGuard

的构造函数可见在实例化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

找到

IlluminateAuthAuthManager

中的:

    /**
     * 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;
    }

继续跟踪到IlluminateAuthAuthManager使用的trait:IlluminateAuthCreatesUserProviders中的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']);
    }

可以确定在 IlluminateAuthSessionGuard的attempt函数中的provider就是IlluminateAuthEloquentUserProvider,找到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();
    }

在这里根据除密码之外的其它参数查询出了用户数据。
回到 IlluminateAuthSessionGuard,再看:

    /**
     * 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);
    }

调用了IlluminateAuthEloquentUserProvider的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:IlluminateAuthCreatesUserProviders中的createUserProvider函数的switch分支里新增一个case,并返回NewEloquentUserProvider的实例,再将config/auth.php中的providers.users.driver配置改为该case的值即可。

我的个人博客:逐步前行STEP

VIM批量注释与取消注释是vim编辑中很基础的一个操作,但是尴尬的是我尝试了很久才发现这个操作只有在VIM中才能成功,很无语。。。。不知道在VI中试了多少遍了,一直以为我的服务器上的VI配置是不是有问题为什么同样的操作命令,别人都很简单就成功了我一直没效果(泪),今天我想着试试VIM中操作批量注释看,结果。。。困扰很久的问题就解决了!!
也不知道确实是我的服务器上VI配置不对,还是网上的教程有坑,明明VI不能这么批量注释却在标题上写“VI/VIM批量注释与取消”之类的,毕竟不是运维,深究的事有空再说。将这个简单的操作记录一下,权当作纪念一下踩坑的难受。
注释:

1、进入文档,vim test.txt 后,按住ctrl+v进入VISUAL BLOCK模式,上下选择需要注释的行
2、按大写键,再按i,或者直接按shift+i,进入INSERT模式,输入注释符号(#或者//)
3、按esc键,之前选择的行首部会自动加上注释符

取消:

1、进入文档,vim test.txt 后,按住ctrl+v进入VISUAL BLOCK模式,上下选择需要注释的行
2、按d键删除注释符

我的个人博客:逐步前行STEP

以前就想着配置cdn了,但是一直配置不得法,添加完配置也没有生效,这两天认真研究了一下,终于解决了。
首先是在阿里云上的cdn配置:

加速域名:image.aaa.com,就是让网站访问图片的域名
回源域名:bbb.com,就是能真实访问到图片资源的域名,不过由于我的图片路径是IP访问的,我配置的是回源IP。
最重要的就是以上两个,之前我没区分加速与回源的意思,配置反了一直不生效~~~
然后在域名解析中配置image.aaa.com的cname记录,当ping
image.aaa.com时提示的域名是:.w.kunlungr.com时就算生效了,不出意外的话,直接通过image.aaa.com访问资源是成功的。

接下来是服务器上的配置:
我的项目架构是这样的:

两台aws服务器部署了网站,一台阿里云服务器做NFS让两台应用服务器共享图片资源,一个域名用于前端访问,通过IP直接访问后端接口。
CDN回源的IP就是阿里云服务器IP,我在这台服务器上简单搭建了NGINX服务器只用来访问静态资源:http://ip/image/xxx/xxx.jpg,而aws服务器上的后端接口都是8080端口,前端端口是80,也就是访问aaa.com的时候,图片路径为http://ip:8080/xxx/xxx.jpg

通过cdn加速域名image.aaa.com访问回源IP(阿里云服务器的IP)来获取图片http://image.aaa.com/xxx/xxx.jpg,只需要配置服务器实现简单的请求转发就可以从CDN缓存中获取图片了:

http://ip:8080/xxx/xxx.jpg转发到http://image.aaa.com/image/xxx/xxx.jpg

当然如果直接将aws服务器上的接口返回的图片路径替换成http://image.aaa.com/xxx/xxx.jpg的话,效果更好,减少了转发的环节。

今天上传一些图片到我们的网站上,发现有些图片上传后没有任何提示就报错500,调试一下发现是在做图片处理的时候报错的,查日志后发现如下提示:

FastCGI sent in stderr: "PHP message: PHP Fatal error: Allowed memory size of 134217728 bytes exhausted (tried to allocate 8192 bytes)

原来是内存溢出了,可是上传的图片都不大,因为网站限制上传文件不能超过10M,而ini配置文件中的上传限制和NGINX服务器的配置文件的上传限制都远大于10M,怎么还会内存溢出???

然后一番检索发现有位仁兄提出一个图片占用内存的公式:

(图片对象的width和height )X(图片的通道数,一般是3)X 1.7

按这个计算,我上传的4.2M的图片(6577 × 4385)占了140M,而默认的内存限制是128M,妥妥的超过了,不过这个公式应该是估算,实际报错提示占用了128.0078125M,也就刚好超过128M,离140M差得有点多,不过多次测试结果表明只要按这个公式计算结果小于134217728就不会内存溢出。

所以,方案是加上一个限制:长不超过6000px,宽不超过4000px,因为这个尺寸按上面的公式计算内存占用超不多116M。

我的个人博客:逐步前行STEP

NFS启动时会随机启动多个端口并向RPC注册,为了设置安全组以及iptables规则,需要设置NFS固定端口。
NFS服务需要开启 mountd,nfs,nlockmgr,portmapper,rquotad这5个服务,其中nfs、portmapper的端口是固定的,另外三个服务的端口是随机分配的,所以需要给mountd,nlockmgr,rquotad设置固定的端口。
其中,给mountd、rquotad设置端口的方式很简单,在/etc/sysconfig/nfs中添加一下设置即可:

RQUOTAD_PORT=30001
LOCKD_TCPPORT=30002
LOCKD_UDPPORT=30002
MOUNTD_PORT=30003
STATD_PORT=30004

重启rpc、nfs的配置与服务:

systemctl restart rpcbind.service
systemctl restart nfs.service

查看端口使用情况:

rpcinfo -p

可以看到mountd服务已经使用了配置的端口,但是nlockmgr的端口还是随机的,还需在/etc/modprobe.d/lockd.conf中添加以下设置:

options lockd nlm_tcpport=30002
options lockd nlm_udpport=30002

重新加载NFS配置和服务:

systemctl restart nfs-config
systemctl restart nfs-idmap
systemctl restart nfs-lock
systemctl restart nfs-server

然后重启服务器,nlockmgr的端口就是固定的端口了。