原理
用过Typecho的都知道,Typecho本身就有登录/注册功能,只不过其主要目的还是给博主自己用的,并不适合开放给读者,一方面,界面实在太丑、太简陋了,另一方面,登录完成后直接跳转到管理后台也很不合适。但是,我们可以依葫芦画瓢,实现我们自己的前台登录/注册功能。因此,在动手之前,先简单了解一下Typecho内部是如何实现的还是很有必要的。 以登录为例,登录功能一共涉及到两个核心文件,一个文件是admin/login.php,核心代码如下: - <form action="<?php $options->loginAction(); ?>" method="post" name="login" role="form">
- <p>
- <label for="name" class="sr-only"><?php _e('用户名或邮箱'); ?></label>
- <input type="text" id="name" name="name" value="<?php echo $rememberName; ?>" placeholder="<?php _e('用户名或邮箱'); ?>" class="text-l w-100" autofocus />
- </p>
- <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('密码'); ?>" required />
- </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 $request->filter('html')->get('referer'); ?>" />
- </p>
- <p>
- <label for="remember">
- <input<?php if (\Typecho\Cookie::get('__typecho_remember_remember')): ?> checked<?php endif; ?> type="checkbox" name="remember" class="checkbox" value="1" id="remember" /> <?php _e('下次自动登录'); ?>
- </label>
- </p>
- </form>
复制代码这个文件主要存放的是和用户交互的表单界面,由于我们需要实现前台登录,所以这个文件肯定是不能再用了。我们只需要模仿着写一个自己的登录表单,然后将action设置为<?php $options->loginAction(); ?>(其本质就是一个类似于https://域名/index.php/action/login?_=xxxxxxxxxxx的请求地址)即可。 而另一个文件是var/Widget/Login.php,核心代码如下: - class Login extends Users implements ActionInterface
- {
- public function action()
- {
- // protect
- $this->security->protect();
- /** 如果已经登录 */
- if ($this->user->hasLogin()) {
- /** 直接返回 */
- $this->response->redirect($this->options->index);
- }
- ... ...
- /** 跳转验证后地址 */
- 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);
- }
- }
复制代码这是一个实现了ActionInterface接口的Login类,里面只有一个action()方法,用于处理登录逻辑,处理完成后,根据不同的条件跳转到不同的页面,其中referer是在前面的表单中通过hidden标签指定的,你可以通过它来自定义跳转的地址,不过我们前端登录是希望通过JS异步请求接口,然后根据返回值来更新界面,并不希望后端重定向,因此我们需要把重定向改为返回值,这个referer也用不上。 这两个文件的基本作用我们了解了,但Typecho是如何将表单提交地址https://域名/index.php/action/login与Login类关联起来的呢?这就涉及到了另一个文件:var/Widget/Action.php,从中可以看到如下路由映射关系: 这样,二者就联系起来了。 具体实现做过 主题 和 插件 开发的朋友通过上面的分析应该不难想到,如果不希望修改源代码,完全是可以通过插件调用Helper::addAction(...)和Helper::removeAction(...)来实现自己的登录处理逻辑,然后在主题中实现前端表单页面的。 但我没有这么做,原因前面提到过,源码我迟早是要改的,所以,这次也不想兜大圈子,直接改源码最省事,下面看看我的实现代码吧!还是以登录为例,首先是自定义表单,核心代码如下: - <form action="<?php $this->options->loginAction(); ?>" method="post" name="login" role="form">
- <div class="modal-header border-0">
- <h1 class="modal-title fs-5" id="loginModalLabel"><?php _e('登录') ?></h1>
- <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
- </div>
- <div class="modal-body pb-0">
- <div class="mb-3">
- <input type="text" name="name" class="form-control" placeholder="<?php _e('用户名或邮箱'); ?>" autofocus />
- </div>
- <div class="mb-3">
- <input type="password" name="password" class="form-control" placeholder="<?php _e('密码'); ?>" required />
- </div>
- <div class="form-check">
- <input class="form-check-input" type="checkbox" name="remember" value="1" id="remember">
- <label class="form-check-label" for="remember">
- <?php _e('下次自动登录'); ?>
- </label>
- </div>
- </div>
- <div class="modal-footer border-0 justify-content-between">
- <div class="d-flex align-items-center">
- <button type="submit" class="btn btn-dark"><?php _e('登录'); ?></button>
- <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>
- </div>
- <?php if ($this->options->allowRegister): ?>
- <div class="d-flex align-items-center">
- <span><?php _e('没有账号?') ?></span>
- <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>
- </div>
- <?php endif; ?>
- </div>
- </form>
复制代码这是一个基于Bootstrap 5实现的模态窗,效果如下图所示: 代码看似很多,但核心其实就是账号、密码,再加一个提交按钮,其它的都可以无视,而具体提交的JS代码如下: - const loginForm = document.querySelector("#loginModal form");
- formSubmit(loginForm, "frontLogin");
- function formSubmit(form, doMethod) {
- if (!form) {
- return;
- }
- form.addEventListener("submit", (e) => {
- e.preventDefault();
- const form = e.target;
- const formData = new FormData(form);
- const data = {};
- for (const [key, value] of formData.entries()) {
- data[key] = value;
- }
- data.do = doMethod;
- axios
- .post(form.action, data, {
- headers: {
- "Content-Type": "application/x-www-form-urlencoded",
- },
- })
- .then(function (response) {
- const data = response.data;
- showToast(data.message, data.success ? "success" : "error");
- if (data.success) {
- window.location.reload();
- }
- })
- .catch(function (error) {
- console.log(error);
- });
- });
- }
复制代码这里有三点需要说明一下: 为了方便,这里用到了axios库,你也可以用其它的,如JQuery或原生的XMLHttpRequest、fetch等;
除了表单参数之外,还为请求指定了一个do参数,值为frontLogin,用于区分前台登录和后台登录;
为了简单起见,这里登录成功后直接调用了window.location.reload();方法重新加载当前页面,事实上,你也可以通过JS更新DOM节点,用户体验会更好一些。
前端提交后,请求会经过路由最终到达Login类的action()方法,这时我们需要通过如下代码来处理前台登录逻辑: - class Login extends Users implements ActionInterface
- {
- /**
- * 初始化函数
- *
- * @access public
- * @return void
- */
- public function action()
- {
- $this->on($this->request->is('do=frontLogin'))->frontLogin();
- // protect
- $this->security->protect();
- ...
- }
- function frontLogin()
- {
- /** 如果已经登录 */
- if ($this->user->hasLogin()) {
- echo json_encode([
- 'success' => true,
- 'message' => _t('您已经登录了')
- ]);
- exit;
- }
- $expire = 30 * 24 * 3600;
- $name = $this->request->get('name');
- $password = $this->request->get('password');
- if (empty($name) || empty($password)) {
- echo json_encode([
- 'success' => false,
- 'message' => _t('账号或密码不能为空')
- ]);
- exit;
- }
- $valid = $this->user->login($name, $password, false, $this->request->is('remember=1') ? $expire : 0);
- if (!$valid) {
- echo json_encode([
- 'success' => false,
- 'message' => _t('账号或密码错误')
- ]);
- exit;
- }
- echo json_encode([
- 'success' => true,
- 'message' => _t('登录成功')
- ]);
- exit;
- }
- }
复制代码这里也有几点需要说明的: 在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/
|