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

由于Laravel-admin只支持表单保存一对一关联数据,要想保存一对多关联数据,还得从了解它的源码入手,看有没有空子可钻。
首先,进入源码中的Form.php的store函数中看它是怎么保存表单数据的:

    public function store()
    {

    //为了节省篇幅,就省略无关代码吧
......

        DB::transaction(function () {
        //这里是保存表单中当前模型的数据
            $inserts = $this->prepareInsert($this->updates);

            foreach ($inserts as $column => $value) {
                $this->model->setAttribute($column, $value);
            }
            $this->model->save();
        //这里是保存表单中关联模型数据
            $this->updateRelation($this->relations);
        });
 ......
    }

然后进入到保存关联模型数据的updateRelation函数中:

 protected function updateRelation($relationsData)
    {
        foreach ($relationsData as $name => $values) {

            if (!method_exists($this->model, $name)) {
                continue;
            }
            $relation = $this->model->$name();

            //在这里判断了关联关系是否是一对一关联、一对一多态关联,不是的话就无法进行后续处理了
            $oneToOneRelation = $relation instanceof Relations\HasOne
                || $relation instanceof Relations\MorphOne
                || $relation instanceof Relations\BelongsTo;

            $prepared = $this->prepareUpdate([$name => $values], $oneToOneRelation);

            if (empty($prepared)) {
                continue;
            }
......
}

额,所以必须使用一对一关联关系来保存数据,即将本来是一对多的关系写一个一对一的关联关系用户保存数据,比如:user模型中又一个friends一对多关联模型,但是为了在表单中保存一个friends数据,创建一个friend一对一关联关系在表单中用户保存关联数据。

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

在真机调试的时候,发现安卓端websocket连接老是报错,一番查证,在 https://www.myssl.cn/tools/check-server-cert.html 中确认是服务器缺少中间证书,于是在 https://www.myssl.cn/tools/downloadchain.html 中下载了一个中间证书。
使用方法:
1、将中间证书内容追加到原证书中
如:

#cat >> my.crt
-----BEGIN CERTIFICATE-----
VVMxCzAJBgNVBAgTAlVUMRcwFQYDVQQHEw5TYWx0IExha2UgQ2l0eTEeMBwGA1UE
ChMVVGhlIFVTRVJUUlVTVCBOZXR3b3JrMSEwHwYDVQQLExhodHRwOi8vd3d3LnVz
ZXJ0cnVzdC5jb20xKzApBgNVBAMTIlVUTi1VU0VSRmlyc3QtTmV0d29yayBBcHBs
NzExMDY5NDAxMTAvBgNVBAsTKFNlZSB3d3cucmFwaWRzc2wuY29tL3Jlc291cmNl
cy9jcHMgKGMpMDcxLzAtBgNVBAsTJkRvbWFpbiBDb250cm9sIFZhbGlkYXRlZCAt
XCKBNQNAzybtImcaJjXQsihqkuohYcWh2QuijBgXZC+o9IUl+2SNhw6OYXSJTuuD
09VFQFAaUC41rLcU9BDh6w7xmGnZJzZ0H8jm2E9NA6s6DId7qQ+f/YdkKePRR+Dw
p9GnjmheMvhqs0DFj+tCFhHX3PK8WGrYBYG8ejsgo8uKAKTkishpOMyTs4CmTlDX
chn5QGRjpq2FlIqqlTwLdMGpkeUSZjuAFblLhTQs158Q5VHC5SH+3DvJW+g7/CpT
jBhiTnfNyD19rUmrWZ2dmic50B32BAiIO9OepmVvI8nA1TBvNFfhX75cOCk=
-----END CERTIFICATE-----
(执行ctrl+d)

2、重启服务器

我的个人博客:逐步前行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的话,效果更好,减少了转发的环节。