查看: 26|回复: 0

[技术教程] Typecho实现前台登录/注册

[复制链接]
  • 打卡等级:常驻代表
  • 打卡总天数:42
  • 打卡月天数:6
  • 打卡总奖励:351
  • 最近打卡:2025-07-17 08:43:57
发表于 2025-7-10 17:26:03 | 显示全部楼层 |阅读模式
原理
用过Typecho的都知道,Typecho本身就有登录/注册功能,只不过其主要目的还是给博主自己用的,并不适合开放给读者,一方面,界面实在太丑、太简陋了,另一方面,登录完成后直接跳转到管理后台也很不合适。但是,我们可以依葫芦画瓢,实现我们自己的前台登录/注册功能。因此,在动手之前,先简单了解一下Typecho内部是如何实现的还是很有必要的。
以登录为例,登录功能一共涉及到两个核心文件,一个文件是admin/login.php,核心代码如下:
  1. <form action="<?php $options->loginAction(); ?>" method="post" name="login" role="form">
  2.     <p>
  3.         <label for="name" class="sr-only"><?php _e('用户名或邮箱'); ?></label>
  4.         <input type="text" id="name" name="name" value="<?php echo $rememberName; ?>" placeholder="<?php _e('用户名或邮箱'); ?>" class="text-l w-100" autofocus />
  5.     </p>
  6.     <p>
  7.         <label for="password" class="sr-only"><?php _e('密码'); ?></label>
  8.         <input type="password" id="password" name="password" class="text-l w-100" placeholder="<?php _e('密码'); ?>" required />
  9.     </p>
  10.     <p class="submit">
  11.         <button type="submit" class="btn btn-l w-100 primary"><?php _e('登录'); ?></button>
  12.         <input type="hidden" name="referer" value="<?php echo $request->filter('html')->get('referer'); ?>" />
  13.     </p>
  14.     <p>
  15.         <label for="remember">
  16.             <input<?php if (\Typecho\Cookie::get('__typecho_remember_remember')): ?> checked<?php endif; ?> type="checkbox" name="remember" class="checkbox" value="1" id="remember" /> <?php _e('下次自动登录'); ?>
  17.         </label>
  18.     </p>
  19. </form>
