typecho无法登录后台的问题解决
typecho重新升级至1.2.0(实际上是重新删除后安装)后,typecho始终无法登录后台。
PHP这种Web编程语言实在没接触过,花了两天来玩一下。
博客网站使用的技术:nginx+php-fpm+typecho(sqlite),nginx与php实际运行一个ARM电视盒子上。
正常运行的网站,各种调试、日志都是关闭的,因此,首先打开php-fpm的日志捕获开关:
;catch_workers_output=no
修改为yes,并将首字符;去掉。
此开关打开后,php的error_log就可以输出信息了。
同时将php.ini的显示错误开关打开:
;display_errors=off
修改为display_errors=on。
修改好这些开关后,重启php-fpm的服务。
之后就可以在typecho的php代码中增加error_log以观察代码的运行轨迹。
登录链接代码在typecho的admin目录login.php,但实际提交的代码为:
https://XXX.XXX.net/blog/index.php/action/login?_=7cf73d0584c577c96833bd2e3a58e0f0
_=后面的字符为一串md5字符,可以不管。
而这个链接首先会被nginx的fastcgi_split_path_info功能拆分,对于blog的fastcgi_split_path_info代码如下:
fastcgi_split_path_info ^(.+?\.php)(\/?.+)$;
fastcgi_split_path_info后面的正则表达式,将/blog/index.php/action/login?_=7cf73d0584c577c96833bd2e3a58e0f0链接拆分成两个group
分别是:
/blog/index.php
与
/action/login?_=7cf73d0584c577c96833bd2e3a58e0f0
然后用$fastcgi_script_name与$fastcgi_path_info存放。
在其后的
set $path_info $fastcgi_path_info;
fastcgi_param PATH_INFO $path_info;
将后部内容传递至PATH_INFO(这个变量在typecho中,即可通过从request头部中获取PATH_INFO变量来取得)
实际执行脚本的是/blog/index.php
但从index.php来看,代码很简单:
<?php
/**
* Typecho Blog Platform
*
* @copyright Copyright (c) 2008 Typecho team (http://www.typecho.org)
* @license GNU General Public License 2.0
* @version $Id: index.php 1153 2009-07-02 10:53:22Z magike.net $
*/
/** 载入配置支持 */
if (!defined('__TYPECHO_ROOT_DIR__') && !@include_once 'config.inc.php') {
file_exists('./install.php') ? header('Location: install.php') : print('Missing Config File');
exit;
}
/** 初始化组件 */
\Widget\Init::alloc();
/** 注册一个初始化插件 */
\Typecho\Plugin::factory('index.php')->begin();
/** 开始路由分发 */
\Typecho\Router::dispatch();
/** 注册一个结束插件 */
\Typecho\Plugin::factory('index.php')->end();
从网络中,搜索一堆资料,了解到,关键代码在\Typecho\Router::dispatch()
主要内容即根据数据库中的路由表,分别调用创建对应的Widget,然后调用相应Widget的action函数。
在Router.php的dispatch函数中加上相应的日志输出对应的widget。
public static function dispatch()
{
/** 获取PATHINFO */
$pathInfo = Request::getInstance()->getPathInfo();
foreach (self::$routingTable as $key => $route) {
if (preg_match($route['regx'], $pathInfo, $matches)) {
self::$current = $key;
error_log('route widget ' . $route['widget']);
try {
/** 载入参数 */
$params = null;
if (!empty($route['params'])) {
unset($matches[0]);
$params = array_combine($route['params'], $matches);
}
$widget = Widget::widget($route['widget'], null, $params);
if (isset($route['action'])) {
$widget->{$route['action']}();
}
return;
} catch (\Exception $e) {
if (404 == $e->getCode()) {
Widget::destroy($route['widget']);
continue;
}
throw $e;
}
}
}
/** 载入路由异常支持 */
throw new RouterException("Path '{$pathInfo}' not found", 404);
}
从日志看出/action/login?_=7cf73d0584c577c96833bd2e3a58e0f0对应的widget为\Widget\Action
对于var\Widget\Action.php的代码见内:
<?php
namespace Widget;
use Typecho\Widget;
if (!defined('__TYPECHO_ROOT_DIR__')) {
exit;
}
/**
* 执行模块
*
* @package Widget
*/
class Action extends Widget
{
/**
* 路由映射
*
* @access private
* @var array
*/
private $map = [
'ajax' => '\Widget\Ajax',
'login' => '\Widget\Login',
'logout' => '\Widget\Logout',
'register' => '\Widget\Register',
'upgrade' => '\Widget\Upgrade',
'upload' => '\Widget\Upload',
'service' => '\Widget\Service',
'xmlrpc' => '\Widget\XmlRpc',
'comments-edit' => '\Widget\Comments\Edit',
'contents-page-edit' => '\Widget\Contents\Page\Edit',
'contents-post-edit' => '\Widget\Contents\Post\Edit',
'contents-attachment-edit' => '\Widget\Contents\Attachment\Edit',
'metas-category-edit' => '\Widget\Metas\Category\Edit',
'metas-tag-edit' => '\Widget\Metas\Tag\Edit',
'options-discussion' => '\Widget\Options\Discussion',
'options-general' => '\Widget\Options\General',
'options-permalink' => '\Widget\Options\Permalink',
'options-reading' => '\Widget\Options\Reading',
'plugins-edit' => '\Widget\Plugins\Edit',
'themes-edit' => '\Widget\Themes\Edit',
'users-edit' => '\Widget\Users\Edit',
'users-profile' => '\Widget\Users\Profile',
'backup' => '\Widget\Backup'
];
/**
* 入口函数,初始化路由器
*
* @throws Widget\Exception
*/
public function execute()
{
/** 验证路由地址 **/
$action = $this->request->action;
/** 判断是否为plugin */
$actionTable = array_merge($this->map, unserialize(Options::alloc()->actionTable));
if (isset($actionTable[$action])) {
$widgetName = $actionTable[$action];
}
if (isset($widgetName) && class_exists($widgetName)) {
$widget = self::widget($widgetName);
if ($widget instanceof ActionInterface) {
$widget->action();
return;
}
}
throw new Widget\Exception(_t('请求的地址不存在'), 404);
}
}
基本思想来看,查map表,然后转到对应的类。对于当前动作,login显然对应到\Widget\Login.php。
\Widget\Login.php的代码如下:
<?php
namespace Widget;
use Typecho\Cookie;
use Typecho\Validate;
use Widget\Base\Users;
if (!defined('__TYPECHO_ROOT_DIR__')) {
exit;
}
/**
* 登录组件
*
* @category typecho
* @package Widget
* @copyright Copyright (c) 2008 Typecho team (http://www.typecho.org)
* @license GNU General Public License 2.0
*/
class Login extends Users implements ActionInterface
{
/**
* 初始化函数
*
* @access public
* @return void
*/
public function action()
{
// protect
$this->security->protect();
/** 如果已经登录 */
if ($this->user->hasLogin()) {
/** 直接返回 */
$this->response->redirect($this->options->index);
}
/** 初始化验证类 */
$validator = new Validate();
$validator->addRule('name', 'required', _t('请输入用户名'));
$validator->addRule('password', 'required', _t('请输入密码'));
$expire = 30 * 24 * 3600;
/** 记住密码状态 */
if ($this->request->remember) {
Cookie::set('__typecho_remember_remember', 1, $expire);
} elseif (Cookie::get('__typecho_remember_remember')) {
Cookie::delete('__typecho_remember_remember');
}
/** 截获验证异常 */
if ($error = $validator->run($this->request->from('name', 'password'))) {
Cookie::set('__typecho_remember_name', $this->request->name);
/** 设置提示信息 */
Notice::alloc()->set($error);
$this->response->goBack();
}
/** 开始验证用户 **/
$valid = $this->user->login(
$this->request->name,
$this->request->password,
false,
1 == $this->request->remember ? $expire : 0
);
/** 比对密码 */
if (!$valid) {
/** 防止穷举,休眠3秒 */
sleep(3);
self::pluginHandle()->loginFail(
$this->user,
$this->request->name,
$this->request->password,
1 == $this->request->remember
);
Cookie::set('__typecho_remember_name', $this->request->name);
Notice::alloc()->set(_t('用户名或密码无效'), 'error');
$this->response->goBack('?referer=' . urlencode($this->request->referer));
}
self::pluginHandle()->loginSucceed(
$this->user,
$this->request->name,
$this->request->password,
1 == $this->request->remember
);
/** 跳转验证后地址 */
if (!empty($this->request->referer)) {
/** fix #952 & validate redirect url */
if (
0 === strpos($this->request->referer, $this->options->adminUrl)
|| 0 === strpos($this->request->referer, $this->options->siteUrl)
) {
$this->response->redirect($this->request->referer);
}
} elseif (!$this->user->pass('contributor', true)) {
/** 不允许普通用户直接跳转后台 */
$this->response->redirect($this->options->profileUrl);
}
$this->response->redirect($this->options->adminUrl);
}
}
在此action函数中,加上相应日志,问题定位到此处代码:
$this->security->protect();
代码在执行到此行,即已经返回,也就根本没有再执行后面的校验用户名、密码等动作。
protect函数的代码如下,相当简单
/**
* 保护提交数据
*/
public function protect()
{
if ($this->enabled && $this->request->get('_') != $this->getToken($this->request->getReferer())) {
$this->response->goBack();
}
}
在此函数中加上日志定位到,此处的request->get('_')获取即为请求中的7cf73d0584c577c96833bd2e3a58e0f0,
而getToken函数如下:
/**
* 获取token
*
* @param string|null $suffix 后缀
* @return string
*/
public function getToken(?string $suffix): string
{
return md5($this->token . '&' . $suffix);
}
根据token与request的getReferer计算的一个md5内容。
token及相应计算规则应该是没有问题的,因此怀疑点定位到getReferer函数的返回内容。
在网上搜索一番后,发现已经有人记录了此种问题及相应解决办法,链接:https://blog.warhut.cn/dmbj/423.html
引用如下:
------------------------------------------------------------------------------------------------------------------
如果加入了no-referrer
,将会导致typecho
无法登录后台,原因如下:
<input type="hidden" name="referrer" value="<?php echo htmlspecialchars($request->get('referrer')); ?>" />
由于typecho
是通过referrer
登录的后台地址,传输参数,所以当加入no-referrer
之后相当于删除了提交的地址。
通过下面即可解决,加在<head></head>
中。
<meta name="referrer" content="same-origin" />
------------------------------------------------------------------------------------------------------------------
解决办法,先在admin\Header.php中加入上面的referer行,如下:
<?php
if (!defined('__TYPECHO_ADMIN__')) {
exit;
}
$header = '<link rel="stylesheet" href="' . $options->adminStaticUrl('css', 'normalize.css', true) . '">
<link rel="stylesheet" href="' . $options->adminStaticUrl('css', 'grid.css', true) . '">
<link rel="stylesheet" href="' . $options->adminStaticUrl('css', 'style.css', true) . '">';
/** 注▒~F~L▒~@个▒~H~]▒~K▒~L~V▒~O~R件 */
$header = \Typecho\Plugin::factory('admin/header.php')->header($header);
?><!DOCTYPE HTML>
<html>
<head>
<meta charset="<?php $options->charset(); ?>">
<meta name="renderer" content="webkit">
<meta name="referrer" content="same-origin" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<title><?php _e('%s - %s - Powered by Typecho', $menu->title, $options->title); ?></title>
<meta name="robots" content="noindex, nofollow">
<?php echo $header; ?>
</head>
<body<?php if (isset($bodyClass)) {echo ' class="' . $bodyClass . '"';} ?>>
加入后,登录功能即正常,但blog首页中的退出登录还有问题,点击退出类似于登录异常的情形,直接返回。
采取在首页相应的header处,添加相应代码。
themes中的header.php中代码如下(默认theme):
<?php if (!defined('__TYPECHO_ROOT_DIR__')) exit; ?>
<!DOCTYPE HTML>
<html>
<head>
<meta charset="<?php $this->options->charset(); ?>">
<meta name="renderer" content="webkit">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<title><?php $this->archiveTitle([
'category' => _t('分类 %s 下的文章'),
'search' => _t('包含关键字 %s 的文章'),
'tag' => _t('标签 %s 下的文章'),
'author' => _t('%s 发布的文章')
], '', ' - '); ?><?php $this->options->title(); ?></title>
<!-- 使用url函数转换相关路径 -->
<link rel="stylesheet" href="<?php $this->options->themeUrl('normalize.css'); ?>">
<link rel="stylesheet" href="<?php $this->options->themeUrl('grid.css'); ?>">
<link rel="stylesheet" href="<?php $this->options->themeUrl('style.css'); ?>">
<!-- 通过自有函数输出HTML头部信息 -->
<?php $this->header(); ?>
</head>
<body>
这儿首页的header()函数,实际对应到var/Widget/Archive.php中的header函数,
/**
* 输出头部元数据
*
* @param string|null $rule 规则
*/
public function header(?string $rule = null)
{
$rules = [];
$allows = [
'description' => htmlspecialchars($this->description),
'keywords' => htmlspecialchars($this->keywords),
'generator' => $this->options->generator,
'template' => $this->options->theme,
'pingback' => $this->options->xmlRpcUrl,
'xmlrpc' => $this->options->xmlRpcUrl . '?rsd',
'wlw' => $this->options->xmlRpcUrl . '?wlw',
'rss2' => $this->feedUrl,
'rss1' => $this->feedRssUrl,
'commentReply' => 1,
'antiSpam' => 1,
'atom' => $this->feedAtomUrl
];
/** 头部是否输出聚合 */
$allowFeed = !$this->is('single') || $this->allow('feed') || $this->makeSinglePageAsFrontPage;
if (!empty($rule)) {
parse_str($rule, $rules);
$allows = array_merge($allows, $rules);
}
$allows = self::pluginHandle()->headerOptions($allows, $this);
$title = (empty($this->archiveTitle) ? '' : $this->archiveTitle . ' » ') . $this->options->title;
$header = '<meta name="referrer" content="same-origin" />';
if (!empty($allows['description'])) {
$header .= '<meta name="description" content="' . $allows['description'] . '" />' . "\n";
}
if (!empty($allows['keywords'])) {
$header .= '<meta name="keywords" content="' . $allows['keywords'] . '" />' . "\n";
}
if (!empty($allows['generator'])) {
$header .= '<meta name="generator" content="' . $allows['generator'] . '" />' . "\n";
}
if (!empty($allows['template'])) {
$header .= '<meta name="template" content="' . $allows['template'] . '" />' . "\n";
}
if (!empty($allows['pingback']) && 2 == $this->options->allowXmlRpc) {
$header .= '<link rel="pingback" href="' . $allows['pingback'] . '" />' . "\n";
}
if (!empty($allows['xmlrpc']) && 0 < $this->options->allowXmlRpc) {
$header .= '<link rel="EditURI" type="application/rsd+xml" title="RSD" href="'
. $allows['xmlrpc'] . '" />' . "\n";
}
.....................
即可解决首页退出登录情况。