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 . ' &raquo; ') . $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";
        }
        .....................

即可解決首頁退出登錄情況。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章