复制代码
这个文件主要存放的是和用户交互的表单界面,由于我们需要实现前台登录,所以这个文件肯定是不能再用了。我们只需要模仿着写一个自己的登录表单,然后将action设置为<?php $options->loginAction(); ?>(其本质就是一个类似于https://域名/index.php/action/login?_=xxxxxxxxxxx的请求地址)即可。
而另一个文件是var/Widget/Login.php,核心代码如下:
  1. class Login extends Users implements ActionInterface
  2. {
  3.     public function action()
  4.     {
  5.         // protect
  6.         $this->security->protect();

  7.         /** 如果已经登录 */
  8.         if ($this->user->hasLogin()) {
  9.             /** 直接返回 */
  10.             $this->response->redirect($this->options->index);
  11.         }

  12.         ... ...

  13.         /** 跳转验证后地址 */
  14.         if (!empty($this->request->referer)) {
  15.             /** fix #952 & validate redirect url */
  16.             if (
  17.                 0 === strpos($this->request->referer, $this->options->adminUrl)
  18.                 || 0 === strpos($this->request->referer, $this->options->siteUrl)
  19.             ) {
  20.                 $this->response->redirect($this->request->referer);
  21.             }
  22.         } elseif (!$this->user->pass('contributor', true)) {
  23.             /** 不允许普通用户直接跳转后台 */
  24.             $this->response->redirect($this->options->profileUrl);
  25.         }

  26.         $this->response->redirect($this->options->adminUrl);
  27.     }
  28. }
复制代码
这是一个实现了ActionInterface接口的Login类,里面只有一个action()方法,用于处理登录逻辑,处理完成后,根据不同的条件跳转到不同的页面,其中referer是在前面的表单中通过hidden标签指定的,你可以通过它来自定义跳转的地址,不过我们前端登录是希望通过JS异步请求接口,然后根据返回值来更新界面,并不希望后端重定向,因此我们需要把重定向改为返回值,这个referer也用不上。
这两个文件的基本作用我们了解了,但Typecho是如何将表单提交地址https://域名/index.php/action/login与Login类关联起来的呢?这就涉及到了另一个文件:var/Widget/Action.php,从中可以看到如下路由映射关系:
3304607612.png
这样,二者就联系起来了。
具体实现
做过 主题 和 插件 开发的朋友通过上面的分析应该不难想到,如果不希望修改源代码,完全是可以通过插件调用Helper::addAction(...)和Helper::removeAction(...)来实现自己的登录处理逻辑,然后在主题中实现前端表单页面的。
但我没有这么做,原因前面提到过,源码我迟早是要改的,所以,这次也不想兜大圈子,直接改源码最省事,下面看看我的实现代码吧!还是以登录为例,首先是自定义表单,核心代码如下:
  1. <form action="<?php $this->options->loginAction(); ?>" method="post" name="login" role="form">
  2.     <div class="modal-header border-0">
  3.         <h1 class="modal-title fs-5" id="loginModalLabel"><?php _e('登录') ?></h1>
  4.         <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
  5.     </div>
  6.     <div class="modal-body pb-0">
  7.         <div class="mb-3">
  8.             <input type="text" name="name" class="form-control" placeholder="<?php _e('用户名或邮箱'); ?>" autofocus />
  9.         </div>
  10.         <div class="mb-3">
  11.             <input type="password" name="password" class="form-control" placeholder="<?php _e('密码'); ?>" required />
  12.         </div>
  13.         <div class="form-check">
  14.             <input class="form-check-input" type="checkbox" name="remember" value="1" id="remember">
  15.             <label class="form-check-label" for="remember">
  16.                 <?php _e('下次自动登录'); ?>
  17.             </label>
  18.         </div>
  19.     </div>
  20.     <div class="modal-footer border-0 justify-content-between">
  21.         <div class="d-flex align-items-center">
  22.             <button type="submit" class="btn btn-dark"><?php _e('登录'); ?></button>
  23.             <a href="#" class="ms-2 link-secondary link-offset-2 link-underline-opacity-0 link-underline-opacity-100-hover" data-bs-toggle="modal" data-bs-target="#forgetModal"><?php _e('忘记密码?'); ?></a>
  24.         </div>

  25.         <?php if ($this->options->allowRegister): ?>
  26.             <div class="d-flex align-items-center">
  27.                 <span><?php _e('没有账号?') ?></span>
  28.                 <a href="#" class="link-secondary link-offset-2 link-underline-opacity-0 link-underline-opacity-100-hover" data-bs-toggle="modal" data-bs-target="#registerModal"><?php _e('立即注册'); ?></a>
  29.             </div>
  30.         <?php endif; ?>
  31.     </div>
  32. </form>
复制代码
这是一个基于Bootstrap 5实现的模态窗,效果如下图所示:
211990994.png
代码看似很多,但核心其实就是账号、密码,再加一个提交按钮,其它的都可以无视,而具体提交的JS代码如下:
  1. const loginForm = document.querySelector("#loginModal form");
  2. formSubmit(loginForm, "frontLogin");

  3. function formSubmit(form, doMethod) {
  4.   if (!form) {
  5.     return;
  6.   }

  7.   form.addEventListener("submit", (e) => {
  8.     e.preventDefault();
  9.     const form = e.target;
  10.     const formData = new FormData(form);
  11.     const data = {};
  12.     for (const [key, value] of formData.entries()) {
  13.       data[key] = value;
  14.     }

  15.     data.do = doMethod;
  16.     axios
  17.       .post(form.action, data, {
  18.         headers: {
  19.           "Content-Type": "application/x-www-form-urlencoded",
  20.         },
  21.       })
  22.       .then(function (response) {
  23.         const data = response.data;
  24.         showToast(data.message, data.success ? "success" : "error");
  25.         if (data.success) {
  26.           window.location.reload();
  27.         }
  28.       })
  29.       .catch(function (error) {
  30.         console.log(error);
  31.       });
  32.   });
  33. }
复制代码
这里有三点需要说明一下:
为了方便,这里用到了axios库,你也可以用其它的,如JQuery或原生的XMLHttpRequest、fetch等;
除了表单参数之外,还为请求指定了一个do参数,值为frontLogin,用于区分前台登录和后台登录;
为了简单起见,这里登录成功后直接调用了window.location.reload();方法重新加载当前页面,事实上,你也可以通过JS更新DOM节点,用户体验会更好一些。
前端提交后,请求会经过路由最终到达Login类的action()方法,这时我们需要通过如下代码来处理前台登录逻辑:
  1. class Login extends Users implements ActionInterface
  2. {
  3.     /**
  4.      * 初始化函数
  5.      *
  6.      * @access public
  7.      * @return void
  8.      */
  9.     public function action()
  10.     {
  11.         $this->on($this->request->is('do=frontLogin'))->frontLogin();

  12.         // protect
  13.         $this->security->protect();
  14.         ...
  15.     }

  16.     function frontLogin()
  17.     {
  18.         /** 如果已经登录 */
  19.         if ($this->user->hasLogin()) {
  20.             echo json_encode([
  21.                 'success' => true,
  22.                 'message' => _t('您已经登录了')
  23.             ]);
  24.             exit;
  25.         }

  26.         $expire = 30 * 24 * 3600;
  27.         $name = $this->request->get('name');
  28.         $password = $this->request->get('password');
  29.         if (empty($name) || empty($password)) {
  30.             echo json_encode([
  31.                 'success' => false,
  32.                 'message' => _t('账号或密码不能为空')
  33.             ]);
  34.             exit;
  35.         }

  36.         $valid = $this->user->login($name, $password, false, $this->request->is('remember=1') ? $expire : 0);
  37.         if (!$valid) {
  38.             echo json_encode([
  39.                 'success' => false,
  40.                 'message' => _t('账号或密码错误')
  41.             ]);
  42.             exit;
  43.         }

  44.         echo json_encode([
  45.             'success' => true,
  46.             'message' => _t('登录成功')
  47.         ]);
  48.         exit;
  49.     }
  50. }
复制代码
这里也有几点需要说明的:
在action()的开始位置调用$this->on($this->request->is('do=frontLogin'))->frontLogin();拦截请求,这里的do=frontLogin就是我们表单提交时传入的参数,参数命中后,请求会交由frontLogin()方法处理;
frontLogin()需要输出一个JSON对象,而不是直接跳转,输出完成之后,需要执行exit中断请求,否则代码会继续执行。
结尾
通过上述几个步骤,我们自定义的前台登录功能就实现了。注册的思路也是一样的,原文件分别对应admin/register.php和var/Widget/Register.php两个文件,就不再赘述了。
原文地址:https://ilaozhu.com/archives/2106/

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

快速回复 返回顶部 返回列表