《Redis入门指南》一5.1 PHP与Redis

5.1 PHP与Redis

Redis入门指南
Redis官方推荐的PHP客户端是Predis1和phpredis2。前者是完全使用PHP代码实现的原生客户端,而后者则是使用C语言编写的PHP扩展。在功能上两者区别并不大,就性能而言后者会更胜一筹。考虑到很多主机并未提供安装PHP扩展的权限,本节会以Predis为示例介绍如何在PHP中使用Redis。

虽然Predis的性能逊于phpredis,但是除非执行大量Redis命令,否则很难区分二者的性能。而且实际应用中执行Redis命令的开销更多在网络传输上,单纯注重客户端的性能意义不大。读者在开发时可以根据自己的项目需要来权衡使用哪个客户端。

Predis对PHP版本的最低要求为5.3。

5.1.1 安装

安装Predis可以克隆其版本库(git clone git://github.com/nrk/predis.git),也可以直接从GitHub项目主页中下载代码的ZIP压缩包。如目前最新版v0.8.1的下载地址为https://github.com/nrk/predis/archive/v0.8.1.zip。下载后解压并将整个文件夹复制到项目目录中即可使用。

使用时首先需要引入autoload.php文件:

require './predis/autoload.php';

Predis使用了PHP 5.3中的命名空间特性,并支持PSR-0标准3。autoload.php文件通过定义PHP的自动加载函数实现了该标准,所以引入了autoload.php文件后就可以自动根据命名空间和类名来自动载入相应的文件了。例如:

$redis = new Predis\Client();

会自动加载Predis目录下的Client.php文件。如果你的项目使用的PHP框架已经支持了这一标准那么就无需再次引入autoload.php了。

5.1.2 使用方法

首先创建一个到Redis的连接:

$redis = new Predis\Client();

该行代码会默认Redis的地址为127.0.0.1,端口为6379。如果需要更改地址或端口,可以使用:

$redis = new Predis\Client(array(
   'scheme' => 'tcp',
   'host'  => '127.0.0.1',
   'port'  => 6379,
));

作为开始,我们首先使用GET命令作为测试:

echo $redis->get('foo');

该行代码获得了键名为 foo 的字符串类型键的值并输出出来,如果不存在则会返回NULL。

当 foo 键的类型不是字符串类型(如列表类型)时会报异常,可以为该行代码加上异常处理:

try {
  echo $redis->get('foo');
} catch (Exception $e) {
  echo "Message: {$e->getMessage()}";
}

这时输出的内容为:“Message: ERR Operation against a key holding the wrong kind of value”。

调用其他命令的方法和GET命令一样,如要执行LPUSH numbers 1 2 3:

$redis->lpush('numbers', '1', '2', '3');

5.1.3 简便用法

为了使开发更方便,Predis为许多命令额外提供了简便用法,这里选择几个典型的用法依次介绍。

1.MGET/MSET
Predis调用MSET命令时支持将PHP的关联数组直接作为参数,就像这样:

$userName = array(
  'user:1:name' => 'Tom',
  'user:2:name' => 'Jack'
);
// 相当于 $redis->mset('user:1:name', 'Tom', 'user:2:name', 'Jack');
$redis->mset($userName);

同样MGET命令支持一个数组作为参数:

$users = array_keys($userName);
print_r($redis->mget($users));

打印的结果为:

Array
(
  [0] => Tom
  [1] => Jack
)
2. HMSET/HMGET/HGETALL
Predis调用HMSET的方式和MSET类似,如:

$user1 = array(
  'name' => 'Tom',
  'age' => '32'
);

$redis->hmset('user:1', $user1);

HMGET与MGET类似,不再赘述。最方便的是HGETALL命令,Predis会将Redis返回的结果组装成关联数组返回:

$user = $redis->hgetall('user:1');
echo $user['name']; // 'Tom'

3.LPUSH/SADD/ZADD
LPUSH和SADD的调用方式类似:

$items = array('a', 'b');

// 相当于$redis->lpush('list', 'a', 'b');
$redis->lpush('list', $items);

// 相当于$redis->sadd('set', 'a', 'b');
$redis->sadd('set', $items);
而ZADD的调用方式为:

$itemScore = array(
  'Tom' => '100',
  'Jack' => '89'
);

// 相当于$redis->zadd('zset', '100', 'Tom', '89', 'Jack');
$redis->zadd('zset', $itemScore);

4.SORT
在Predis中调用SORT命令的方式和其他命令不同,必须将SORT命令中除键名外的参数作为关联数组传入到函数中。如对SORT mylist BY weight_ LIMIT 0 10 GET value_ GET # ASC ALPHA STORE result这条命令而言,使用Predis的调用方法如下:

$redis->sort('mylist', array(
  'by'  => 'weight_*',
  'limit' => array(0, 10),
  'get'  => array('value_*', '#'),
  'sort' => 'asc',
  'alpha' => true,
  'store' => 'result'
));

5.1.4 实践:用户注册登录功能

本节将使用PHP和Redis实现用户注册登录功能,下面分模块来介绍具体实现方法。

1.注册
需求描述:用户注册时需要提交邮箱、登录密码和昵称。其中邮箱是用户的唯一标识,每个用户的邮箱不能重复,但允许用户修改自己的邮箱。

我们使用散列类型来存储用户的资料,键名为user:用户ID。其中用户ID是一个自增的数字,之所以使用 ID 而不是邮箱作为用户的标识是因为考虑到在其他键中可能会通过用户的标识与用户对象相关联,如果使用邮箱作为用户的标识的话在用户修改邮箱时就不得不同时需要修改大量的键名或键值。为了尽可能地减少要修改的地方,我们只把邮箱作为该散列键的一个字段。为此还需要使用一个散列类型的键email.to.id来记录邮箱和用户ID间的对应关系以便在登录时能够通过邮箱获得用户的ID。

用户填写并提交注册表单后首先需要验证用户输入,我们在项目目录中建立一个register.php文件来实现用户注册的逻辑。验证部分的代码如下:

// 设置Content-type以使浏览器可以使用正确的编码显示提示信息,
// 具体的编码需要根据文件实际编码选择,此处是utf-8。
header("Content-type: text/html; charset=utf-8");

if(!isset($_POST['email']) ||
  !isset($_POST['password']) ||
  !isset($_POST['nickname'])) {
  echo '请填写完整的信息。';
  exit;
}

$email = $_POST['email'];
// 验证用户提交的邮箱是否正确
if(!filter_var($email, FILTER_VALIDATE_EMAIL)) {
   echo '邮箱格式不正确,请重新检查';
  exit;
}

$rawPassword = $_POST['password'];
// 验证用户提交的密码是否安全
if(strlen($rawPassword) < 6) {
   echo '为了保证安全,密码长度至少为6。';
  exit;
}

$nickname = $_POST['nickname'];
//不同的网站对用户昵称有不同的要求,这里不再做检查,即使是空也可以。

// 而后我们需要判断用户提交的邮箱是否被注册了:
$redis = new Predis\Client();
if($redis->hexists('email.to.id', $email)) {
   echo '该邮箱已经被注册过了。';
  exit;
}

验证通过后接下来就需要将用户资料存入Redis中。在存储的时候要记住使用散列函数处理用户提交的密码,避免在数据库中存储明文密码。原因是如果数据库中数据泄露(外部原因或内部原因都有可能),攻击者也无法获得用户的真实密码,也便无法正常地登录进系统。更重要的是考虑到用户很可能在其他网站中也使用了同样的密码,所以明文密码泄露还会给用户造成额外的损失。

除此之外,还要避免使用速度较快的散列函数处理密码以防止攻击者使用穷举法破解密码,并且需要为每个用户生成一个随机的“盐”(salt)以避免攻击者使用彩虹表破解。这里作为示例,我们使用Bcrypt算法来对密码进行散列。PHP 5.3中提供的crypt函数支持Bcrypt算法,我们可以实现一个函数来随机生成盐并调用crypt函数获得散列后的密码:

function bcryptHash($rawPassword, $round = 8)
{   
  if ($round < 4 || $round > 31) $round = 8;
  $salt = '$2a$' . str_pad($round, 2, '0', STR_PAD_LEFT) . '$';
  $randomValue = openssl_random_pseudo_bytes(16);
  $salt .= substr(strtr(base64_encode($randomValue), '+', '.'), 0, 22);
  return crypt($rawPassword, $salt);
}

提示

openssl_random_pseudo_bytes函数需要安装OpenSSL扩展。

之后使用如下代码获得散列后的密码:

$hashedPassword = bcryptHash($rawPassword);

存储用户资料就很简单了,所有命令都在第3章介绍过了。代码如下:

require './predis/autoload.php';
$redis = new Predis\Client();
// 首先获取一个自增的用户ID
$userID = $redis->incr('users:count');
// 存储用户信息
$redis->hmset("user:{$userID}", array(
  'email'  => $email,
  'password'  => $hashedPassword,
  'nickname'  => $nickname
));

// 记得记录下邮箱和用户ID的对应关系
$redis->hset('email.to.id', $email, $userID);

// 提示用户注册成功
echo '注册成功!';

大部分情况下在注册时我们需要验证用户的邮箱,不过这部分的逻辑与忘记密码部分相似,所以在这里不做更多的介绍。

2.登录
需求描述:用户登录时需要提交邮箱和登录密码,如果正确则输出“登录成功”,否则输出“用户名或密码错误”。

当用户提交邮箱和登录密码后首先通过email.to.id键获得用户ID,然后将用户提交的登录密码使用同样的盐进行散列并与数据库存储的密码比对,如果一样则表示登录成功。我们新建一个login.php文件来处理用户的登录,处理该逻辑的部分代码如下:

header("Content-type: text/html; charset=utf-8");
if(!isset($_POST['email']) ||
  !isset($_POST['password'])) {
  echo '请填写完整的信息。';
  exit;
}

$email = $_POST['email'];
$rawPassword = $_POST['password'];

require './predis/autoload.php';
$redis = new Predis\Client();

// 获得用户的ID
$userID = $redis->hget('email.to.id', $email);
if(!$userID) {
   echo '用户名或密码错误。';
  exit;
}

$hashedPassword = $redis->hget("user:{$userID}", 'password');

现在我们得到了之前存储过的经过散列后的密码,接着定义一个函数来对用户提交的密码进行散列处理。bcryptHash函数中返回的密码中已经包含了盐,所以只需要直接将散列后的密码作为crypt函数的第二个参数,crypt函数会自动地提取出密码中的盐:

function bcryptVerify($rawPassword, $storedHash)
{ 
  return crypt($rawPassword, $storedHash) == $storedHash;
}
之后就可以使用此函数进行比对了:

if(!bcryptVerify($rawPassword, $hashedPassword)) {
   echo '用户名或密码错误。';
  exit;
}

echo '登录成功!';

3.忘记密码
需求描述:当用户忘记密码时可以输入自己的邮箱,系统会发送一封包含更改密码的链接的邮件,用户单击该链接后会进入密码修改页面。该模块的访问频率限制为1分钟10次以防止恶意用户通过此模块向某个邮箱地址大量发送垃圾邮件。

当用户在忘记密码的页面输入邮箱后,我们的程序需要做两件事。

(1)进行访问频率限制。这里使用4.2.3节介绍的方法以邮箱为标示符对发送修改密码邮件的过程进行访问频率限制。当用户提交了邮箱地址后首先验证邮箱地址是否正确,如果正确则检查访问频率是否超限:

$keyName = "rate.limiting:{$email}";
$now = time();

if($redis->llen($keyName) < 10) {
  $redis->lpush($keyName, $now);
} else {
  $time = $redis->lindex($keyName, -1);
  if($now - $time < 60) {
     echo '访问频率超过了限制,请稍后再试。';
    exit;
  } else {
    $redis->lpush($keyName, $now);
    $redis->ltrim($keyName, 0, 9);
  }
}

一般在全站中还会有针对IP地址的访问频率限制,原理与此类似。

(2)发送修改密码邮件。用户通过访问频率限制后我们会为其生成一个随机的验证码,并将验证码通过邮件发送给用户。同时在程序中要把用户的邮箱地址存入名为retrieve.password.code:散列后的验证码的字符串类型键中,然后使用EXPIRE命令为其设置一个生存时间(如1个小时)以提供安全性并且保证及时释放存储空间。由于忘记密码需要的安全等级与用户注册登录相同,所以我们依然使用Bcrypt算法来对验证码进行散列,具体的算法同上这里不再详述。

时间: 2024-08-04 00:47:19

《Redis入门指南》一5.1 PHP与Redis的相关文章

《Redis入门指南(第2版)》一导读

前 言 Redis入门指南(第2版)Redis如今已经成为Web开发社区中最火热的内存数据库之一,而它的诞生距现在不过才4年.随着Web 2.0的蓬勃发展,网站数据快速增长,对高性能读写的需求也越来越多,再加上半结构化的数据比重逐渐变大,人们对早已被铺天盖地地运用着的关系数据库能否适应现今的存储需求产生了疑问.而Redis的迅猛发展,为这个领域注入了全新的思维. Redis凭借其全面的功能得到越来越多的公司的青睐,从初创企业到新浪微博这样拥有着几百台Redis服务器的大公司,都能看到Redis的

《Redis入门指南》一4.3 排序

4.3 排序 Redis入门指南 午后,宋老师正在批改学生们提交的程序,再过几天就会迎来第一次计算机全市联考.他在每个学生的程序代码末尾都用注释详细地做了批注--严谨的治学态度让他备受学生们的爱戴. 一个电话打来."小白的?"宋老师拿出手机,"博客最近怎么样了?"未及小白开口,他就抢先问道. 特别好!现在平均每天都有50多人访问我的博客.不过昨天我收到一个访客的邮件,他向我反映了一个问题:查看一个标签下的文章列表时文章不是按照时间顺序排列的,找起来很麻烦.我看了一下

《Redis入门指南》一5.3 Python与Redis

5.3 Python与Redis Redis入门指南 Redis官方推荐的Python客户端是redis-py1. 5.3.1 安装 推荐使用pip install redis安装最新版本的redis-py,也可以使用easy_install:easy_install redis. 5.3.2 使用方法 首先需要引入redis-py: import redis 下面的代码将创建一个默认连接到地址127.0.0.1,端口6379的Redis连接: r = redis.StrictRedis() 也

《Redis入门指南》一4.1 事务

4.1 事务 Redis入门指南 傍晚时候,忙完了一天的教学工作,宋老师坐在办公室的电脑前开始为明天的课程做准备.尽管有着近5年的教学经验,可是宋老师依然习惯在备课时写一份简单的教案.正在网上查找资料时,在浏览器的历史记录里他突然看到了小白的博客.心想:不知道他的博客怎么样了? 于是宋老师点进了小白的博客,页面刚载入完他就被博客最下面的一行大得夸张的文字吸引了:"Powered by Redis".宋老师笑了笑,接着就看到了小白博客中最新的一篇文章: 标题: 使用Redis来存储微博中

《Redis入门指南》一4.4 消息通知

4.4 消息通知 Redis入门指南 凭着小白的用心经营,博客的访问量逐渐增多,甚至有了小白自己的粉丝.这不,小白刚收到一封来自粉丝的邮件,在邮件中那个粉丝强烈建议小白给博客加入邮件订阅功能,这样当小白发布新文章后订阅小白博客的用户就可以收到通知邮件了.在信的末尾,那个粉丝还着重强调了一下:"这个功能对不习惯使用RSS的用户很重要,希望能够加上!" 看过信后,小白心想:"是个好建议!不过话说回来,似乎他还没发现其实我的博客连RSS功能都没有." 邮件订阅功能太好实现

《Redis入门指南》一5.4 Node.js与Redis

5.4 Node.js与Redis Redis入门指南 Redis官方推荐的Node.js客户端是node_redis1. 5.4.1 安装 使用npm install redis命令安装最新版本的node_redis,目前版本是0.8.2. 5.4.2 使用方法 首先加载node_redis模块: var redis = require('redis'); 下面的代码将创建一个默认连接到地址127.0.0.1,端口6379的Redis连接: var client = redis.createC

《Redis入门指南》一4.5 管道

4.5 管道 Redis入门指南 客户端和Redis使用TCP协议连接.不论是客户端向Redis发送命令还是Redis向客户端返回命令的执行结果,都需要经过网络传输,这两个部分的总耗时称为往返时延.根据网络性能不同,往返时延也不同,大致来说到本地回环地址(loop back address)的往返时延在数量级上相当于Redis处理一条简单命令(如LPUSH list 1 2 3)的时间.如果执行较多的命令,每个命令的往返时延累加起来对性能还是有一定影响的. 在执行多个命令时每条命令都需要等待上一

《Redis入门指南》一第5章 实践

第5章 实践 Redis入门指南 小白把宋老师向自己讲解的知识总结成了一篇帖子发在了学校的网站上,引起了强烈的反响.很多同学希望宋老师能够再写一些关于Redis实践方面的教程,宋老师爽快地答应了. 在此之前我们进行的操作都是通过Redis的命令行客户端redis-cli进行的,并没有介绍实际编程时如何操作Redis.本章将会通过4个实例分别介绍Redis的PHP.Python.Ruby和 Node.js 客户端的使用方法,即使你不了解其中的某些语言,粗浅的阅读一下也能收获很多实践方面的技巧.

《Redis入门指南(第2版)》一第3章 入门

第3章 入门 Redis入门指南(第2版) 学会如何安装和运行Redis,并了解Redis的基础知识后,本章将详细介绍Redis的5种主要数据类型及相应的命令,带领读者真正进入Redis的世界.在学习的时候,手边打开一个redis-cli程序来跟着一起输入命令将会极大地提高学习效率.尽管在目前多数公司和团队的Redis的应用是以缓存和队列为主. 在之后的章节中你会遇到两个学习伙伴:小白和宋老师.小白是一个标准的极客,最近刚开始他的Redis学习之旅,而他大学时的计算机老师宋老师恰好对Redis颇