安全丨Typecho 后台登录验证码

首页 / 默认分类 / 正文

前言

验证码机制很常见,多用于过滤非法注册/登录数据。本文将简述在 Typecho 博客后台登录页面引入 Google 的 reCAPTCHA 验证码功能的步骤。
成功引入验证码

前置条件

  1. 你需要登录 Google reCaptcha 管理控制台 注册你的网站,并获取站点密钥和密钥。
  2. 因为 Google reCAPTCHA 在大陆境内存在网络不流畅的情况,所以你可能需要将 reCAPTCHA API 校验的接口地址替换为 www.recaptcha.net 地址。
  3. 按照文档,在 Typecho 的登录处理方法中引入相关 API。

reCAPTCHA v2

在控制台页面点击右上方的“+”号创建一个。
V2版本
点击“提交”后,就可以获取到 reCAPTCHA 接口的网站密钥(site key)及通信密钥(secret key),点击复制以供后续在集成代码中使用。

The easiest method for rendering the reCAPTCHA widget on your page is to include the necessary JavaScript resource and a g-recaptcha tag. The g-recaptcha tag is a DIV element with class name g-recaptcha and your site key in the data-sitekey attribute

在你的 Typecho 网站根目录下找到 /var/Widget/Login.php 文件并打开,修改其中的 action() 方法:

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('请输入密码'));
    // 新增
    $validator->addRule('g-recaptcha-response', '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', 'g-recaptcha-response'))) {
        Cookie::set('__typecho_remember_name', $this->request->name);

        /** 设置提示信息 */
        Notice::alloc()->set($error);
        $this->response->goBack();
    }

    /** Google reCaptcha v2 */
    $post_data = [        
        'secret' => '你的通信密钥',
        'response' => $_POST["g-recaptcha-response"]
    ]; 
    $recaptcha_result = $this->send_post('https://www.google.com/recaptcha/api/siteverify', $post_data);

    if(!($recaptcha_result && $recaptcha_result['success'])){
        /** 设置提示信息 */
        $error = "未通过 recaptcha 验证";
        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);
}

随后在 Login 方法中新增一个 send_post() 方法:

private function send_post($url, $post_data) {
    $postdata = http_build_query($post_data);
    $options = array(
        'http' => array(
            'method' => 'POST',
            'header' => 'Content-type:application/x-www-form-urlencoded',
            'content' => $postdata,
            'timeout' => 10 // 超时时间(单位:s)
        )
    );
    $context = stream_context_create($options);
    $recaptcha_json_result = file_get_contents($url, false, $context);  
    $result = json_decode($recaptcha_json_result, true);
    return $result;
}

接着,在网站根目录下找到 /admin/login.php 文件并打开,在登录表单中密码输入框与提交按钮之间新增验证码的 <div> 标签:

<p>
    <label for="password" class="sr-only"><?php _e('密码'); ?></label>
    <input type="password" id="password" name="password" class="text-l w-100" placeholder="<?php _e('密码'); ?>" />
</p>
<!-- 新增 -->
<p>
    <div class="g-recaptcha" data-sitekey="你的网站密钥"></div>
</p>
<p class="submit">
    <button type="submit" class="btn btn-l w-100 primary"><?php _e('登录'); ?></button>
    <input type="hidden" name="referer" value="<?php echo htmlspecialchars($request->get('referer') ?? ''); ?>" />
</p>

最后在文件底部的 footer 前插入 <script> 标签:

<?php 
include 'common-js.php';
?>
<script>
$(document).ready(function () {
    $('#name').focus();
});
</script>
<!-- 修改验证API地址 -->
<script src="https://www.recaptcha.net/recaptcha/api.js" async defer></script>
<?php
include 'footer.php';
?>

原JavaScript API地址
修改完毕后,重新打开登录页面,即可看到效果:
V2图片验证

reCAPTCHA v3

目前 reCaptcha 最新版本是 V3,相对比 V2, 它不需要用户去手动点击验证,它会在后台对用户的行为进行监测,在用户不知道的情况下完成验证。如果是纯前端的验证操作,那么会和 V2 一样,成功会返回一个 token。
按照官方文档,首先是在登录页面 /admin/login.php 中载入 JavaScript API:

<script src="https://www.recaptcha.net/recaptcha/api.js?hl=zh-CN"></script>

再添加一个回调函数来处理 token:

<script>
   function onSubmit(token) {
       document.getElementById("login-form").submit();
   }
</script>

最后向登录表单的提交按钮添加必要属性即可:

<p class="submit">
<button class="btn btn-l w-100 primary g-recaptcha"
        data-sitekey="你的网站密钥"
        data-callback='onSubmit'
        data-action='submit'><?php _e('登录'); ?></button>
<input type="hidden" name="referer" value="<?php echo htmlspecialchars($request->get('referer') ?? ''); ?>" />
</p>

使用 reCAPTCHA,会在网站的右下角显示其图标,如果觉得碍眼,可以在 admin/css/style.css 文件中新增一句使其隐藏:

/* hidden google recaptcha v3 */
.grecaptcha-badge { display: none; }

参考

  1. Google Developers - reCAPTCHA v2
  2. Google Developers - reCAPTCHA v3
  3. 知乎 - reCaptcha 新版,国内可无障碍使用
打赏
评论区
头像
文章目录