背景
在团队中使用 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 –