信不信由你,我们已经做了很多工作,使我们与 CouchDB 的交互成为一个简单的过程。在本章中,我们将直接进入 CouchDB 的核心部分,并开始对用户文档进行建模。
更具体地说,我们将:
- 安装 Bootstrap,一个来自 Twitter 的工具包,它将处理繁重的 CSS、表单、按钮等
- 仔细看看 CouchDB 在默认情况下是如何存储用户文档的,以及我们如何向它们添加字段
- 为用户添加基本功能,以便他们可以注册、登录和注销我们的应用
- 了解如何处理异常和错误
这将是我们迄今为止收获最大的一章;您会喜欢将一些标准身份验证和安全性转移到 CouchDB。系好安全带。这将是一次有趣的旅行!
在我们开始之前
我们已经使用了很多文件来测试骨骼和凹陷,但是您会注意到我们的应用看起来仍然非常贫乏。所以,让我们稍微美化一下设计。由于设计和实现 UI 不是本书的目的,我们将使用名为Bootstrap的工具包为我们完成繁重的工作。引导(http://twitter.github.com/bootstrap/ 由 Twitter 创建,旨在启动 web 应用和网站的开发。它将允许我们轻松完成前端开发,而无需做太多工作。让我们启动并运行引导程序,然后对布局进行整理。
通过安装 Bootstrap 清理我们的界面
设置引导非常简单。我们可以在他们的远程服务器上引用 CSS,但我们将下载并在本地调用 CSS,因为这是减少外部调用量的最佳实践。
行动时间-本地安装引导程序
安装引导程序是轻而易举的事;在本节中,我们将介绍安装它的基础知识。
- 打开浏览器,进入http://twitter.github.com/bootstrap/ 。
- 点击下载引导。
- 一个
.zip文件将下载到您的downloads文件夹中;通过双击或使用您喜爱的解压工具解压。 - You will find three directories inside the
bootstrapfolder, namelycss, img, andjs, each containing a number of files.

- Copy all of the files in each of these folders into the respective folders in your
vergeproject:/public/css, public/img, andpublic/js. Yourvergedirectory should look similar to the following screenshot when you're done:

刚才发生了什么事?
我们刚刚通过下载一个包含所有资产的 .zip文件,并将它们放在本地机器上的正确文件夹中,将 Twitter 的引导程序安装到我们的项目中。
只要看看我们项目中的新文件,您可能会注意到每个文件都会显示两次,一个文件名中有 min,另一个没有。这两个文件是相同的,只是文件名中包含 min的文件已经缩小。缩小意味着从代码中删除所有非必要字符以减小文件大小。删除的字符包括空格、换行符、注释等。因为这些文件是从网站按需加载的,所以它们必须尽可能小以加快应用的速度。如果您试图打开一个缩小的文件,通常很难判断发生了什么,这很好,因为我们不想在这些文件中进行任何更改。
可能很清楚所有这些文件都在做什么, css文件定义了一些引导的全局样式。如果我们愿意, img文件用于帮助我们在网站周围使用图标, js文件用于帮助我们在网站上添加互动、过渡和效果。但是,在 css文件夹中,有 bootstrap和 bootstrap-responsivecss 文件,这可能会让人困惑。响应式设计在过去的几年里,它真的发生了爆炸性的变化,而且就其本身而言,有很多关于它的书籍。简而言之, bootstrap在 bootstrap-responsive文件中包含样式,以帮助我们的站点在不同分辨率和设备上工作。因此,我们的网站应能在网络和现代移动设备上运行(大部分情况下)。
现在,您可能可以告诉我为什么选择使用引导;通过将文件复制到我们的项目中,我们获得了很多功能。但是,一切还没有完全连接起来;我们需要告诉我们的 layout.php文件在哪里查找,以便它可以使用这些新文件。
是时候采取行动了——包括引导和调整我们的布局以配合它
因为 Bootstrap 框架只是一系列文件,所以我们很容易将其包含在我们的项目中,就像我们在第 4 章中处理 master.css文件一样
- 首先在
layout.php文件
<head>
<link href="<?php echo $this->make_route('/css/bootstrap.min.css') ?>" rel="stylesheet" type="text/css" />
<link href="<?php echo $this->make_route('/css/master.css') ?>" rel="stylesheet" type="text/css" />
<link href="<?php echo $this->make_route('/css/bootstrap-responsive.min.css') ?>" rel="stylesheet" type="text/css" />
</head>
中的 master.css之前添加一个指向 bootstrap.min.css和 bootstrap-responsive.min.css的链接
- 接下来,我们通过添加以下代码来确保引导在旧版本的 Internet Explorer 和移动浏览器中运行良好:
<link href="<?php echo $this->make_route('/css/bootstrap- responsive.min.css') ?>" rel="stylesheet" type="text/css" />
<!--[if lt IE 9]>
<script src="http://html5shim.googlecode.com/svn/trunk/html5.js">
</script>
<![endif]-->
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
- 让我们用以下内容替换
views/layout.php文件的内容,为我们的应用创建一个干净、简单的包装器:
<body>
<div class="navbar navbar-fixed-top">
<div class="navbar-inner">
<div class="container">
<a class="btn btn-navbar" data-toggle="collapse" data- target=".nav-collapse">
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</a>
<a class="brand" href="<?php echo $this->make_route('/') ?>">Verge</a>
<div class="nav-collapse">
<ul class="nav">
<li><a href="<?php echo $this->make_route('/') ?>">
Home
</a></li>
<li>
<a href="<?php echo $this->make_route('/signup') ?>">Signup</a>
</li>
</ul>
</div>
</div>
</div>
</div>
<div class="container">
<?php include($this->content); ?>
</div>
</body>
- 删除
master.css文件的内容,替换为以下内容,对我们的布局进行一些小的调整:
.page-header {margin-top: 50px;}
input {height: 20px;}
刚才发生了什么事?
我们在 layout.php文件中加入了引导程序,并通过添加许多开发人员使用的 HTML5 垫片来确保 InternetExplorer 版本正常工作。如果您想了解更多有关这项工作的信息,请随时访问http://html5shim.googlecode.com/ 。
接下来,我们添加了一些 HTML 以符合 Bootstrap 中定义的 CSS 定义。对你来说,理解 HTML 为什么是这样设置并不重要,但是如果你好奇,你可以参考 Bootstrap 主页了解更多(http://twitter.github.com/bootstrap/ 。然后,我们在 main.css文件中添加了一些规则,在引导的基础上添加了其他样式。我这样做是为了在我们的应用中创建一点空间,这样事情就不会杂乱无章了。
如果您现在要通过进入进入主页 http://localhost/verge/ ,标题看起来确实很酷,但是主页需要一些爱。让我们快速清理主页。
行动时间到了-整理主页
Bootstrap 将再次为我们节省一些实时性;我们只需要一点 HTML 标记就可以让我们的应用看起来非常漂亮!将 views/home.php的内容替换为以下内容:
<div class="hero-unit">
<h1>Welcome to Verge!</h1>
<p>Verge is a simple social network that will make you popular.</p>
<p>
<a href="<?php echo $this->make_route('/signup') ?>" class="btn btn-primary btn-large">
Signup Now
</a>
</p>
</div>
刚才发生了什么事?
我们刚刚为我们的主页添加了一个整洁的布局,带有一个按钮,当人们来到我们的网站时,可以提示他们注册。请注意,我们从文件中删除了<? php echo $message; ?>,当时我们最初添加了<? php echo $message; ?>以向用户显示简单消息,但我们将在本章后面探索一种更干净的方法。
准备好看魔术了吗?打开浏览器,进入http://localhost/verge/ 。

我们几乎不需要在设计上花费任何时间,但我们已经有了一个更友好的应用。当我们深入处理用户时,这种新的设计将会派上用场。
准备好看些酷的东西了吗?尝试缩小浏览器窗口,看看会发生什么。

注意内容如何调整到屏幕大小;这意味着在移动设备上,您的应用将进行调整,以便于查看。Bootstrap 的快速响应样板代码只是一个开始。根据浏览器的大小,您可以选择显示和隐藏内容。
当浏览器窗口变小时,你会注意到导航栏也被压缩了,你看到的不是你的链接,而是一个有三个栏的按钮。试着点击它…什么都没发生!
该组件需要 Bootstrap 的 JavaScript 文件,以及一个名为jQuery的 JavaScript 库。这对我们来说还不是很重要,所以让我们在下一章中回到这一点上!
将所有用户文件移动到用户文件夹中
在这一部分中,我们的应用将开始有相当大的增长。如果我们继续像现在这样乱扔文件,我们的视图会变得相当混乱。让我们进行一点整理,并为 views目录添加一些结构。
行动时间-组织我们的用户视图
当我们继续为我们的应用创建视图时,我们最好有一些组织来确保我们保持良好和直观。
- 在
views目录中创建一个名为user的文件夹。 - Move the existing
signup.phpview into this folder. The resulting directory structure will look similar to the following screenshot:

- 我们需要更新
index.php并让它知道在哪里可以找到我们刚刚移动的注册视图:
get('/signup', function($app) {
$app->render('user/signup');
});
刚才发生了什么事?
通过创建一个 user文件夹,我们清理了 views文件夹结构,我们将把所有与用户相关的视图放入其中。然后,我们将现有的 signup.php文件移动到 user文件夹中,并告诉我们的 index.php文件在哪里可以找到 user/signup.php文件。请注意,注册页面 /signup的路径根本没有改变。
设计我们的用户文档
我们已经在第 3 章、开始使用 CouchDB 和 Futon中看到了 CouchDB 是如何看待用户文档的。在本章中,我们将学习如何利用现有的 CouchDB 功能,并在其上添加一些附加字段。
CouchDB 如何看待基本用户文档
CouchDB 已经有了一种存储用户文档的机制,我们已经看到并使用了这种机制。我们将为应用的用户使用相同的结构:
{
"_id": "org.couchdb.user:your_username",
"_rev": "1-b9af54a7cdc392c2c298591f0dcd81f3",
"name": "your_username",
"password_sha": "3bc7d6d86da6lfed6d4d82e1e4d1c3ca587aecc8",
"roles": [],
"salt": "9812acc4866acdec35c903f0cc072c1d",
"type": "user"
}
CouchDB 需要这七个字段,用户才能在 CouchDB 中正确操作:
_id是用户的唯一标识符。它需要以org.couchdb.user:开始,并以name属性的相同值结束。这些角色由_auth设计文件强制执行。关于设计文档,我们还没有谈太多。但是,在这一点上,您最好知道设计文档是直接在数据库中运行的代码。它们可用于强制执行验证和业务角色。_rev为文件的修订标识。我们很快触及了第 3 章中的修订内容。name是用户的用户名。此字段为_auth设计文件所需,且需与:字符后的文件_id值匹配。password_sha是密码与salt组合后的 SHA-1 加密值。稍后我们将介绍 SHA-1 加密。password_sha是密码与salt组合后的 SHA-1 加密值。稍后我们将介绍 SHA-1 加密。roles是用户可能拥有的权限数组。通过具有[]的值,我们知道该用户没有特权。salt是用户唯一的salt。salt与密码的明文值结合,通过 SHA-1 加密得到password_sha值。type是 CouchDB 用来标识文档类型的标识符。记住 CouchDB 是一个平面文档存储。此type字段标识文档的分类。
这些用户文档的独特之处在于它们具有少量必需的结构,但我们始终可以向这些文档添加其他字段。让我们下一步做吧!
向用户文档添加更多字段
让我们讨论一些我们知道需要从 Verge 用户那里收集信息的额外字段。请记住,如果应用需要,您可以随时添加更多字段。
- 用户名:我们知道我们希望能够存储一个唯一的用户名,这样我们的用户将拥有一个唯一的 URL,如
/user/johndoe。幸运的是,CouchDB 的name字段已经处理了这个功能。考虑到这一点,这里没什么可做的。我们将使用现有的name来代替! - 全名:该用户的全名,因此我们可以将该用户的名称显示为
John Doe。这将是一个用户友好的名称,我们可以使用它向访问用户显示,我们需要在文档中添加一个字段来支持这一点。 - 电子邮件:一个电子邮件地址,以便我们可以与用户进行通信,例如通知电子邮件:
<[[email protected]](https://raw.githubusercontent.com/apachecn/apachecn-php-zh/master/docs/couchdb-php-web-dev-begin-guide/mailto:[email protected])>。实际上,我们已经在当前类中保存了电子邮件,所以我们也可以忽略这一点。
听起来很简单;我们只需要添加一个字段!任何时候向文档添加新字段时,都应该考虑如何格式化它。让我们讨论一下 CouchDB 可以采用的不同方法。
讨论添加这些字段的选项
我们可以使用多种方式在 CouchDB 的基本用户文档上添加字段:
- 我们可以创建一种新类型的文档并将其命名为
verge_user。此文档将包含我们在应用中需要的任何其他用户属性,然后将有一个对用户文档的引用。 - 我们可以在用户文档中创建一个具有应用特定属性的数组,并在其中添加所有用户属性。
- 或者我们可以在用户文档中添加两个新字段。
我认为,就目前而言,我们都可以同意,通过添加一个字段,我们可以只使用上面提到的最后一个选项。
考虑到这一点,我们的最终文件将类似于以下内容:
{
"_id": "org.couchdb.user:johndoe",
"_rev": "1-b9af54a7cdc392c2c298591f0dcd81f3",
"name": "johndoe",
"full_name": "John Doe",
"email": "[email protected]",
"password_sha": "3bc7d6d86da6lfed6d4d82e1e4d1c3ca587aecc8",
"roles": [],
"salt": "9812acc4866acdec35c903f0cc072c1d",
"type": "user"
}
在这么多地方看到用户名的变化可能会让你感到奇怪: _id, name和 full_name。但是请记住 CouchDB 这样做是有充分理由的。通过将用户名存储在 _id中,CouchDB 将自动检查以确保每个用户名都是唯一的。
注
请记住,如果我们想开始存储 website, biography或 location等字段,我们可能需要更具创造性。我们将在本书后面的部分中详细讨论这一点。
增加对附加字段的支持
为了将这些字段添加到用户文档中,我们不需要在代码中做太多更改;我们只需要在 user.php类中添加几个变量。
行动时间-添加字段以支持用户文档
我们已经在 classes/user.php文件中设置了用户文档的基本结构,但是让我们再看看并添加几个字段。
- 我们目前没有在任何项目中设置
_id,但我们需要为用户文档设置_id。让我们打开classes/base.php,添加_id,这样我们就可以在任何文档上设置_id。
<?php
abstract class Base {
protected $_id;
protected $type;
- 我们需要将刚才讨论过的所有用户字段添加到
classes/user.php文件中,以及其他一些字段。在classes/user.php中添加以下代码,如下所示:
<?php
class User extends Base {
protected $name;
protected $email;
protected $full_name;
protected $salt;
protected $password_sha;
protected $roles;
刚才发生了什么事?
我们添加了将用户文档保存到系统中所需的所有字段。我们在 base.php类中添加了 _id,因为我们知道每个 CouchDB 文档都需要这个字段。我们能够在没有 _id的情况下生活,因为 CouchDB 已经自动为我们设置了一个 _id。然而,在本章中,我们需要能够为我们的用户文档设置和检索 _id。然后我们添加了 full_name和其他一些字段,这些字段可能会让您有点不舒服。 $salt和 $password_sha用于安全存储密码。这个过程更容易用一个例子来解释,所以我们将在注册过程中详细讨论这个过程。最后,我们添加了角色,这在本书中是空的,但对于开发基于角色的系统非常有用,允许某些用户能够看到应用的某些部分,等等。
现在我们已经定义了用户结构,我们需要遍历注册过程,这比我们目前所做的 CouchDB 文档创建要复杂一些。
注册流程
现在我们已经支持 user 类中的所有字段,让我们添加对用户注册 Verge 的支持。注册是一个有点复杂的过程,但我们将尝试通过一步一步的方式来打破它。在本节中,我们将:
- 定义我们的数据库管理员用户和密码,以便我们可以创建新的用户文档
- 创建一个新的注册界面来支持我们添加的所有字段
- 添加一个引导帮助程序,使创建表单输入更加容易
- 开发注册流程的快速而肮脏的实现
- 深入研究对密码使用 SHA-1 加密
- 重构我们的注册过程,使其更加结构化
一点管理员设置
在第三章中,我们锁定了 our _users数据库,因此我们可以保护我们的用户数据,这意味着我们在处理 _users数据库时,需要提供管理员登录。为此,我们将在 index.php文件的顶部为用户添加 PHP 常量和密码,以便在需要执行管理员功能时随时引用它。如果这看起来很混乱,不要担心;我们将在本书后面部分对此进行清理。
<?php
include 'lib/bones.php';
define('ADMIN_USER', 'tim');
define('ADMIN_PASSWORD', 'test');
更新接口
如果您现在打开浏览器查看注册页面,转到: http://localhost/verge/signup,您会注意到它已经过时,因为我们的新引导更改已经过时。事实上,你甚至可能看不到所有的输入框!让我们使用引导来帮助清理注册界面,使其看起来正确。
- 将
views/user/signup.php页面的所有内容替换为以下 HTML 代码:
<div class="page-header">
<h1>Signup</h1>
</div>
<div class="row">
<div class="span12">
<form class="form-vertical" action="<?php echo $this- >make_route('/signup') ?>" method="post">
<fieldset>
<label for="full_name">Full Name</label>
<input class="input-large" id="full_name" name="full_name" type="text" value="">
<label for="email">Email</label>
<input class="input-large" id="email" name="email" type="text" value="">
<div class="form-actions">
<button class="btn btn-primary">Sign Up!</button>
</div>
</fieldset>
</form>
</div>
</div>
- Refresh the signup page, and you'll see our awesome form now!

- 我们的表格看起来又漂亮又干净。但是,老实说,当我们添加更多字段时,为输入字段添加代码将开始成为一种痛苦。让我们创建一个小助手类来帮助我们创建一个可以很好地使用引导的 HTML 标记:
define('ROOT', __DIR__ . '/..');
require_once ROOT . '/lib/bootstrap.php';
require_once ROOT . '/lib/sag/src/Sag.php';
- 打开
lib/bootstrap.php,创建一个基础类。
<?php
class Bootstrap {
}
- 我们将创建一个名为
make_input的函数,它将接受四个参数:$id, $label, $type和$value。
<?php
class Bootstrap {
public static function make_input($id, $label, $type, $value = '') {
echo '<label for="' . $id . '">' . $label . '</label> <input class="input-large" id="' . $id . '" name="' . $id . '" type="' . $type . '" value="' . $value . '">';
}
}
- 返回到
views/user/signup.php,并简化代码以使用新的make_input函数。
<div class="page-header">
<h1>Signup</h1>
</div>
<div class="row">
<div class="span12">
<form action="<?php echo $this->make_route('/signup') ?>" method="post">
<fieldset>
<?php Bootstrap::make_input('full_name', 'Full Name', 'text'); ?>
<?php Bootstrap::make_input('email', 'Email', 'text'); ?>
<div class="form-actions">
<button class="btn btn-primary">Sign Up!</button>
</div>
</fieldset>
</form>
</div>
</div>
- 现在我们有了
lib/bootstrap.php来让我们的生活更轻松,让我们再向用户询问两个字段:username和password。
<fieldset>
<?php Bootstrap::make_input('full_name', 'Full Name', 'text'); ?>
<?php Bootstrap::make_input('email', 'Email', 'text'); ?>
<?php Bootstrap::make_input('username', 'Username', 'text'); ?>
<?php Bootstrap::make_input('password', 'Password', 'password'); ?>
<div class="form-actions">
<button class="btn btn-primary">Sign Up!</button>
</div>
</fieldset>
- Refresh your browser, and you'll see a dramatically improved signup form. If it doesn't look like the following screenshot, check to make sure your code matches mine.

我们的状态看起来很棒!不幸的是,当你点击注册时,它实际上还没有注册用户!。让我们在下一节中对此进行更改。
快速肮脏的注册
现在,我们将用户注册代码直接写入 index.php。我们将对这段代码进行几次重构,到本章结束时,我们将把大部分注册功能移到 classes/user.php文件中。
行动时间-处理简单用户注册
让我们一步一步地完成注册过程,在这个过程中,我们将从头开始重建注册 POST路由中的代码。我将解释每一段代码,然后,我们将在本节末尾做一个完整的回顾。
- 打开
index.php,开始收集简单字段:full_name, email和roles。字段full_name和email将直接来自表单提交,roles将设置为空数组,因为该用户没有特殊权限。
post('/signup', function($app) {
$user = new User();
$user->full_name = $app->form('full_name');
$user->email = $app->form('email');
$user->roles = array();
- 接下来,我们将要捕获用户提交的用户名,但我们要防止出现奇怪的字符或空格,因此我们将使用正则表达式将发布的用户名转换为不带任何特殊字符的小写字符串。最终结果将用作我们的
name字段,也是 ID 的一部分。请记住,用户文档要求_id以org.couchdb.user开头,以用户的name结尾。
post('/signup', function($app) {
$user = new User();
$user->full_name = $app->form('full_name'); $user->email = $app->form('email');
$user->roles = array();
$user->name = preg_replace('/[^a-z0-9-]/', '', strtolower($app- >form('username')));
$user->_id = 'org.couchdb.user:' . $user->name;
- 为了加密用户输入的密码的纯文本值,我们将临时设置一个字符串作为
salt的值。然后我们将明文密码传递到 SHA-1 函数中,并将其保存在password_sha中。稍后我们将深入了解 SHA-1 的工作原理。
post('/signup', function($app) {
$user = new User();
$user->full_name = $app->form('full_name'); $user->email = $app->form('email');
$user->roles = array();
$user->name = preg_replace('/[^a-z0-9-]/', '', strtolower($app- >form('username')));
$user->_id = 'org.couchdb.user:' . $user->name;
$user->salt = 'secret_salt';
$user->password_sha = sha1($app->form('password') . $user- >salt);
- 为了保存用户文档,我们需要将数据库设置为
_users,并以使用 PHP 常量设置的管理员用户身份登录。然后,我们将put用户使用 Sag 访问 CouchDB。
post('/signup', function($app) {
$user = new User();
$user->full_name = $app->form('full_name'); $user->email = $app->form('email');
$user->roles = array();
$user->name = preg_replace('/[^a-z0-9-]/', '', strtolower($app- >form('username')));
$user->_id = 'org.couchdb.user:' . $user->name;
$user->salt = 'secret_salt';
$user->password_sha = sha1($app->form('password') . $user- >salt);
$app->couch->setDatabase('_users');
$app->couch->login(ADMIN_USER, ADMIN_PASSWORD);
$app->couch->put($user->_id, $user->to_json());
- 最后,让我们关闭用户注册函数并呈现主页。
post('/signup', function($app) {
$user = new User();
$user->full_name = $app->form('full_name'); $user->email = $app->form('email');
$user->roles = array();
$user->name = preg_replace('/[^a-z0-9-]/', '', strtolower($app- >form('username')));
$user->_id = 'org.couchdb.user:' . $user->name;
$user->salt = 'secret_salt';
$user->password_sha = sha1($app->form('password') . $user- >salt);
$app->couch->setDatabase('_users');
$app->couch->login(ADMIN_USER, ADMIN_PASSWORD);
$app->couch->put($user->_id, $user->to_json());
$app->render('home');
});
刚才发生了什么事?
我们刚刚添加了代码来设置 CouchDB 用户文档的所有值。收集 full_name, email和 roles的值非常简单;我们只是执行了一个简单的调用,从发布的表单中获取值。设置 name变得更加复杂,我们将用户名的发布值转换为小写字符串,并使用正则表达式(Regex)函数将任何特殊字符更改为空白字符。使用干净的名称,我们将其附加到 org.couchdb.user并保存到文档的 _id。唷!那是一口。
快速进入加密世界,我们设置了一个静态(而且非常不安全) salt。将 salt与 SHA-1 函数中的纯文本密码相结合,生成一个加密密码,该密码保存到对象的 password_sha字段中。接下来,我们使用 setDatabase设置 Sag 的数据库,以便我们可以与 CouchDB 的 _users数据库进行对话。为了与用户进行通信,我们需要有管理员凭据。因此,我们使用 ADMIN_USER和 ADMIN_PASSWORD常量登录 CouchDB。最后,我们使用 HTTP 动词 PUT在 CouchDB 中创建文档,并为用户呈现主页。
让我们测试一下这一切,看看当我们提交注册表单时会发生什么。
- 进入浏览器中的
http://localhost/verge/signup打开注册页面。 - Fill in the form, with Full Name set as
John Doe, Email set as<[[email protected]](https://raw.githubusercontent.com/apachecn/apachecn-php-zh/master/docs/couchdb-php-web-dev-begin-guide/mailto:[email protected])>, Username set asjohndoe, and Password set astemp123. When you're finished, click on Sign Up!

- Your user has been created! Let's go to Futon by going to
http://localhost:5984/_utils, and look at the_usersdatabase for the new document.

- Perfect, everything should have saved correctly! When you are finished looking at this, delete the user by clicking on Delete Document. If you are not currently logged in as an admin user, you'll need to log in first before CouchDB will allow you to delete the document.
我让你删除了这个用户,因为如果每个用户都有一个等于secret_salt的salt,那么我们的密码也可能是纯文本的。为了让你明白为什么会这样,让我们后退一步,看看 SHA-1 所做的。
沙一
在安全方面,存储纯文本密码是最大的禁忌之一。因此,我们使用 SHA-1(http://en.wikipedia.org/wiki/SHA-1 创建加密散列。SHA-1 是由国家安全局创建的加密散列函数。SHA-1 背后的基本原理是,我们将密码与盐结合起来,以使密码无法区分。Salt是一个随机位串,我们将其与密码结合,以独特的方式对密码进行加密。
在我们刚刚编写的注册代码中,我们跳过了一些非常重要的内容。我们的 salt每次都被设置为 secret_salt。我们真正需要做的是为每个密码创建一个随机的 salt。
为了创建随机 salt,我们可以使用 CouchDB 的 RESTful JSON API。coach 在 http://localhost:5984/_uuids提供了一个资源,当调用该资源时,将返回一个唯一的 UUID供我们使用。每个 UUID都是一个长而随机的字符串,这正是盐所需要的!Sag 通过一个名为 generateIDs的函数使获得 UUID 变得非常容易。
让我们更新注册代码,以反映我们刚才讨论的内容。打开 index.php,更改 salt值的设置以匹配以下内容:
post('/signup', function($app) {
$user = new User();
$user->full_name = $app->form('full_name'); $user->email = $app->form('email');
$user->roles = array();
$user->name = preg_replace('/[^a-z0-9-]/', '', strtolower($app- >form('username')));
$user->_id = 'org.couchdb.user:' . $user->name;
$user->salt = $app->couch->generateIDs(1)->body->uuids[0];
$user->password_sha = sha1($app->form('password') . $user->salt);
$app->couch->setDatabase('_users');
$app->couch->login(ADMIN_USER, ADMIN_PASSWORD);
$app->couch->put($user->_id, $user->to_json());
$app->render('home');
});
再次测试注册流程
现在我们已经修复了 salt 的不安全性,让我们返回并再次尝试注册过程。
- 进入浏览器中的
http://localhost/verge/signup打开注册页面。 - 填写表格,全名为
John Doe、邮箱为<[[email protected]](https://raw.githubusercontent.com/apachecn/apachecn-php-zh/master/docs/couchdb-php-web-dev-begin-guide/mailto:[email protected])>、用户名为johndoe、密码为temp123。完成后,点击注册!。 - Your user has been created! Let's go to Futon by going to
http://localhost:5984/_utils, and look for our new document in the_usersdatabase. This time oursaltis random and unique!

重构注册流程
正如我前面提到的,我们将在我们的用户类中把这段代码重构成干净的函数,而不是直接放在 index.php中。我们想保留使用 index.php来处理路由、传递值和渲染视图。
行动时间-清理注册流程
让我们通过在 User类中创建一个名为 signup的公共函数来清理注册代码。
- 打开
classes/user.php,创建public注册功能。
public function signup($username,$password) {
}
- 输入与下面代码匹配的代码。它与我们在上一节中输入的代码几乎相同,只是我们没有引用
$user,而是引用$this。您还将注意到,full_name和email不在该函数中;你马上就会看到他们。
public function signup($username, $password) {
$bones = new Bones();
$bones->couch->setDatabase('_users');
$bones->couch->login(ADMIN_USER, ADMIN_PASSWORD);
$this->roles = array();
$this->name = preg_replace('/[^a-z0-9-]/', '', strtolower($username));
$this->_id = 'org.couchdb.user:' . $this->name;
$this->salt = $bones->couch->generateIDs(1)->body->uuids[0];
$this->password_sha = sha1($password . $this->salt);
$bones->couch->put($this->_id, $this->to_json());
}
- 打开
index.php,清理注册路径,使其匹配以下代码:
post('/signup', function($app) {
$user = new User();
$user->full_name = $app->form('full_name');
$user->email = $app->form('email');
$user->signup($app->form('username'), $app->form('password'));
$app->set('message', 'Thanks for Signing Up ' . $user->full_name . '!');
$app->render('home');
});
刚才发生了什么事?
我们创建了一个名为 signup的公共函数,它将容纳用户注册所需的所有注册代码。然后我们从 index.php注册路径复制了大部分代码。你会注意到里面有一些我们以前没见过的新东西。例如,对 $user的所有引用都已更改为 $this,因为我们使用的所有变量都附加到当前用户对象。您还会注意到,在开始时,我们创建了一个新的 Bones对象,以便使用它。我们还创建了 Sag,我们已经将其连接到骨骼,我们能够在不引起任何意外的情况下对其进行初始化,因为我们使用的是单例模式。请记住,单例模式允许我们调用在其他地方用于此请求的同一对象,而无需创建新对象。最后,我们回到了 index.php文件,简化了注册代码路径,以便只处理直接来自表单的值。然后,我们通过注册函数传递未触及的用户名和密码,以便处理它们并执行注册代码。
我们的注册代码现在是干净的,可以在类级别上运行,并且不受应用的影响。但是,如果你测试一下我们的表格,你会发现它还不是万无一失的。
异常处理和解决错误
如果您试图返回您的注册表单并保存另一个名为 John Doe的文档,您将看到一个非常不友好的错误页面,该页面看起来类似于以下屏幕截图:

如果您使用的浏览器不是 Chrome,您可能会收到不同的消息,但结果仍然相同。发生了一些我们没有预料到的糟糕情况,更糟糕的是,我们没有捕获这些异常。
出了问题怎么办?我们怎么才能知道是什么东西坏了?答案是:我们查看日志。
破译错误日志
当 PHP 和 Apache 一起工作时,它们会产生很多日志供我们查看。一些是访问级别日志,另一些是错误级别日志。让我们看看是否可以通过查看 Apache 错误日志来调查这里发生了什么。
行动时间-检查 Apache 日志
让我们从查找 Apache 的错误日志开始。
- 打开终端。
- 运行以下命令询问 Apache 的
config文件,它将日志保存到哪里:
grep ErrorLog /etc/apache2/httpd.conf
- 终端将返回类似于以下内容的内容:
# ErrorLog: The location of the error log file.
# If you do not specify an ErrorLog directive within a <VirtualHost>
ErrorLog "/private/var/log/apache2/error_log"
- 通过运行以下命令检索日志的最后几行:
tail /private/var/log/apache2/error_log
- 日志将向您显示一系列内容,但最重要的信息是这一条,即 PHP
Fatal error。你的可能略有不同,但整体信息是相同的。
[Sun Sep 11 22:10:31 2011] [error] [client 127.0.0.1] PHP Fatal error: Uncaught exception 'SagCouchException' with message 'CouchDB Error: conflict (Document update conflict.)' in /Library/WebServer/Documents/verge/lib/sag/src/Sag.php:1126\nStack trace:\n#0 /Library/WebServer/Documents/verge/lib/sag/src/Sag.php(286): Sag->procPacket('PUT', '/_users/org.cou...', '{"name":"johndoe')\n#1 /Library/WebServer/Documents/verge/classes/user.php(30): Sag->put('org.couchdb.use...', '{"name":"johndoe')\n#2 /Library/WebServer/Documents/verge/index.php(20): User->signup('w')\n#3 /Library/WebServer/Documents/verge/lib/bones.php(91): {closure}(Object(Bones))\n#4 /Library/WebServer/Documents/verge/lib/bones.php(17): Bones::register('/signup', Object(Closure), 'POST')\n#5 /Library/WebServer/Documents/verge/index.php(24): post('/signup', Object(Closure))\n#6 {main}\n thrown in /Library/WebServer/Documents/verge/lib/sag/src/Sag.php on line 1126, referer: http://localhost/verge/signup
[Sun Sep 11 22:10:31 2011] [error] [client 127.0.0.1] PHP Fatal error: Uncaught exception 'SagCouchException' with message 'CouchDB Error: conflict (Document update conflict.)' in /Library/WebServer/Documents/verge/lib/sag/src/Sag.php:1126\nStack trace:\n#0 /Library/WebServer/Documents/verge/lib/sag/src/Sag.php(286): Sag->procPacket('PUT', '/_users/org.cou...', '{"name":"johndoe')\n#1 /Library/WebServer/Documents/verge/classes/user.php(30): Sag->put('org.couchdb.use...', '{"name":"johndoe')\n#2 /Library/WebServer/Documents/verge/index.php(20): User->signup('w')\n#3 /Library/WebServer/Documents/verge/lib/bones.php(91): {closure}(Object(Bones))\n#4 /Library/WebServer/Documents/verge/lib/bones.php(17): Bones::register('/signup', Object(Closure), 'POST')\n#5 /Library/WebServer/Documents/verge/index.php(24): post('/signup', Object(Closure))\n#6 {main}\n thrown in /Library/WebServer/Documents/verge/lib/sag/src/Sag.php on line 1126, referer: http://localhost/verge/signup
刚才发生了什么事?
当我们发现日志文件保存在哪里时,我们询问了 Apache 将日志存储在哪里。我们使用 tail命令返回 Apache 日志的最后几行。
注
阅读日志的方法有很多,我们不会深入讨论,但要做让你感到舒服的事情。您可以通过搜索互联网对 tail进行更多研究,也可以在预装在 Mac OSX 机器上的控制台应用中打开日志。
查看我们收到的 PHP 致命错误非常令人困惑。如果你开始深入研究这个问题,你会发现这是一个 CouchDB 错误。更具体地说,此错误中的主线是:
Uncaught exception 'SagCouchException' with message 'CouchDB Error: conflict (Document update conflict.)
这个消息意味着 CouchDB 对我们传递给它的内容不满意,并且我们没有处理 Sag 以 SagCouchException. SagCouchException的形式向我们抛出的异常,这是一个帮助我们破译 CouchDB 抛出的异常的类,但是为了做到这一点,我们需要知道 CouchDB 正在传回什么状态码。
为了获取状态代码,我们需要查看 CouchDB 日志。
行动时间:查看 CouchDB 日志
由于我们安装 CouchDB 的方式与 Homebrew 相同,因此我们可以确保 CouchDB 日志都位于同一位置。考虑到这一点,让我们看看 CouchDB 日志。
- 打开终端。
- 通过运行以下命令检索日志的最后几行:
tail /usr/local/var/log/couchdb/couch.log
- 终端将返回类似于以下内容的内容:
[Mon, 12 Sep 2011 16:04:56 GMT] [info] [<0.879.0>] 127.0.0.1 - - 'GET' /_uuids?count=1 200
[Mon, 12 Sep 2011 16:04:56 GMT] [info] [<0.879.0>] 127.0.0.1 - - 'PUT' /_users/org.couchdb.user:johndoe 409
刚才发生了什么事?
我们使用 tail命令返回 CouchDB 日志的最后几行。
您会注意到的第一条记录是 /uuids?count=1,这是我们在 signup函数中获取 salt的 UUID。请注意,它返回了一个 200状态,这意味着它执行成功。
下一行写着'PUT' /_users/org.couchdb.user:johndoe,它返回了 409响应。 409响应表示存在更新冲突,这是因为我们传递给用户的名称与已经存在的名称相同。这对我们来说应该很容易修复,但我们首先需要讨论捕获错误。
捕捉错误
幸运的是,在我们友好的 try...catch语句的帮助下,捕捉错误相对容易。 try...catch语句允许您测试代码块的错误。 try块包含您试图运行的代码,如果出现问题,则执行 catch块。
try...catch语句的语法如下所示:
try {
// Code to execute
} catch {
// A problem occurred, do this
}
正如我前面提到的,Sag 包含一个名为 SagCouchException的异常类。这个类让我们能够看到 CouchDB 是如何响应的,然后我们可以采取相应的行动。
行动时间-使用 SagCouchException 处理文档更新冲突
在上一节中,我们确定我们的代码由于 409响应而被破坏。因此,让我们调整 classes/user.php文件中的注册函数,以使用 SagCouchException处理异常。
public function signup($username, $password) {
...
try {
$bones->couch->put($this->_id, $this->to_json());
} catch(SagCouchException $e) {
if($e->getCode() == "409") {
$bones->set('error', 'A user with this name already exists.');
$bones->render('user/signup');
exit;
}
}
}
刚才发生了什么事?
我们使用 try...catch语句来解决触发的重复文档更新冲突。通过使用 (SagCouchException $e)来施法,我们告诉它现在只能捕捉 SagCouchExceptions了。一旦捕获到此异常,我们将检查返回的代码。如果代码为 409,则我们正在设置一个带有错误消息的 error变量。然后我们需要重新显示用户/注册表单,以便用户有机会再次尝试注册过程。为了确保在这个错误之后不再执行代码,我们使用了 exit命令,以便应用停止在它所在的位置。
我们刚刚设置了一个 error变量。我们来讨论一下如何显示这个变量。
显示警报
在我们的应用中,我们将显示标准通知以响应用户交互,我们称之为警报。我们只是设置了一个错误变量用于错误警报,但我们也希望能够显示一条成功消息。
行动时间-显示警报
在本节中,我们将使用 bones 中现有的变量向用户显示警报消息。
- 打开
lib/bones.php并创建一个名为display_alert()的新函数。将调用此函数以查看是否设置了alert变量。如果设置了alert变量,我们将回显一些 HTML 以在布局上显示警报框。
public function display_alert($variable = 'error') {
if (isset($this->vars[$variable])) {
return "<div class='alert alert-" . $variable . "'><a class='close' data-dismiss='alert'>x</a>" . $this- >vars[$variable] . "</div>";
}
}
- 将代码添加到容器
div正内侧的layout.php中,以显示对display_flash功能的快速调用。
<div class="container">
<?php echo $this->display_alert('error'); ?>
<?php echo $this->display_alert('success'); ?>
<?php include($this->content); ?>
</div>
- 现在我们已经添加了这些 Flash 消息,让我们回到
index.php中的注册POST路线,并添加一条感谢用户注册的 Flash 消息。
$user->signup($app->form('username'), $app->form('password'));
$app->set('success', 'Thanks for Signing Up ' . $user->full_name . '!');
$app->render('home');
});
刚才发生了什么事?
我们创建了一个名为 display_alert的函数,该函数检查是否设置了带有传递变量的变量。如果设置了,那么在引导的帮助下,我们会在警报框中显示变量的内容。然后我们在 layout.php中添加了两行代码,这样我们就可以显示错误和成功的 Flash 消息。最后,我们在注册过程中添加了一条成功的 Flash 消息。
让我们测试一下。
- Go back and try to sign up for a user with the username of
johndoeagain.
您将看到此友好的错误消息,通知您出现问题:

- Now, let's test the success alert message. Change the username to
johndoe2. Click on Sign Up!, and you'll retrieve a nice and green alert.

- 即使有这些简单的提醒,我们的注册表单也不是完美的。随机的异常和错误可能会发生,我们将无法处理它们。更令人担忧的是,我们不要求填写表单中的字段。这些项目需要在我们的雷达上,但我们不可能在这本书中涵盖所有这些。
让我们继续讨论用户身份验证。
用户认证
现在我们已经创建了用户,我们肯定需要找到一种方法让他们登录到我们的系统。幸运的是,CouchDB 和 Sag 将在这方面为我们做大量的工作。在本节中,我们将:
- 设置登录表单
- 了解会话、cookies,以及 CouchDB 和 Sag 如何为我们处理身份验证
- 添加对用户注销的支持
- 已登录和未登录用户的 UI 处理方式不同
登录表单的设置
让我们创建一些登录表单,以便我们的用户可以登录到我们的网站并使用他们新创建的帐户。
有一个围棋英雄-设置登录的路线和表单
我们已经经历了创建页面、设置路由和创建表单的过程。那么,让我们看看你这次是否可以自己试一试。没有帮助我不会离开你的。我将首先告诉您需要尝试做什么,然后当您完成后,我们将检查并确保我们的代码匹配。
以下是您需要做的:
- 创建一个名为
user/login.php的新页面。 - 在
index.php文件中为登录页面创建新的GET和POST路由。 - 告诉登录页面的
GET路径以呈现user/login视图。 - 使用
user/signup.php作为向导,创建一个包含username和password字段的表单。 - 使用引导辅助程序和
submit按钮添加名为username和password的字段。
当你这么做的时候,我要去看电视。准备好后翻到下一页,我们来看看进展如何!
干得好我希望你能在不需要太多帮助的情况下做到这一点。如果您需要返回并查看旧代码以寻求帮助,请不要担心,因为许多开发人员在陷入困境时都会这样做。让我们看看你的代码是如何与我的代码相匹配的。
除此之外,您的 index.php文件应类似于以下内容:
get('/login', function($app) {
$app->render('user/login');
});
post('/login', function($app) {
});
您的 views/user/login.php页面应类似于以下内容:
<div class="page-header">
<h1>Login</h1>
</div>
<div class="row">
<div class="span12">
<form action="<?php echo $this->make_route('/login') ?>" method="post">
<fieldset>
<?php Bootstrap::make_input('username', 'Username', 'text'); ?>
<?php Bootstrap::make_input('password', 'Password', 'password'); ?>
<div class="form-actions">
<button class="btn btn-primary">Login</button>
</div>
</fieldset>
</form>
</div>
</div>
如果您的代码与我的不同,请确保将其更新为我的代码,以便我们的代码在将来匹配。
登录和注销
现在我们已经准备好了表单,让我们来谈谈我们需要做些什么来让表单真正工作。让我们快速讨论一下我们在登录过程中要完成的任务。
- Sag 将连接到 CouchDB
_users数据库。 - Sag 将把登录信息从我们的 PHP 直接传递给 CouchDB。
- 如果登录成功,CouchDB 将传回一个 cookie,表明您已通过身份验证。
- 然后,我们将查询 CouchDB 以获取当前登录的用户名,并将其保存到会话变量中以供以后使用。
如果您已经与其他数据库一起开发了一段时间,您将立即看到登录过程有何妙处。CouchDB 正在处理我们通常必须自己处理的大多数身份验证问题!
让我们看一下登录功能。幸运的是,这比注册过程简单得多。
行动时间-添加用户登录功能
我们将缓慢地完成这一过程,但我认为您会喜欢我们能够以如此快的速度将此功能添加到我们迄今为止编写的所有代码中。
- 打开
classes/user.php。 - 创建一个名为
login的public函数,我们可以将纯文本$password作为参数传递给该函数。
public function login($password) {
}
Create a new bones object and set the database to _users.
public function login($password) {
$bones = new Bones();
$bones->couch->setDatabase('_users');
}
- 为我们的登录代码创建一个
try...catch语句。在catch块中,我们将捕获错误代码401。如果它被触发,我们想告诉用户他们的登录不正确。
public function login($password) {
$bones = new Bones();
$bones->couch->setDatabase('_users');
try {
}
catch(SagCouchException $e) {
if($e->getCode() == "401") {
$bones->set('error', ' Incorrect login credentials.');
$bones->render('user/login');
exit;
}
}
}
- 添加启动会话的代码,然后通过 Sag 将用户名和密码传递到 CouchDB。用户成功登录后,从 CouchDB 获取当前用户的用户名。
public function login($password) {
$bones = new Bones();
$bones->couch->setDatabase('_users');
try {
$bones->couch->login($this->name, $password, Sag::$AUTH_COOKIE);
session_start();
$_SESSION['username'] = $bones->couch->getSession()->body- >userCtx->name;
session_write_close();
}
刚才发生了什么事?
我们在 user类中创建了一个名为 login的 public函数,允许用户登录。然后,我们创建了一个新的骨骼参照,以便访问 Sag。为了处理无效的登录凭证,我们创建了一个 try...catch块,并首先处理 catch块。这一次,我们正在检查错误代码 401。如果错误代码匹配,我们将 error变量设置为显示错误消息,呈现登录页面,最后退出当前代码。
接下来,我们通过将用户名和纯文本密码传递给 Sag 的登录方法以及设置 Sag::$AUTH_COOKIE来处理登录代码。这个参数告诉我们使用 CouchDB 的 cookie 身份验证。通过使用 cookie 身份验证,我们可以处理身份验证,而不必每次都传递用户名和密码。
在幕后,我们的用户名和密码被发布到 /_sessionURL。如果登录成功,它将返回一个 cookie,我们可以从此处开始对每个请求使用它,而不是用户名和密码。幸运的是,Sag 为我们处理了所有这些!
接下来,我们使用 session_start函数初始化了一个会话,该函数允许我们设置会话变量,只要会话存在,这些变量就会持续存在。然后,我们为用户名设置一个会话变量,该变量等于当前登录用户的用户名。我们通过使用 Sag 使用 $bones->couch->getSession().We抓取会话信息,然后使用 ->body()抓取响应主体,最后使用 userCtx抓取当前用户,进一步抓取 username属性。所有这些都产生了一行代码,如下所示:
$_SESSION['username'] = $bones->couch->getSession()->body->userCtx->name;
最后,我们使用 session_write_close编写会话变量并关闭会话。这将提高速度并减少锁定的机会。别担心;通过再次调用 session_start(),我们可以再次检索 session变量。
最后,我们需要在 index.php中为我们的 post路由添加登录功能。让我们一起快一点。
post('/login', function($app) {
$user = new User();
$user->name = $app->form('username');
$user->login($app->form('password'));
$app->set('success', 'You are now logged in!');
$app->render('home');
});
我们可以现在就去测试,但是让我们完成更多的事情,这样我们就可以完全测试这里发生了什么。
行动时间-添加用户注销功能
我打赌你认为登录脚本相当简单。等待,直到您看到我们允许用户注销是多么容易。
- 打开
classes/user.php,创建一个名为logout.
public static function logout() {
$bones = new Bones();
$bones->couch->login(null, null);
session_start();
session_destroy();
}
的public static函数
- 在
index.php文件中添加一条路由,并让它调用logout函数。
get('/logout', function($app) {
User::logout();
$app->redirect('/');
});
- 请注意,我们正在调用骨骼内部的一个新功能
aredirect`函数。为了实现这一点,让我们在
public function redirect($path = '/') {
header('Location: ' . $this->make_route($path));
}
的底部快速添加一个新函数`
`## 刚才发生了什么事?
我们添加了一个名为logout的public static函数。我们之所以这样做public static是因为对我们来说,哪个用户当前登录并不重要。我们只需要执行一些简单的会话级操作。首先,我们像往常一样创建了一个$bones实例化,但是接下来的部分非常有趣,所以我们设置了$bones->couch->login(null, null)。通过这样做,我们使当前用户成为匿名用户,有效地将他们注销。然后,我们调用了session_start和session_destroy.记住,使用session_start,我们可以访问会话,然后销毁会话,这将删除与当前会话相关的所有数据。
完成了login函数后,我们打开index.php并使用User::logout().调用了public static函数
最后,我们在index.php文件中添加了一个重定向函数。因此,我们很快在 Bones 中添加了一个函数,该函数允许我们使用make_route.将用户重定向到路由
处理当前用户
我们真的希望能够确定用户是否登录,并相应地更改导航。幸运的是,我们只需几行代码就可以实现这一点。
行动时间–处理当前用户
随着拼图的大部分内容就绪,让我们来看看用户是否登录,从而改变布局的过程。
- 让我们在
classes/user.php中添加一个名为current_user的函数,以便从会话中检索当前用户的用户名。
public static function current_user() {
session_start();
return $_SESSION['username'];
session_write_close();
}
- 在
classes/user.php中添加一个名为is_authenticated的公共静态函数,这样我们可以看到用户是否经过身份验证。
public static function is_authenticated() {
if (self::current_user()) {
return true;
} else {
return false;
}
}
- 现在我们已经完成了身份验证,让我们收紧
layout.php中的导航,以便根据用户是否登录显示不同的导航项目。
<ul class="nav">
<li><a href="<?php echo $this->make_route('/') ?>">Home</a></li>
<?php if (User::is_authenticated()) { ?>
<li>
<a href="<?php echo $this->make_route('/logout') ?>">
Logout
</a>
</li>
<?php } else { ?>
<li>
<a href="<?php echo $this->make_route('/signup') ?>">
Signup
</a>
</li>
<li>
<a href="<?php echo $this->make_route('/login') ?>">
Login
</a>
</li>
<?php } ?>
</ul>
刚才发生了什么事?
我们首先创建一个名为current_user的public static函数,该函数将检索存储在会话中的用户名。然后我们创建了另一个名为is_authenticated的public static函数。此功能检查current_user是否有用户名,如果有,则用户已登录。如果未登录,则表示用户当前未登录。
最后,我们很快进入了布局,以便在用户登录时显示主页和注销的链接,以及在用户当前未登录时显示主页、注册和登录的链接。
让我们测试一下:
- Open your browser to the login page by going to
http://localhost/verge/login. Notice that the header is displaying Home, Signup, and Login for you because you aren't currently logged in.

- Log in with the credentials of one of your user accounts. You'll receive a nice alert message, and the header changes to display Home and Logout.

总结
我希望你被我们在这一章中所取得的成就所震惊。我们的应用真的开始合二为一了。
具体而言,我们涵盖:
- 如何通过使用来自 Twitter 的 Bootstrap 显著改进界面
- 如何在现有 CouchDB 用户文档之上创建其他字段
- 如何处理日志中的错误和调试问题
- 如何完全构建该功能,以便用户可以在 Sag 和 CouchDB 的帮助下注册、登录和注销我们的应用
这只是我们申请的开始。我们还有很多事情要做。在下一章中,我们将开始处理用户配置文件,并在 CouchDB 中创建新文档。这些文件将成为我们用户的帖子。`