扩展phabricator验证
Toc
  1. 背景
  2. provider 和 adapter
    1. provider
    2. adapter
  3. 过程和工具
  4. last

背景

在团队中使用 phabricator 也有一段时间了,近期想让更多的人使用,为了让大家能够使用现有的帐号来便捷地登录 phabricator,需要修改验证方式,由默认的 username/password 修改为团队已有的统一登录接口,类似 oauth 的方式:

  • 检验统一的帐号密码
  • 检验通过后获取到一个 ticket
  • 由 ticket 获取用户的具体信息
  • 把用户信息按约定的接口,提交给 phabricator ,再由框架保存到 db、缓存等
  • 用户完成登录

翻了一下源码,发现 phabricator 的扩展非常方便。其中一个出发点在于:phabricator 会把所有 src 源码都加载到框架中,在运行时由框架帮忙查找并决定使用哪个子类

依据 phabricator 的 模块机制,在扩展目录 extensions 中添加自己的类,继承指定的接口或类,phabricator 就能自动加载我们扩展的类,添加到 web 界面中作为新的可选项,迅速扩展功能。

由于 phabricator 有 3 个部分,因此有 3 个扩展目录:

  • phabricator/src/extensions/
  • libphutil/src/extensions/
  • arcanist/src/extensions/

我本次扩展的功能需要用到前面两个目录。

以下内容主要是结论性的描述文字,至于我对源码分析过程、框架实现细节并未展开,因此这里的内容更适合已经接触过 phabricator、对其源码有一定了解的同学。

开始之前,有如下假设:

  • phabricator 的 web server 域名为 my.ph.com
  • 团队现有的认证中心域名为 my.auth.com
  • 假设认证中心 my.auth.com 包含了: a) 帐号的检验,b) ticket的分发,c) 通过 ticket 获取用户信息

provider 和 adapter

provider

那么首先需要在my.ph.com/auth/config/new/ 中新增一个新的认证方式,并启用它。

从结论上来说,在目录 phabricator/src/applications/auth/provider 新增一个类 PhabricatorMyAuthProvider,继承类 PhabricatorAuthProvider,按需实现其约定的功能:

  • 构造登录的表单,供用户输入其用户名、密码,由约定的函数 renderLoginForm 完成
  • 表单处理逻辑,提交用户帐号和密码到统一认证中心 my.auth.com,获取 ticket,根据 ticket 获取用户信息,用户信息经约定好的函数再由 phabricator 框架使用,完成用户的登录,由约定的函数 processLoginRequest 完成

当然,这里的流程跟正规的 oauth 认证方式未必都一致,具体实现根据具体情况分析。伪代码如下:

<?php

final class PhabricatorMyAuthProvider extends PhabricatorAuthProvider {
    private $adapter;

    // 名称,显示在「认证方式」列表中
    public function getProviderName() {
        return pht('MyAuth');
    }

    // 描述,显示在「认证方式」列表中
    public function getDescriptionForCreate() {
        return pht(
            'Configure to use the MyAuth account to log in to Phabricator');
    }

    // 使用另一个我们扩展的类 PhutilMyAuthAdapter,后续继续说明
    public function getAdapter() {
        if (!$this->adapter) {
            $this->adapter = id(new PhutilMyAuthAdapter());
        }
        return $this->adapter;
    }

    // 表单处理逻辑,提交用户名、密码到认证中心
    // 若是认证成功,获取到一个 ticket
    // 由 ticket 获取用户信息
    // 把用户信息提交到框架
    public function processLoginRequest(PhabricatorAuthLoginController $controller) {
        //var_dump(debug_print_backtrace());

        // 获取 url 上携带的参数,判断是否有 ticket
        $request = $controller->getRequest();
        $ticket = $request->getStr('LoginTicket', null);
        if ($ticket) {
            // 由 ticket 获取用户信息
            $url = 'http://my.auth.com/getUserInfo?LoginTicket=' . $ticket;

            $ch = curl_init();
            curl_setopt($ch, CURLOPT_URL, $url);
            curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
            $result = curl_exec($ch);
            curl_close($ch);
            $user = json_decode($result, true);
            print_r($user);

            if ($user['returnCode'] == 0) {
                $adapter = $this->getAdapter();
                // 保存用户信息
                $adapter->setUserInfo($user['userInfo']);
                // 提交到框架(判断是否已经 db 中,若没有,则创建一条记录,即为注册用户信息到 phabricator)
                return array($this->loadOrCreateAccount($adapter->getAccountId()), null);
            }
        }

        // 表单提交 url
        $url = 'http://my.auth.com?action=signin';
        // 校验后重定向的 uri,还是当前的 uri
        // 校验成功后,uri 会附带上 ticket 作为参数
        // 此处重定向的实现细节是 my.auth.com 的校验逻辑
        $r_url = 'http://' . $request->getHost() . $request->getPath();

        // 获取表单中填写的用户名、密码
        $username = $request->getStr('userName');
        $password = $request->getStr('userPwd');
        $postData = array(
            'userName' => $username,
            'userPwd' => $password,
            'url' => $r_url,
        );

        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, $url);
        curl_setopt($ch, CURLOPT_POST, true);
        curl_setopt($ch, CURLOPT_POSTFIELDS, $postData);
        curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_exec($ch);
        // 获取重定向的 uri 
        $last_url = curl_getinfo($ch, CURLINFO_EFFECTIVE_URL);
        curl_close($ch);

