PHP 7: 真实世界的应用开发
  • 前言
  • 模块一
    • 第一章、建立基础
      • PHP 7 安装注意事项
      • 使用内置的 PHP web 服务器
      • 创建一个 MySQL 测试数据库
      • 安装 PHPUnit
      • 实现类的自动加载
      • 抓取一个网站
      • 建立一个深度网络扫描器
      • 创建一个 PHP 5 到 PHP 7 代码转换器
    • 第二章、使用 PHP 7 高性能特性
      • 了解抽象语法树
      • 理解句法分析中的差异
      • 理解 foreach() 处理中的差异
      • 使用 PHP 7 增强功能提高性能
      • 遍历海量文件
      • 将电子表格上传到数据库
      • 递归目录迭代器
    • 第三章、使用 PHP 函数
      • 函数开发
      • 数据类型提示
      • 使用返回值数据类型
      • 使用迭代器
      • 使用生成器编写自己的迭代器
    • 第四章、使用 PHP 面向对象程序设计
      • 类的开发
      • 类的扩展
      • 使用静态属性和方法
      • 使用命名空间
      • 定义可见性
      • 使用接口
      • 使用特性
      • 实现匿名类
    • 第五章、与数据库的交互
      • 使用PDO连接数据库
      • 构建一个 OOP SQL 查询生成器
      • 处理分页
      • 定义实体以匹配数据库表
      • 将实体类与RDBMS查询绑定
      • 将二次查找嵌入到查询结果中
      • 实现jQuery DataTables的PHP查找
    • 第六章、建立可扩展的网站
      • 创建通用表单元素生成器
      • 创建一个HTML单选元素生成器
      • 创建一个HTML选择元素生成器
      • 实现表单工厂
      • 链式 $_POST 过滤器
      • 链式 $_POST 验证器
      • 将验证绑定到表单
    • 第七章、访问Web服务
      • 在PHP和XML之间转换
      • 创建一个简单的REST客户端
      • 创建一个简单的REST服务器
      • 创建一个简单的SOAP客户端
      • 创建一个简单的SOAP服务器
    • 第八章、处理日期/时间和国际化方面
      • 在视图脚本中使用 emoji
      • 转换复杂字符
      • 从浏览器数据获取语言环境
      • 按地区设置数字格式
      • 按地区处理货币
      • 按地区设置日期/时间格式
      • 创建一个HTML国际日历生成器
      • 构建一个周期性事件生成器
      • 不使用gettext处理翻译
    • 第九章、开发中间件
      • 使用中间件进行认证
      • 使用中间件实现访问控制
      • 使用高速缓存提高性能
      • 实施路由选择
      • 进行框架间的系统调用
      • 使用中间件来跨语言
    • 第十章、高级算法
      • 使用 getter 和 setter
      • 实现一个链表
      • 建立冒泡排序
      • 实现一个堆栈
      • 构建一个二分法查找类
      • 实现一个搜索引擎
      • 显示多维数组并累计总数
    • 第十一章、软件设计模式的实现
      • 创建数组到对象的转化器
      • 构建对象到数组到转化器
      • 实施策略模式
      • 定义一个映射器
      • 实现对象关系映射
      • 实施发布/订阅设计模式
    • 第十二章、提高网站安全
      • 过滤$_POST数据
      • 验证$_POST数据
      • 保护PHP session
      • 用令牌保护表格的安全
      • 建立一个安全的密码生成器
      • 带有验证码的安全保护表格
      • 不使用mcrypt进行加密/解密
    • 第十三章、最佳实践、测试和调试
      • 使用特征和接口
      • 通用异常处理程序
      • 通用错误处理程序
      • 编写一个简单的测试
      • 编写测试套件
      • 生成虚假的测试数据
      • 使用session_start参数自定义会话
    • PSR-7
  • 模块二
  • 模块三
    • GoF 设计模式
      • 结构型
      • 行为型
      • 小结
    • SOLID 设计原则
      • 开闭原则
      • 里氏替换原则
      • 接口隔离原则
      • 依赖反转原则
      • 小结
    • 模块化网店应用的需求规范
      • 线框设计
      • 定义技术栈
      • 小结
    • Symfony 概述
      • 创建一个空白项目
      • 使用 Symfony 控制台
      • 控制器
      • 路由
      • 模板
      • 表单
      • 配置 Symfony
      • bundle 系统
      • 数据库和 Doctrine
      • 测试
      • 验证
      • 小结
    • 构建核心模块
    • 构建目录模块
    • 构建客户模块
    • 构建支付模块
    • 构建发货模块
    • 构建销售模块
    • 总结
