浅谈多个社交账号的绑定设计

Dearmadman 在 Laravel Socialite 详解 中使用 larastarscn/socialite 解决了第三方账号登录集成的问题,那么在获取到用户资料之后呢?集成多个社交账号,该如何绑定同一个账号?本篇就让我们来探讨一下集成登录的那点事。

起初

起初,当我们只需要集成单个社交登录时,我们可能会为了快速的完成任务简单粗暴的在用户模型中加入 open_id 或者 github_id 类似的属性,那么在数据库中,我们需要在表中添加相应的字段。这是可以快速有效的完成任务的做法。

但是,当更多的需求来临时,要求我们额外的集成一种或者多种社交登录,那该怎么办?难道我们还要任性的在表结构中添加相应的字段?

Schema::table('users', function ($table) {
  $table->string('github_id');
  $table->string('douban_id');
});

这很明显的违背了开放封闭原则,假如我们这么做,那么可以想象的当每多集成一种登录时,我们就需要对数据表结构做出一次修正,并且,在登录授权回调验证时,还要增加一道集成驱动与字段查询匹配的工序。

那应该怎么做?

设想

这么想来,User 表是否承担了过多的能力,它是否应该浪费自己的精力来管理这些社交标识?那不如我们安排 SocialiteUser 来专门管理用户与社交账号之间的关系?我们需要设计一种易扩展的方案来管理不同驱动的社交登录,那么我们很容易的设计出这种表结构:

- socialite_users
  - id
  - user_id
  - driver
  - open_id

SocialiteUser 应该具有哪些职责?很显然,它主要用来维护社交登录标识和用户模型之间的关系。那么它应该具有以下能力:

  • 将社交登录账户绑定到用户模型上
  • 获取匹配的用户模型

以下为简单的代码演示:

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class SocialiteUser extends Model
{
    public $guarded = ['id'];

    /**
     * Get user instance by driver and openid.
     *
     * @param  $driver  string
     * @param  $openid  string
     * @return /App/User|null
     */
    public function getUser($driver, $openid)
    {
        $finder =  $this->where([
            'driver' => $driver,
            'open_id' => $openid
        ])->first();

        return $finder ? $finder->user : $finder;
    }

    /**
     * get related user model.
     *
     * @return /App/User||null
     */
    public function user()
    {
        return $this->belongsTo('App\User');
    }

    /**
     * Save a new record.
     *
     * @param  $userId  integer
     * @param  $driver  string
     * @param  $id  string
     * @return /App/SocialiteUser
     */
    public function saveOne($userId, $driver, $id)
    {
        return $this->create([
            'user_id' => $userId,
            'driver' => $driver,
            'open_id' => $id
       ]);
    }
}

使用

在授权登录流程中,用户同意授权,第三方应用将重定向到回调路由,回调路由中 Socialite 会主动请求获取用户资料,并将用户的社交标识 ID 映射到 User 模型的 id 属性上。

那么我们就可以在回调路由中根据驱动标识和用户相应的社交标识 ID 来匹配查询库中是否已存在绑定的用户。如果存在那就直接使用匹配到的用户登录,如果不存在,那么就生成一个用户,并为这个用户附加社交账户信息。然后使用新生成的账户登录。

<?php

namespace App\Http\Controllers;

use App\SocialiteUser;
use App\User;
use Socialite;

class OAuthAuthorizationController extends Controller
{
    //
    public function redirectToProvider($driver)
    {
        return Socialite::driver($driver)->redirect();
    }

    public function handleProviderCallback($driver)
    {
        $user =  Socialite::driver($driver)->user();

        $model = new User();
        $socialiteUser = new SocialiteUser();
        $finder = $socialiteUser->getUser($driver, $user->id);
        if (! $finder) {
            $finder = $model->generateUserInstance();
            $finder->save();
            $socialiteUser->saveOne($finder->id, $driver, $user->id);
        }

        Auth::login($finder);

        return view('home');
    }
}

这样看来,如果需求一种新的社交登录的集成,那么完全不需要做出其它代码的改动,直接配置驱动就可以了。

PS: 欢迎关注简书 Laravel 专题,也欢迎 Laravel 相关文章的投稿 :),作者知识技能水平有限,如果你有更好的设计方案欢迎讨论交流,如果有错误的地方也请批评指正,在此表示感谢谢谢 :)

本帖已被设为精华帖!
本帖由系统于 5年前 自动加精
《L01 基础入门》
我们将带你从零开发一个项目并部署到线上,本课程教授 Web 开发中专业、实用的技能,如 Git 工作流、Laravel Mix 前端工作流等。
《L02 从零构建论坛系统》
以构建论坛项目 LaraBBS 为线索,展开对 Laravel 框架的全面学习。应用程序架构思路贴近 Laravel 框架的设计哲学。
讨论数量: 4
nickfan