        // 简单判断是否携带有 ticket,若有,重新跳转到本函数,由 ticket 获取用户信息
        $pos = strpos($last_url, 'LoginTicket');
        if ($pos) {
            // get the ticket
            header('Location: ' . $last_url);
            exit;
        }

        // 若检验失败,附带一个 errorMsg 到 url 中,跳转到表单输入界面
        $request->setRequestData(array('errorMsg' => '用户名或密码错误'));
        $response = $controller->buildProviderPageResponse(
                $this,
                $this->renderLoginForm($request, 'login'));
        return array(null, $response);
    }

    // 构造一个表单,作为登录界面
    // 当检验失败时,依然返回到此处
    protected function renderLoginForm(AphrontRequest $request, $mode) {
        //var_dump(debug_print_backtrace());

        $viewer = $request->getUser();

        $dialog = id(new AphrontDialogView())
            ->setSubmitURI($this->getLoginURI())
            ->setUser($viewer)

            ->setTitle(pht('Login with ' . $this->getProviderName()))
            ->addSubmitButton(pht('Login'))
            //->addCancelButton($this->getStartURI());
            ->addFooter(
                phutil_tag(
                    'level',
                    array(),
                    'my.auth.com统一登录'
                ));

        // 是否已经失败过,显示错误消息
        $errMsg = $request->getStr('errorMsg');
        if (!empty($errMsg)) {
            $dialog->appendChild(
                id(new PHUIInfoView())->setErrors(array($errMsg))
            );
        }

        $form = id(new PHUIFormLayoutView())
            ->setUser($viewer)
            ->setFullWidth(true)
            ->appendChild(
                    id(new AphrontFormTextControl())
                    ->setLabel(pht('my.auth.com Username'))
                    ->setName('userName'))
            ->appendChild(
                    id(new AphrontFormPasswordControl())
                    ->setLabel(pht('my.auth.com Password'))
                    ->setName('userPwd'));

        $dialog->appendChild($form);
        return $dialog;
    }
}

adapter

adapter 的作用在于:把用户信息按约定的接口提供给 phabricator 框架。框架用到几个几个字段如下代码所示。

在目录 libphutil/src/auth/ 新增一个类 PhutilMyAuthAdapter,继承类 PhutilAuthAdapter,按需实现其约定的功能,返回框架需要的各个用户信息字段。

<?php

final class PhutilMyAuthAdapter extends PhutilAuthAdapter {
    private $userInfo;

    public function getAdapterType() {
        return 'MyAuth';
    }

    public function getAdapterDomain() {
        return 'my.auth.com';
    }

    public function getAccountID() {
        if ($this->userInfo === null) 
            return null;
        return idx($this->userInfo, 'userName');
    }

    public function getAccountName() {
        if ($this->userInfo === null)
            return null;
        return $this->getAccountID();
    }

    public function getAccountRealName() {
        if ($this->userInfo === null)
            return null;
        return idx($this->userInfo, 'chineseName');
    }

    public function getAccountEmail() { 
        if ($this->userInfo === null)
            return null;
        return idx($this->userInfo, 'email');
    }

    public function setUserInfo($user) {
        $this->userInfo = $user;
    }
}

过程和工具

  • web server 使用的是 nginx,根据 nginx 配置可找到入口 phabricator/webroot/index.php
  • 借助 phabricator 自带的工具 phabricator/scripts/aphront/aphrontpath.php 可查询某个 uri 是由哪个 controller 处理的,controller 的 handleRequest 函数就是逻辑入口:
$ cd phabricator/scripts/aphront/

$ ./aphrontpath.php  /
PhabricatorHomeMainController

$ ./aphrontpath.php  /auth
PhabricatorAuthListController

有助分析源码或快速定位哪个类。

last

其他功能的扩展,也是类似的逻辑,比如按自己的方式发送邮件。

– EOF –

Categories: web