由 GitBook 提供支持
在本页
  • 如何做...
  • 如何运行...
  • 更多...
  1. 模块一
  2. 第九章、开发中间件

使用中间件进行认证

中间件的一个非常重要的用途是提供认证。大多数基于网络的应用程序都需要通过用户名和密码来验证访问者的能力。通过将PSR-7标准整合到一个认证类中,你将使它在全局范围内通用,可以说,它足够安全,可以在任何提供符合PSR-7的请求和响应对象的框架中使用。

如何做...

1.我们首先定义一个Application\Acl\AuthenticateInterface类。我们使用这个接口来支持Adapter软件设计模式,使我们的Authenticate类更加通用,允许各种适配器,每个适配器都可以从不同的源头(例如,从一个文件,使用OAuth2,等等)获取认证。注意使用 PHP 7 的能力来定义返回值数据类型。

namespace Application\Acl;
use Psr\Http\Message\ { RequestInterface, ResponseInterface };
interface AuthenticateInterface
{
  public function login(RequestInterface $request) : 
    ResponseInterface;
}

请注意,通过定义一个需要符合PSR-7标准的请求并产生符合PSR-7标准的响应的方法,我们已经使这个接口普遍适用。

2. 接下来,我们定义实现接口所需的login()方法的适配器。我们确保使用适当的类,并定义合适的常量和属性。构造函数使用了Application\Database\Connection,它在第5章,与数据库的交互中定义。

namespace Application\Acl;
use PDO;
use Application\Database\Connection;
use Psr\Http\Message\ { RequestInterface, ResponseInterface };
use Application\MiddleWare\ { Response, TextStream };
class DbTable  implements AuthenticateInterface
{
  const ERROR_AUTH = 'ERROR: authentication error';
  protected $conn;
  protected $table;
  public function __construct(Connection $conn, $tableName)
  {
    $this->conn = $conn;
    $this->table = $tableName;
  }

3. 核心的login()方法从请求对象中提取用户名和密码。然后我们直接进行数据库查询。如果有匹配的信息,我们将用户信息存储在JSON编码的响应体中。

public function login(RequestInterface $request) : 
  ResponseInterface
{
  $code = 401;
  $info = FALSE;
  $body = new TextStream(self::ERROR_AUTH);
  $params = json_decode($request->getBody()->getContents());
  $response = new Response();
  $username = $params->username ?? FALSE;
  if ($username) {
      $sql = 'SELECT * FROM ' . $this->table 
        . ' WHERE email = ?';
      $stmt = $this->conn->pdo->prepare($sql);
      $stmt->execute([$username]);
      $row = $stmt->fetch(PDO::FETCH_ASSOC);
      if ($row) {
          if (password_verify($params->password, 
              $row['password'])) {
                unset($row['password']);
                $body = 
                new TextStream(json_encode($row));
                $response->withBody($body);
                $code = 202;
                $info = $row;
              }
            }
          }
          return $response->withBody($body)->withStatus($code);
        }
      }

最佳实践

永远不要用明文存储密码。当你需要进行密码匹配时,使用password_verify(),这样就可以否定重现密码哈希的必要性。

4. Authenticate类是一个实现AuthenticationInterface的适配器类的封装器。相应地,构造函数将一个适配器类作为参数,以及一个作为密钥的字符串,认证信息存储在$_SESSION中。