其实在整个三方账户关联中,逻辑上应该拆分成:

  1. 本地未登录->三方认证成功->三方driver&三方id在本地binds库中有记录->本地自动登录为绑定本地uid用户

  2. 本地未登录->三方认证成功->三方driver&三方id在本地binds库中无记录->用户选择登录并绑定此三方id || 手动注册本地账户/自动注册本地账户并绑定此三方账户

  3. 本地已登录->三方认证成功->三方driver&三方id在本地binds库中无记录->绑定当前本地用户与三方driver & 三方id

  4. 本地已登录->三方认证成功->三方driver&三方id在本地binds库中有记录->本地绑定记录中三方绑定id的本地uid与当前登录uid相同->更新绑定记录中的关联信息(如token,name,avatar_src等)

  5. 本地已登录->三方认证成功->三方driver&三方id在本地binds库中有记录->本地绑定记录中三方绑定id的本地uid与当前登录uid不同->检查并解除旧uid与三方id的绑定关系(如果仅有此一项绑定关联的旧账户可以软删除/禁用) 并将绑定关系设定到新本地uid上

大部分设计人员只考虑1,3的情景而忽略了2,5的情景,自动注册本地账户的策略虽简便,但如果没有username重新更新的用户选择,也会让部分处女座同学难以忍受,比如京东的注册策略。

另外仅仅关联账户其实只记三方id即可,如果还想后续的比如po个微博啥的,最好还是把token啥的记号并做好更新策略和ui交互,比如可以参考多说的账户绑定过期更新通知。

附,我本地的关联数据结构设计(适配器id=三方driver):

CREATE TABLE `socialconnects` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `adapterid` tinyint(3) unsigned NOT NULL DEFAULT '0' COMMENT '适配器id',
  `user_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '本地用户id',
  `idlabel` varchar(255) COLLATE utf8_unicode_ci NOT NULL DEFAULT '' COMMENT '三方id标识',
  `authdata` text COLLATE utf8_unicode_ci NOT NULL COMMENT '相关认证json数据',
  `expire_at` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00' COMMENT '认证过期时间戳',
  `created_at` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00',
  `updated_at` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00',
  PRIMARY KEY (`id`),
  UNIQUE KEY `user_id` (`adapterid`,`user_id`),
  UNIQUE KEY `idlabel` (`adapterid`,`idlabel`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
7年前 评论

感觉不错的样子~

7年前 评论
正遇到这个问题呢,不过我设计的表就两张,老是感觉不对劲儿
7年前 评论
nickfan

其实在整个三方账户关联中,逻辑上应该拆分成:

  1. 本地未登录->三方认证成功->三方driver&三方id在本地binds库中有记录->本地自动登录为绑定本地uid用户

  2. 本地未登录->三方认证成功->三方driver&三方id在本地binds库中无记录->用户选择登录并绑定此三方id || 手动注册本地账户/自动注册本地账户并绑定此三方账户

  3. 本地已登录->三方认证成功->三方driver&三方id在本地binds库中无记录->绑定当前本地用户与三方driver & 三方id

  4. 本地已登录->三方认证成功->三方driver&三方id在本地binds库中有记录->本地绑定记录中三方绑定id的本地uid与当前登录uid相同->更新绑定记录中的关联信息(如token,name,avatar_src等)

  5. 本地已登录->三方认证成功->三方driver&三方id在本地binds库中有记录->本地绑定记录中三方绑定id的本地uid与当前登录uid不同->检查并解除旧uid与三方id的绑定关系(如果仅有此一项绑定关联的旧账户可以软删除/禁用) 并将绑定关系设定到新本地uid上

大部分设计人员只考虑1,3的情景而忽略了2,5的情景,自动注册本地账户的策略虽简便,但如果没有username重新更新的用户选择,也会让部分处女座同学难以忍受,比如京东的注册策略。

另外仅仅关联账户其实只记三方id即可,如果还想后续的比如po个微博啥的,最好还是把token啥的记号并做好更新策略和ui交互,比如可以参考多说的账户绑定过期更新通知。

附,我本地的关联数据结构设计(适配器id=三方driver):

CREATE TABLE `socialconnects` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `adapterid` tinyint(3) unsigned NOT NULL DEFAULT '0' COMMENT '适配器id',
  `user_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '本地用户id',
  `idlabel` varchar(255) COLLATE utf8_unicode_ci NOT NULL DEFAULT '' COMMENT '三方id标识',
  `authdata` text COLLATE utf8_unicode_ci NOT NULL COMMENT '相关认证json数据',
  `expire_at` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00' COMMENT '认证过期时间戳',
  `created_at` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00',
  `updated_at` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00',
  PRIMARY KEY (`id`),
  UNIQUE KEY `user_id` (`adapterid`,`user_id`),
  UNIQUE KEY `idlabel` (`adapterid`,`idlabel`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
7年前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!