namespace Application\Acl;
use Application\MiddleWare\ { Response, TextStream };
use Psr\Http\Message\ { RequestInterface, ResponseInterface };
class Authenticate
{
  const ERROR_AUTH = 'ERROR: invalid token';
  const DEFAULT_KEY = 'auth';
  protected $adapter;
  protected $token;
  public function __construct(
  AuthenticateInterface $adapter, $key)
  {
    $this->key = $key;
    $this->adapter = $adapter;
  }

5. 此外,我们还提供了一个带有安全令牌的登录表单,这有助于防止跨站点请求伪造(CSRF)攻击。

public function getToken()
{
  $this->token = bin2hex(random_bytes(16));
  $_SESSION['token'] = $this->token;
  return $this->token;
}
public function matchToken($token)
{
  $sessToken = $_SESSION['token'] ?? date('Ymd');
  return ($token == $sessToken);
}
public function getLoginForm($action = NULL)
{
  $action = ($action) ? 'action="' . $action . '" ' : '';
  $output = '<form method="post" ' . $action . '>';
  $output .= '<table><tr><th>Username</th><td>';
  $output .= '<input type="text" name="username" /></td>';
  $output .= '</tr><tr><th>Password</th><td>';
  $output .= '<input type="password" name="password" />';
  $output .= '</td></tr><tr><th>&nbsp;</th>';
  $output .= '<td><input type="submit" /></td>';
  $output .= '</tr></table>';
  $output .= '<input type="hidden" name="token" value="';
  $output .= $this->getToken() . '" />';
  $output .= '</form>';
  return $output;
}

6. 最后,该类中的login()方法会检查token是否有效。如果无效,则返回一个400响应。否则,适配器的login()方法将被调用。

public function login(
RequestInterface $request) : ResponseInterface
{
  $params = json_decode($request->getBody()->getContents());
  $token = $params->token ?? FALSE;
  if (!($token && $this->matchToken($token))) {
      $code = 400;
      $body = new TextStream(self::ERROR_AUTH);
      $response = new Response($code, $body);
  } else {
      $response = $this->adapter->login($request);
  }
  if ($response->getStatusCode() >= 200
      && $response->getStatusCode() < 300) {
      $_SESSION[$this->key] = 
        json_decode($response->getBody()->getContents());
  } else {
      $_SESSION[$this->key] = NULL;
  }
  return $response;
}

}

如何运行...

首先,一定要按照附录《定义PSR-7类》中定义的事例。接下来,继续定义本配方中所介绍的类,总结如下表。

Class

在这些步骤中

Application\Acl\AuthenticateInterface

1

Application\Acl\DbTable

2 - 3

Application\Acl\Authenticate

4 - 6

然后你可以定义一个chap_09_middleware_authenticate.php调用程序,设置自动加载并使用相应的类。

<?php
session_start();
define('DB_CONFIG_FILE', __DIR__ . '/../config/db.config.php');
define('DB_TABLE', 'customer_09');
define('SESSION_KEY', 'auth');
require __DIR__ . '/../Application/Autoload/Loader.php';
Application\Autoload\Loader::init(__DIR__ . '/..');

use Application\Database\Connection;
use Application\Acl\ { DbTable, Authenticate };
use Application\MiddleWare\ { ServerRequest, Request, Constants, TextStream };

现在您可以设置认证适配器和核心类了。

$conn   = new Connection(include DB_CONFIG_FILE);
$dbAuth = new DbTable($conn, DB_TABLE);
$auth   = new Authenticate($dbAuth, SESSION_KEY);

一定要对传入的请求进行初始化,并将请求设置为认证类。

$incoming = new ServerRequest();
$incoming->initialize();
$outbound = new Request();

检查传入类方法是否为POST。如果是,则向认证类传递一个请求。

if ($incoming->getMethod() == Constants::METHOD_POST) {
  $body = new TextStream(json_encode(
  $incoming->getParsedBody()));
  $response = $auth->login($outbound->withBody($body));
}
$action = $incoming->getServerParams()['PHP_SELF'];
?>

显示逻辑是这样的。

<?= $auth->getLoginForm($action) ?>

以下是无效验证尝试的输出。注意右边的401状态码。在这个例子中,你可以添加一个响应对象的var_dump()。

这里是一个成功的认证。

更多...

有关如何避免CSRF和其他攻击的指导,请参见第12章《提高网站安全》。

上一页第九章、开发中间件下一页使用中间件实现访问控制

最后更新于4年前