七、安全

在开发应用程序时,我们应该始终考虑如何使我们的微服务更加安全。为了避免安全问题,每个开发人员都应该了解一些技术和方法。在本章中,您将了解在微服务中使用身份验证和授权的方法,以及用户登录后如何管理每个功能的权限。您还将发现可用于加密数据的不同方法。

微服务中的加密

我们可以将加密定义为以只有授权方才能读取的方式转换信息的过程。这个过程实际上可以在应用程序的任何级别上完成。例如,您可以对整个数据库进行加密,也可以使用 SSL/TSL 或JSON Web 令牌JWT在传输层添加加密。

如今,加密/解密过程是通过现代算法完成的,添加加密的最高级别是在传输层。该层中使用的所有算法至少提供以下功能:

  • 认证:此功能允许您验证消息的来源
  • 完整性:此功能可以证明消息的内容在从源站发送的途中没有更改

加密算法的最终任务是为您提供一个安全层,以便您可以交换或存储敏感数据,而不必担心有人窃取您的信息,但这并非免费的。您的环境将使用一些处理加密、解密或握手等相关内容的资源。

作为一名开发人员,您需要考虑您将被部署到一个敌对的环境中——生产是一个战区。如果你开始这样想,你可能会问自己以下问题:

  • 我们将部署到硬件还是虚拟化环境?我们会共享资源吗?
  • 我们能否信任应用程序的所有可能邻居?
  • 我们会将应用程序划分为不同的区域吗?我们将如何连接我们的区域?
  • 我们的应用程序是否符合 PCI 标准,或者由于我们存储/管理的数据,它是否需要非常高的安全性?

当您开始回答所有这些问题(以及其他问题)时,您将开始了解应用程序所需的安全级别。

在本节中,我们将向您展示加密应用程序中数据的最常用方法,以便您以后可以选择实现哪种方法。

请注意,我们不考虑使用全磁盘加密,因为它被认为是保护数据最薄弱的方法。

数据库加密

在处理敏感数据时,保护数据的最灵活且开销较低的方法是在应用程序层中使用加密。但是,如果由于某种原因,您无法更改应用程序,会发生什么情况?下一个最强大的解决方案是加密数据库。

对于我们的应用,我们选择了一个关系数据库;具体来说,我们使用的是 Percona,一个 MySQL 分支。目前,您有两个不同的选项来加密此数据库中的数据:

  • 通过 MariaDB 补丁(另一种类似于 Percona 的 MySQL 表单)启用加密。此修补程序在 10.1.3 及更高版本中提供。
  • InnoDB 表空间级加密方法可从 Percona Server 5.7.11 或 MySQL 5.7.11 获得。

也许你想知道,当我们选择 Percona 时,为什么我们要谈论 MariaDB 和 MySQL。这是因为它们三个拥有相同的核心,共享它们的大部分核心功能。

提示

所有主要的数据库软件都允许您加密数据。如果您没有使用 Percona,请查看数据库的官方文档,以找到允许加密所需的步骤。

作为开发人员,您需要知道在应用程序中使用数据库级加密的缺点。除其他外,我们可以强调以下几点:

  • 特权数据库用户有权访问密匙环文件,因此请严格控制数据库中的用户权限。
  • 数据存储在服务器的 RAM 中时不加密,仅当数据写入硬盘驱动器时才加密。特权和恶意用户可以使用某些工具读取服务器内存,因此也会读取应用程序数据。
  • 一些工具(如 GDB)可用于更改根用户密码结构,允许您复制数据而不会出现任何问题。

MariaDB 中的加密

想象一下,你不想使用 Percona,而是想使用 MariaDB;由于file_key_management插件,数据库加密可用。在我们的应用程序示例中,我们使用 Percona 作为 secrets microservice 的数据存储,因此让我们为 MariaDB 添加一个新容器,以便您稍后可以尝试并交换两个 RDBMS。

首先,在 secrets microservice 中的 Docker 存储库中创建一个与数据库文件夹相同级别的mariadb文件夹。在这里,您可以添加一个Dockerfile,内容如下:

FROM mariadb:latest

RUN apt-get update \
&& apt-get autoremove && apt-get autoclean \
&& rm -rf /var/lib/apt/lists/*

RUN mkdir -p /volumes/keys/
RUN echo 
"1;
C472621BA1708682BEDC9816D677A4FDC51456B78659F183555A9A895EAC9218" > 
/volumes/keys/keys.txt
RUN openssl enc -aes-256-cbc -md sha1 -k secret -in 
/volumes/keys/keys.txt -out /volumes/keys/keys.enc
COPY etc/ /etc/mysql/conf.d/

在前面的代码中,我们正在提取最新的官方 MariaDB 映像,更新它,并创建加密所需的一些证书。keys.txt文件中保存的长字符串是我们自己用以下命令生成的密钥:

openssl enc -aes-256-ctr -k secret@phpmicroservices.com -P -md sha1

Dockerfile的最后一个命令将复制容器内的定制数据库配置。使用以下内容在etc/encryption.cnf中创建我们的自定义数据库配置:

    [mysqld]
    plugin-load-add=file_key_management.so
    file_key_management_filekey = FILE:/mount/keys/server-key.pem
    file-key-management-filename = /mount/keys/mysql.enc
    innodb-encrypt-tables = ON
    innodb-encrypt-log = 1
    innodb-encryption-threads=1
    encrypt-tmp-disk-tables=1
    encrypt-tmp-files=0
    encrypt-binlog=1
    file_key_management_encryption_algorithm = AES_CTR

在前面的代码中,我们告诉数据库引擎我们将证书存储在哪里,并启用加密。现在,您可以编辑我们的docker-compose.yml文件并添加以下容器定义:

    microservice_secret_database_mariadb:
      build: ./microservices/secret/mariadb/
      environment:
        - MYSQL_ROOT_PASSWORD=mysecret
        - MYSQL_DATABASE=finding_secrets
        - MYSQL_USER=secret
        - MYSQL_PASSWORD=mysecret
      ports:
        - 7777:3306

从前面的代码中可以看出,我们没有定义任何新的内容;您现在可能有足够的 Docker 经验来理解我们正在定义Dockerfile的位置。我们设置了一些环境变量,并将7777本地端口映射到集装箱3306端口。完成所有更改后,一个简单的docker-compose build microservice_secret_database命令将生成新容器。

建造完容器后,是时候检查所有东西是否正常工作了。使用docker-compose up microservice_secret_database旋转新容器,并尝试将其连接到本地7777端口。现在,您可以开始在此容器中使用加密。考虑下面的例子:

    CREATE TABLE `test_encryption` (
      `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
      `text_field` varchar(255) NOT NULL,
      PRIMARY KEY (`id`)
    ) ENGINE=InnoDB `ENCRYPTED`=YES `ENCRYPTION_KEY_ID`=1;

在前面的代码中,我们向 SQL 添加了一些额外的标记;他们在表中启用加密,并使用 ID 为1的加密密钥存储在keys.txt(我们用来启动数据库的文件)中。试一试,如果一切顺利,可以随意进行必要的更改,以使用这个新的数据库容器,而不是我们项目中的另一个数据库容器。

InnoDB 加密

Percona 和 MySQL 5.7.11+版本提供了一个开箱即用的新功能——支持InnoDB表空间级加密。使用此功能,您可以加密所有 InnoDB 表,而无需太多麻烦或配置。在我们的示例应用程序中,我们在 secrets microservice 上使用 Percona 5.7。让我们看看如何加密表。

首先,我们需要对 Docker 环境进行一些小的修改;首先,打开microservices/secret/database/Dockerfile并用以下代码行替换所有内容:

    FROM percona:5.7
 RUN mkdir -p /mount/mysql-keyring/ \
 && touch /mount/mysql-keyring/keyring \
 && chown -R mysql:mysql /mount/mysql-keyring
COPY etc/ /etc/mysql/conf.d/

在本书的这一点上,您可能不需要解释我们在Dockerfile中所做的事情,因此让我们创建一个新的config文件,稍后将其复制到容器中。在secret microservice文件夹中,创建一个etc文件夹并生成一个新的encryption.cnf文件,该文件包含以下内容:

    [mysqld]
    early-plugin-load=keyring_file.so
    keyring_file_data=/mount/mysql-keyring/keyring

在前面创建的配置文件中,我们正在加载keyring库,我们的数据库可以在其中找到并存储生成的用于加密数据的密钥环。

此时,您已经具备了启用加密所需的一切,因此使用docker-compose build microservice_secret_database重新构建容器,并使用docker-compose up -d再次旋转所有容器。

如果一切正常,您应该能够毫无问题地打开数据库,并且可以使用以下 SQL 命令更改存储的表:

ALTER TABLE `secrets` ENCRYPTION='Y'

您可能想知道,如果我们已经在数据库中启用了加密,为什么要修改我们的secrets表。这背后的原因是,加密在默认情况下没有启用,因此需要显式地告诉引擎要加密哪些表。

性能开销

在数据库中使用加密将降低应用程序的性能。您的计算机/容器将使用一些资源来处理加密/解密过程。在一些测试中,当您不使用表空间级别的加密(MySQL/Percona+5.7)时,此开销可能超过 20%。我们的建议是在启用和不启用加密的情况下测量应用程序的平均性能。通过这种方式,您可以确保加密不会对应用程序产生很大影响。

在本节中,我们向您展示了向应用程序添加额外安全层的两种快速方法。使用这些功能的最终决定取决于您和应用程序的规格。

TSL/SSL 协议

传输层安全TSL)和安全套接字层SSL)是用于保护不受信任网络(例如,ISP 的 Internet 或 LAN)中通信安全的加密协议。SSL 是 TSL 的前身,两者经常互换使用或与 TLS/SSL 结合使用。现在,SSL 和 TSL 实际上是同一件事,如果您选择使用其中一种,也没有什么区别,您将使用由服务器指定的相同级别的加密。如果应用程序(例如电子邮件客户端)允许您在 SSL 或 TSL 之间进行选择,则您只需选择安全连接的启动方式,而无需选择其他方式。

这些协议的所有功能和安全性都依赖于我们所知的证书。TSL/SSL 证书可以定义为将加密密钥数字绑定到组织或个人详细信息的小型数据文件。您可以找到各种销售 TSL/SSL 证书的公司,但是如果您不想花钱(或者您正处于开发阶段),您可以创建自签名证书。这些类型的证书可用于加密数据,但除非跳过验证,否则客户端不会信任它们。

TSL/SSL 协议的工作原理

在应用程序中开始使用 TSL/SSL 之前,您需要了解它是如何工作的。还有很多其他的书专门解释这些协议是如何工作的,所以我们只给你一个粗略的了解。

下图总结了 TSL/SSL 协议的工作原理;首先,您需要知道 TSL/SSL 是一种 TCP 客户机-服务器协议,加密在几个步骤后开始:

How the TSL/SSL protocol works

TSL/SSL 协议

以下是 TSL/SSL 协议的步骤:

  1. 我们的客户机希望启动到由 TSL/SSL 保护的服务器/服务的连接,因此它要求服务器标识自己。
  2. 服务器处理请求并向客户端发送其 TSL/SSL 证书的副本。
  3. 客户端检查 TSL/SSL 证书是否为受信任证书,如果是,则向服务器发送消息。
  4. 服务器返回数字签名确认以启动会话。
  5. 在所有前面的步骤(握手)之后,加密数据在客户端和服务器之间共享。

可以想象,客户机服务器这两个术语是不明确的;客户端可以是试图访问您的页面的浏览器,也可以是试图与其他微服务通信的微服务。

TSL/SSL 终端

如前所述,向应用程序添加 TSL/SSL 层会增加应用程序总体性能的一点开销。为了缓解这个问题,我们使用了我们称之为 TSL/SSL 终止的方法,这是 TSL/SSL 卸载的一种形式,它将加密/解密的责任从服务器转移到应用程序的不同部分。

TSL/SSL 终止依赖于这样一个事实,即一旦所有数据被解密,您就可以信任用于移动此解密数据的所有通信通道。让我们看一个微服务的例子;请看下图:

TSL/SSL termination

微服务中的 TSL/SSL 终端

在上图中,所有输入/输出通信都使用我们的微服务体系结构的特定组件进行加密。该组件将充当代理,它将处理所有 TSL/SSL 内容。一旦客户机发出请求,它就会管理所有握手并解密请求。一旦请求被解密,它将被代理到特定的微服务组件(在我们的例子中,它是 NGINX),我们的微服务执行所需的操作,例如,从数据库获取一些数据。一旦微服务需要返回响应,它就会使用代理,在那里我们的所有响应都被加密。如果您有多个微服务,您可以扩展这个小示例并执行相同的操作——加密不同微服务之间的所有通信,并在微服务内使用加密数据。

使用 NGINX 的 TSL/SSL

您可以找到多个可用于进行 TSL/SSL 终止的软件。除其他外,以下列表显示了最著名的:

  • 负载均衡器:亚马逊 ELB 和 HaProxy
  • 代理:NGINX、Traefik 和 Fabio

在我们的例子中,我们将使用 NGINX 来管理所有的 TSL/SSL 终端,但是可以尝试其他选项。

您可能已经知道,NGINX 是市场上最通用的软件之一。您可以将其用作具有高性能级别和稳定性的反向代理或 web 服务器。

我们将解释如何在 NGINX 中进行 TSL/SSL 终止,例如,用于 battle microservice。首先,打开microservices/battle/nginx/Dockerfile文件,在 CMD 命令之前添加以下命令:

RUN echo 01 > ca.srl \
&& openssl genrsa -out ca-key.pem 2048 \
&& openssl req -new -x509 -days 365 -subj "/CN=*" -key ca-key.pem -out ca.pem \
&& openssl genrsa -out server-key.pem 2048 \
&& openssl req -subj "/CN=*" -new -key server-key.pem -out server.csr \
&& openssl x509 -req -days 365 -in server.csr -CA ca.pem -CAkey ca-key.pem -out server-cert.pem \
&& openssl rsa -in server-key.pem -out server-key.pem \
&& cp *.pem /etc/nginx/ \
&& cp *.csr /etc/nginx/

在这里,我们创建了一些自签名证书,并将它们存储在nginx容器的/etc/nginx文件夹中。

一旦我们有了证书,就可以更改 NGINX 配置文件了。打开microservices/battle/nginx/config/nginx/nginx.conf.ctmpl文件,添加以下服务器定义:

    server {
      listen 443 ssl;
      server_name _;
      root /var/www/html/public;
      index index.php index.html;
      ssl on;
      ssl_certificate /etc/nginx/server-cert.pem;
      ssl_certificate_key /etc/nginx/server-key.pem;
      location = /favicon.ico { access_log off; log_not_found off; }
      location = /robots.txt { access_log off; log_not_found off; }
      access_log /var/log/nginx/access.log;
      error_log /var/log/nginx/error.log error;
      sendfile off;
      client_max_body_size 100m;
      location / {
        try_files $uri $uri/ /index.php?_url=$uri&$args;
      }
      location ~ /\.ht {
        deny all;
      }
      {{ if service $backend }}
      location ~ \.php$ {
        try_files $uri =404;
        fastcgi_split_path_info ^(.+\.php)(/.+)$;
        fastcgi_pass {{ $backend }};
        fastcgi_index /index.php;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME 
        $document_root$fastcgi_script_name;
        fastcgi_intercept_errors off;
        fastcgi_buffer_size 16k;
        fastcgi_buffers 4 16k;
      }
      {{ end }}
    }

前面的代码在nginx服务器的443端口中设置了一个新的侦听器。如您所见,它与默认服务器设置非常相似;区别在于端口和我们在上一步中创建的证书的位置。

要使用这个新的 TSL/SSL 端点,我们需要对docker-compose.yml文件进行一些小的更改,并映射443NGINX 端口。为此,您只需转到microservice_battle_nginx定义并在端口声明中添加新行,如下所示:

    - 8443:443

新线路将把我们的8443端口映射到nginx集装箱443端口,允许我们通过 TSL/SSL 进行连接。您可以立即使用 Postman 进行尝试,但由于它是一个自签名证书,因此默认情况下不接受它。打开首选项并禁用SSL 证书验证。作为家庭作业,您可以将我们的所有示例服务更改为仅使用 TSL/SSL 层彼此通信。

在本章的这一节中,我们向您展示了为应用程序添加额外安全层的主要方法,即加密数据和用于交换消息的通信通道。既然我们已经确定应用程序至少具有某种级别的加密,那么让我们继续讨论任何应用程序的另一个重要方面——身份验证。

认证

每个项目的起点都是身份验证系统,在该系统中,可以识别将使用我们的应用程序或 API 的用户或客户。有许多库可以实现不同的用户身份验证方法;在本书中,我们将看到两种最重要的方式:OAuth 2JWT

正如我们已经知道的,微服务是无状态,这意味着它们应该使用访问令牌而不是 cookie 和会话与彼此和用户进行通信。那么,让我们看看使用它的身份验证工作流是什么样的:

Authentication

令牌认证工作流

如上图所示,这应该是获取客户或用户所需机密列表的过程:

  1. 用户要求前端登录获取机密列表。
  2. 前端登录询问后端机密列表。
  3. 后端询问前端登录用户访问令牌。
  4. 前端登录谷歌(或任何其他提供商)索要访问令牌。
  5. 谷歌用户索要其凭证。
  6. 用户谷歌提供凭证。
  7. 谷歌前端登录提供用户访问令牌。
  8. 前端登录提供后端用户访问令牌。
  9. 后端谷歌确认该接入令牌的用户。
  10. 谷歌告知后端用户是谁。
  11. 后端检查用户并告知前端登录机密列表。
  12. 前端登录显示用户机密列表。

显然,在这个过程中,一切都是在用户不知情的情况下发生的。用户只需向适当的服务提供其凭据。在前面的示例中,服务是GOOGLE,但它甚至可以是我们自己的应用程序。

现在,我们将构建一个新的 docker 容器,以便创建和设置一个数据库,使用 OAuth 2 和 JWT 对用户进行身份验证。

docker/microservices/user/database/Dockerfile数据库文件夹下的 docker user microservice 中创建一个Dockerfile,如下所示。我们将使用 Percona,就像我们为秘密微服务所做的那样:

    FROM percona:5.7

创建了Dockerfile,之后,打开docker-composer.yml文件,并在用户微服务部分的末尾(就在源容器之前)添加用户数据库微服务配置。另外,将microservice_user_database添加到microservice_user_fpm链接部分以使数据库可见:

    microservice_user_fpm:
    {{omitted code}}
    links:
    {{omitted code}}
 - microservice_user_database
 microservice_user_database:
 build: ./microservices/user/database/
 environment:
 - CONSUL=autodiscovery
 - MYSQL_ROOT_PASSWORD=mysecret
 - MYSQL_DATABASE=finding_users
 - MYSQL_USER=secret
 - MYSQL_PASSWORD=mysecret
 ports:
 - 6667:3306

一旦我们设置了配置,就可以构建它了,因此在您的终端上运行以下命令来创建我们刚刚设置的新容器:

docker-compose build microservice_user_database

这可能需要一些时间;完成后,我们必须通过运行以下命令再次启动容器:

docker-compose up -d

您可以通过执行docker ps来检查用户数据库微服务是否正确创建,因此可以在其上查看新的microservice_user_database

现在是设置用户微服务以使用我们刚刚创建的数据库容器的时候了,因此在bootstrap/app.php文件中添加以下行:

    $app->configure('database');

另外,使用以下配置创建config/database.php文件:

    <?php
      return [
        'default'     => 'mysql',
        'migrations'  => 'migrations',
        'fetch'       => PDO::FETCH_CLASS,
        'connections' => [
          'mysql' => [
            'driver'    => 'mysql',
            'host'      => env('DB_HOST','microservice_user_database'),
            'database'  => env('DB_DATABASE','finding_users'),
            'username'  => env('DB_USERNAME','secret'),
            'password'  => env('DB_PASSWORD','mysecret'),
            'collation' => 'utf8_unicode_ci',
          ]
        ]
      ];

注意,在前面的代码中,为了连接到数据库容器,我们使用了在docker-compose.yml文件上使用的相同凭据。

这就是一切。我们现在有一个新的数据库容器连接到用户 microservice,可以使用了。通过在您喜爱的 SQL 客户端中创建迁移或执行以下查询,添加新的 users 表:

    CREATE TABLE `users` (
      `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
      `email` varchar(255) NOT NULL,
      `password` varchar(255) NOT NULL,
      `api_token` varchar(255) DEFAULT NULL,
      PRIMARY KEY (`id`)
    ) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=latin1;

OAuth 2

让我们介绍一个基于访问令牌的安全且特别有用的微服务身份验证系统。

OAuth2 是一个标准协议,允许我们将 API REST 的某些方法限制为特定用户,从而避免向用户询问用户名和密码。

此协议非常常见,因为它更安全,因为它避免了在 API 之间通信时共享密码或微妙的凭据。

OAuth 2 使用用户需要获得的访问令牌来使用应用程序。令牌将有一个过期时间,可以刷新它,而无需再次提供用户凭据。

如何在管腔上使用 OAuth 2

现在,我们将解释如何在 Lumen 上安装、设置和尝试 OAuth2 身份验证。其目标是为您的方法提供一个使用 OAuth2 的微服务;换句话说,消费者需要在使用需要身份验证的方法之前提供令牌。

OAuth 2 安装

通过在 docker 文件夹上执行以下命令,转到用户微服务:

docker-compose up -d
docker exec -it  docker_microservice_user_fpm_1 /bin/bash

一旦进入用户微服务,就需要通过在require部分的composer.json文件中添加以下行来安装 OAuth 2:

    "lucadegasperi/oauth2-server-laravel": "^5.0"

然后,执行composer update,软件包将在您的微服务上安装 OAuth 2。

设置

一旦安装了这个包,我们就必须设置一些重要的东西来运行 OAuth 2。首先,我们需要将位于/vendor/lucadegasperi/oauth2-server-laravel/config/oauth2.php的 OAuth 2 配置文件复制到/config/oauth2.php;如果config文件夹不存在,请创建它。另外,我们需要复制包含在/vendor/lucadegasperi/oauth2-server-laravel/database/migrations to /database/migrations文件夹中的迁移文件。

不要忘记通过在/bootstrap/app.php中添加以下行来注册 OAuth 2:

    $app-
    >register(\LucaDegasperi\OAuth2Server\Storage\
    FluentStorageServiceProvider::class);
    $app-    >register(\LucaDegasperi\OAuth2Server\
    OAuth2ServerServiceProvider::class);
    $app->middleware([
      \LucaDegasperi\OAuth2Server\Middleware\
      OAuthExceptionHandlerMiddleware::class
    ]);

在文件顶部的app->withFacades();行之前(如果未注释,请执行),添加以下行:

    class_alias('Illuminate\Support\Facades\Config', 'Config');
    class_alias(\LucaDegasperi\OAuth2Server\Facades\Authorizer::class, 
    'Authorizer');

现在,我们将执行迁移,以便在数据库中创建必要的表:

composer dumpautoload
php artisan migrate

提示

如果在执行迁移时遇到问题,请尝试将'migrations' => 'migrations', 'fetch' => PDO::FETCH_CLASS,行添加到config/database.php文件中,然后执行php artisan migrate:install --database=mysql

一旦我们创建了所有必要的表,使用 Lumen seeders 或通过在您喜爱的 SQL 客户端上执行以下查询,在oauth_clients表中插入一个寄存器:

    INSERT INTO `finding_users`.`oauth_clients`
    (`id`, `secret`, `name`, `created_at`, `updated_at`)
    VALUES
    ('1', 'YouAreTheBestDeveloper007', 'PHPMICROSERVICES', NULL, NULL);

现在,我们必须在/app/Http/routes.php上添加一个新路由,以便为我们刚刚创建的用户获取有效令牌。例如,路由可以是oauth/access_token

    $app->post('oauth/access_token', function() {
      return response()->json(Authorizer::issueAccessToken());
    });

最后,修改/config/oauth2.php文件上的grant_types值,将其更改为以下代码行:

    'grant_types' => [
      'client_credentials' => [
        'class'            => '\League\OAuth2\Server\Grant\
        ClientCredentialsGrant',
        'access_token_ttl' => 0
      ]
    ],

让我们试试 OAuth2

我们现在已经准备好通过向http://localhost:8084/api/v1/oauth/access_token投递邮递员进行投递呼叫来获取我们的代币,包括身体上的以下参数:

 grant_type: client_credentials
 client_id: 1
 client_secret: YouAreTheBestDeveloper007

如果我们输入了错误的凭据,它将给出以下响应:

    {
      "error": "invalid_client",
      "error_description": "Client authentication failed."
    }

如果凭证正确,我们将获得 JSON 格式的access_token

    {
      "access_token": "anU2e6xgXiLm7UARSSV7M4Wa7u86k4JryKWrIQhu",
      "token_type": "Bearer",
      "expires_in": 3600
    }

一旦我们有了一个有效的访问令牌,我们就可以为未注册的用户限制一些方法。这是非常容易对流明。我们只需在/bootstrap/app.php上启用 route Middleware,因此在该文件中添加以下代码:

    $app->routeMiddleware(
      [
        'check-authorization-params' => 
        \LucaDegasperi\OAuth2Server\Middleware\
        CheckAuthCodeRequestMiddleware::class,
        'csrf' => \Laravel\Lumen\Http\Middleware\
        VerifyCsrfToken::class,
        'oauth' => 
        \LucaDegasperi\OAuth2Server\Middleware\
        OAuthMiddleware::class,
        'oauth-client' => \LucaDegasperi\OAuth2Server\Middleware\
        OAuthClientOwnerMiddleware::class,
        'oauth-user' => \LucaDegasperi\OAuth2Server\Middleware\
        OAuthUserOwnerMiddleware::class,
      ]
    );

进入控制器UserController.php文件,添加__construct()功能,代码如下:

    public function __construct(){
      $this->middleware('oauth');
    }

这将影响控制器上的所有方法,但我们可以使用以下代码排除其中一些方法:

    public function __construct(){
      $this->middleware('oauth', ['except' => 'index']);
    }
    public function index()
    {
      return response()->json(['method' => 'index']);
    }

现在我们可以通过对http://localhost:8084/api/v1/user进行 GET 调用来测试索引函数。不要忘记包含一个名为Authorization的标题和Bearer anU2e6xgXiLm7UARSSV7M4Wa7u86k4JryKWrIQhu值。

如果我们排除了索引函数,或者如果我们正确输入了令牌,我们将得到状态代码为 200 的 JSON 响应:

    {"method":"index"}

如果我们没有排除 index 方法,并且输入了错误的令牌,我们将得到错误代码 401 和以下消息:

    {"error":"access_denied","error_description":"The resource owner or 
    authorization server denied the request."}

现在您有了一个安全且更好的应用程序。请记住,您可以将上一章中学习的错误处理添加到授权方法中。

JSON Web 令牌

JSON Web TokenJWT)是 HTTP 请求中使用的一组安全方法,在客户端和服务器之间传输。JWT 令牌是使用 JSON web 签名进行数字签名的 JSON 对象。

为了使用 JWT 创建令牌,我们需要用户凭据、密钥和要使用的加密类型;它可以是 HS256、HS384 或 HS512。

如何在管腔上使用 JWT

可以使用 composer 在内腔上安装 JWT。因此,一旦进入用户微服务容器,请在终端中执行以下命令:

composer require tymon/jwt-auth:"^1.0@dev"

另一种安装库的方法是打开您的composer.json文件并将"tymon/jwt-auth": "^1.0@dev"添加到所需库的列表中。安装之后,我们需要在注册服务提供商上注册 JWT,就像我们在 OAuth2 上所做的那样。在 Lumen 上,可以通过在bootstrap/app.php文件中添加以下行来完成此操作:

    $app->register('Tymon\JWTAuth\Providers\JWTAuthServiceProvider');

另外,取消对以下行的注释:

    $app->register(App\Providers\AuthServiceProvider::class);

您的bootstrap/app.php文件应如下所示:

    <?php
      require_once __DIR__.'/../vendor/autoload.php';
      try {
        (new Dotenv\Dotenv(__DIR__.'/../'))->load();
      } catch (Dotenv\Exception\InvalidPathException $e) {
        //
      }
      $app = new Laravel\Lumen\Application(
        realpath(__DIR__.'/../')
      );
      // $app->withFacades();
 $app->withEloquent();
      $app->singleton(
        Illuminate\Contracts\Debug\ExceptionHandler::class,
        App\Exceptions\Handler::class
      );
      $app->singleton(
        Illuminate\Contracts\Console\Kernel::class,
        App\Console\Kernel::class
      );
 $app->routeMiddleware([
 'auth' => App\Http\Middleware\Authenticate::class,
 ]);
      $app->register(App\Providers\AuthServiceProvider::class);
 $app->register
      (Tymon\JWTAuth\Providers\LumenServiceProvider::class);
      $app->group(['namespace' => 'App\Http\Controllers'], 
      function ($app) 
      {
           require __DIR__.'/../app/Http/routes.php';
      });
      return $app;

建立 JWT

现在我们需要一个密钥,因此运行以下命令以生成密钥并将其放置在 JWT 配置文件中:

php artisan jwt:secret

生成后,您可以看到放置在.env文件中的密钥(您的密钥将不同)。检查此项并确保您的.env看起来如图所示:

    APP_DEBUG=true
    APP_ENV=local
    SESSION_DRIVER=file
    DB_HOST=microservice_user_database
    DB_DATABASE=finding_users
    DB_USERNAME=secret
    DB_PASSWORD=mysecret
 JWT_SECRET=wPB1mQ6ADZrc0ouxMCYJfiBbMC14IAV0
    CACHE_DRIVER=file

现在,转到config/jwt.php文件;这是 JWTconfig文件,请确保您的文件如下:

    <?php
      return [
        'secret' => env('JWT_SECRET'),
        'keys' => [
          'public' => env('JWT_PUBLIC_KEY'),
          'private' => env('JWT_PRIVATE_KEY'),
          'passphrase' => env('JWT_PASSPHRASE'),
        ],
        'ttl' => env('JWT_TTL', 60),
        'refresh_ttl' => env('JWT_REFRESH_TTL', 20160),
        'algo' => env('JWT_ALGO', 'HS256'),
        'required_claims' => ['iss', 'iat', 'exp', 'nbf', 'sub', 
        'jti'],
        'blacklist_enabled' => env('JWT_BLACKLIST_ENABLED', true),
        'blacklist_grace_period' => env('JWT_BLACKLIST_GRACE_PERIOD', 
        0),
        'providers' => [
          'jwt' => Tymon\JWTAuth\Providers\JWT\Namshi::class,
          'auth' => Tymon\JWTAuth\Providers\Auth\Illuminate::class,
          'storage' => 
          Tymon\JWTAuth\Providers\Storage\Illuminate::class,
        ],
      ];

正确设置config/app.php也是必要的。确保您正确输入了用户模型,它将定义 JWT 应在其中搜索提供的用户和密码的表:

    <?php
      return [
        'defaults' => [
          'guard' => env('AUTH_GUARD', 'api'),
          'passwords' => 'users',
        ],
        'guards' => [
          'api' => [
            'driver' => 'jwt',
            'provider' => 'users',
          ],
        ],
        'providers' => [
          'users' => [
            'driver' => 'eloquent',
 'model' => \App\Model\User::class,
          ],
        ],
        'passwords' => [
          'users' => [
            'provider' => 'users',
            'table' => 'password_resets',
            'expire' => 60,
          ],
        ],
      ];

现在我们准备通过编辑/app/Http/routes.php来定义需要认证的方法:

    <?php
      $app->get('/', function () use ($app) {
        return $app->version();
      });
      use Illuminate\Http\Request;
      use Tymon\JWTAuth\JWTAuth;
      $app->post('login', function(Request $request, JWTAuth $jwt) {
        $this->validate($request, [
          'email' => 'required|email|exists:users',
          'password' => 'required|string'
        ]);
        if (! $token = $jwt->attempt($request->only(['email', 
        'password']))) {
          return response()->json(['user_not_found'], 404);
        }
        return response()->json(compact('token'));
      });
      $app->group(['middleware' => 'auth'], function () use ($app) {
        $app->post('user', function (JWTAuth $jwt) {
          $user = $jwt->parseToken()->toUser();
          return $user;
        });
      });

您可以在前面的代码中看到,我们的中间件只影响我们在其中定义中间件的组中包含的方法。我们可以创建我们想要的所有组,以便通过我们选择的中间件传递方法。

最后,编辑/app/Providers/AuthServiceProvider.php文件并添加以下突出显示的代码:

    <?php
      namespace App\Providers;
      use App\User;
      use Illuminate\Support\ServiceProvider;
      class AuthServiceProvider extends ServiceProvider
      {
        public function register()
        {
          //
        }
        public function boot()
        {
 $this->app['auth']->viaRequest('api', function ($request) {
 if ($request->input('email')) {
 return User::where('email', $request->input('email'))-
              >first();
 }
 });
        }
      }

最后,我们需要对您的用户模型文件进行一些更改,因此转到/app/Model/User.php并将JWTSubject的以下行添加到类实现列表中:

    <?php
      namespace App\Model;
      use Illuminate\Contracts\Auth\Access\Authorizable as 
      AuthorizableContract;
      use Illuminate\Database\Eloquent\Model;
      use Illuminate\Auth\Authenticatable;
      use Laravel\Lumen\Auth\Authorizable;
      use Illuminate\Contracts\Auth\Authenticatable as 
      AuthenticatableContract;
 use Tymon\JWTAuth\Contracts\JWTSubject;
      class User extends Model implements JWTSubject, 
      AuthorizableContract, 
      AuthenticatableContract {
        use Authenticatable, Authorizable;
        protected $table = 'users';
        protected $fillable = ['email', 'api_token'];
        protected $hidden = ['password'];
        public function getJWTIdentifier()
 {
 return $this->getKey();
 }
 public function getJWTCustomClaims()
 {
 return [];
 }
      }

不要忘记添加getJWTIdentifier()getJWTCustomClaims()函数,正如您在前面的代码中看到的那样。这些功能是实现JWTSubject所必需的。

让我们试试 JWT

为了测试这一点,我们必须在数据库的 users 表中创建一个新用户。因此,通过迁移或在您喜爱的 SQL 客户端中执行以下查询来添加它:

    INSERT INTO `finding_users`.`users`
    (`id`, `email`, `password`, `api_token`)
    VALUES
    (1,'john@phpmicroservices.com',
    '$2y$10$m5339OpNKEh5bL6Erbu9r..sjhaf2jDAT2nYueUqxnsR752g9xEFy',
    NULL,);

手动插入的哈希密码对应于“123456”。出于安全原因,Lumen 将保存散列的用户密码。

打开“邮递员”并通过拨打http://localhost:8084/user的邮寄电话进行尝试。您应该会收到以下回复:

    Unauthorized.

发生这种情况是因为http://localhost:8084/user方法受到身份验证中间件的保护。您可以在您的routes.php文件中查看此信息。为了获得用户,必须提供有效的访问令牌。

获取有效访问令牌的方法是http://localhost:8084/login,因此使用我们添加的用户email = john@phpmicroservices.com和密码123456对应的参数进行 POST 调用。如果正确,我们将获得有效的访问令牌:

    {"token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.
    eyJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjgwODQvbG9naW4iLCJ
    pYXQiOjE0ODA4ODI4NTMsImV4cCI6MTQ4MDg4NjQ1MywibmJmIjox
    NDgwODgyODUzLCJqdGkiOiJVVnRpTExZTFRWcEtyWnhsIiwic3
    ViIjoxfQ.jjgZO_Lf4dlfwYiOYAOhzvcTQ4EGxJUTgRSPyMXJ1wg"}

现在,我们可以像以前一样使用前面的访问令牌对http://localhost:8084/user进行 POST 调用。这一次,我们将获得用户信息:

    {"id":1,"email":"john@phpmicroservices.com","api_token":null}

如您所见,使用有效的访问令牌保护您的方法非常简单。它将使您的应用程序更加安全。

访问控制列表

这是所有应用程序中非常常见的系统,无论其大小。访问控制列表ACL)为我们提供了一种简单的方式来管理和过滤每个用户的权限。让我们更详细地看看这个。

什么是 ACL?

应用程序用于标识应用程序的每个用户的方法是 ACL。这是一个系统,用于通知应用程序用户或用户组对特定任务或操作具有哪些访问权限。

每个任务(函数或操作)都有一个属性来标识哪些用户可以使用它,ACL 是一个列表,它将每个任务与每个操作(如读、写或执行)链接起来。

对于使用 ACL 的应用程序,ACL 具有以下两个特点优势:

  • 管理:在我们的应用程序中使用 ACL 可以将用户添加到组中,并管理每个组的权限。此外,向许多用户或组添加、修改或删除权限也更容易。 
  • 安全性:对每个用户拥有不同的权限更有利于应用程序的安全性。它只需向普通用户和管理员授予不同的权限,就可以避免假冒用户或利用漏洞破坏应用程序。

对于基于微服务的应用程序,我们建议为每个微服务使用不同的 ACL;这避免了整个应用程序只有一个入口点。请记住,我们正在构建微服务,其中一个要求是微服务应该是隔离的和独立的;因此,使用微服务来控制其余部分并不是一种好的做法。

这不是一项困难的任务,它是有意义的,因为每个微服务都应该有不同的任务,每个任务对于每个用户的权限都是不同的。假设我们有一个拥有以下权限的用户。此用户可以创建机密并检查附近的机密,但不允许创建战斗或新用户。当一个新的微服务被添加到系统中,或者甚至当新的开发人员加入团队并且他们必须了解全局 ACL 的复杂系统时,全局管理 ACL 在可伸缩性方面将是一个问题。正如您所看到的,最好为每个微服务都有一个 ACL 系统,因此当您添加一个新的系统时,不必修改其余的 ACL。

如何使用 ACL

Lumen 为我们提供了一个身份验证过程,以便让用户注册、登录、注销和重置密码,它还提供了一个 ACL 系统,其类称为Gate

Gate允许我们了解特定用户是否具有执行特定操作的权限。这非常简单,可以在 API 的每个方法中使用。

要在 Lumen 上设置 ACL,您必须通过从app->withFacades();行中删除分号来启用bootstrap/app.php上的 facades;如果此行未显示在文件中,请添加它。

还需要在config/Auth.php上创建一个新文件,代码如下:

    <?php
      return [
        'defaults' => [
          'guard' => env('AUTH_GUARD', 'api'),
        ],
        'guards' => [
          'api' => [
            'driver' => 'token',
            'provider' => 'users'
          ],
        ],
        'providers' => [
          'users' => [
            'driver' => 'eloquent',
            // We should get model name from JWT configuration
            'model' => app('config')->get('jwt.user'),
          ],
        ],
      ];

为了检查用户权限,在控制器上使用Gate类需要前面的代码。

一旦设置好了,我们就必须为特定用户定义不同的操作或情况。为此,打开app/Providers/AuthServiceProvider.php文件;在boot()函数中,我们可以通过编写以下代码来定义每个动作或情况:

    <?php
      /* Code Omitted */
      use Illuminate\Contracts\Auth\Access\Gate;
      class AuthServiceProvider extends ServiceProvider
      {
        /* Code Omitted */
        public function boot()
        {
          Gate::define('update-profile', function ($user, $profile) {
            return $user->id === $profile->user_id;
          });
        }

一旦我们确定了形势,我们就可以把它运用到我们的职能中去。有三种不同的使用方式:允许检查拒绝。前两个相同,当定义的情况返回 true 时,它们返回 true;当定义的情况返回 false 时,最后一个返回 true:

    if (Gate::allows('update-profile', $profile)) {
      // The current user can update their profile...
    }
    if (Gate::denies('update-profile', $profile)) {
      // The current user can't update their profile...
    }

如您所见,不需要发送$user变量,它会自动获取当前用户。

源代码的安全性

最可能的情况是,您的项目将使用某些凭据(例如数据库)连接到外部服务。您将把所有这些信息存储在哪里?最常见的方法是在源代码中有一个配置文件,在其中放置所有凭据。这种方法的主要问题是,您将提交凭据,任何可以访问源的人都可以访问这些凭据。你是否信任有权获得回购协议的人并不重要;存储凭据不是一个好主意。

如果您不能在源代码中存储凭据,您可能想知道如何存储它们。您有两个主要选择:

  • 环境变量
  • 对外服务

让我们看看每一个,这样你就可以选择哪个选项对你的项目更好。

环境变量

这种存储凭据的方法非常容易实现——您只需定义要存储在环境中的变量,以后就可以在源代码中获取这些变量。

我们为我们的项目选择的框架是 Lumen,有了这个框架,很容易定义环境变量,然后在代码中使用它们。最重要的文件是.env文件,它位于源代码的根目录中。默认情况下,此文件位于gitignore中以避免提交,但框架附带了.env.example示例,以便您可以检查如何定义变量。在此文件中,您可以找到定义,例如以下定义:

    DB_CONNECTION=mysql
    DB_HOST=localhost
    DB_PORT=3306
    DB_DATABASE=homestead
    DB_USERNAME=homestead
    DB_PASSWORD=secret

前面的定义将创建环境变量,您可以通过简单的env('DB_DATABASE');env('DB_DATABASE', 'default_value');在代码中获取值。env()函数支持两个参数,因此如果您试图获取的变量未定义,您可以定义一个默认值。

使用环境变量的主要好处是,您可以拥有不同的环境,而无需更改源中的任何内容;您甚至可以在不更改代码的情况下更改这些值。

对外服务

这种存储凭据的方式使用外部服务来存储所有凭据,它们的工作方式或多或少类似于环境变量。当您需要任何凭据时,您必须询问此服务。

现在主流的凭证存储系统之一是 HashiCorp Vault 项目,这是一个开源工具,允许您创建一个安全的地方来存储凭证。它有多种好处,我们强调以下几点:

  • HTTP API
  • 键滚动
  • 审计日志
  • 支持多个秘密后端

使用外部服务的主要缺点是增加了应用程序的额外复杂性;您将添加一个新组件来管理和更新。

跟踪监控

当您处理应用程序中的安全性时,跟踪和监视应用程序中发生的事情是很重要的。在第 6 章监控中,我们实现了岗哨作为日志和监控系统,我们还添加了 Datadog 作为我们的 APM,因此您可以使用这些工具跟踪正在发生的事情并向您发送警报。

但是,您希望跟踪什么?假设您有一个登录系统,这个组件是添加跟踪的好地方。如果您跟踪某个用户的每个失败登录,就可以知道是否有人试图攻击您的登录系统。

您的应用程序是否允许用户添加、修改和删除内容?跟踪对内容的任何更改,以便检测不受信任的用户。

在安全方面,没有关于跟踪什么和不跟踪什么的标准,只需使用您的常识即可。我们的主要建议是在应用程序中创建一个敏感点列表,其中至少包括用户可以登录、创建内容或删除内容的位置,然后将这些列表用作添加跟踪和监视的起点。

最佳实践

与应用程序的任何其他部分一样,在处理安全性问题时,您需要遵循或至少了解一些众所周知的最佳实践,以避免将来出现问题。在这里,您可以找到与 web 开发相关的最常见的方法。

文件权限和所有权

最基本的安全机制之一是文件/文件夹权限和所有权。假设您在 Linux/Unix 系统上工作,主要建议将源代码的所有权分配给 web 服务器或 PHP 引擎用户。关于文件权限,应使用以下设置:

  • 500 目录权限(dr-x------):此设置防止意外删除或修改目录中的文件。
  • 400 文件权限(-r------):此设置防止任何用户覆盖文件。
  • 700 权限(drwx------):这是针对任何可写目录的。它将完全控制所有者,并用于上载文件夹。
  • 600 权限(-rw------):此设置适用于任何可写文件。它避免非所有者的任何用户对文件进行任何修改。

PHP 执行位置

通过只允许在选定路径上执行 PHP 脚本,并拒绝在敏感(可写)目录(例如,任何上载目录)中执行任何类型的 PHP 脚本,避免将来出现任何问题。

永远不要信任用户

根据经验,永远不要相信用户。过滤任何来自任何人的输入,你永远不知道表单提交背后的黑暗意图。当然,永远不要只依赖前端过滤和验证。如果在前端添加了筛选和验证,请在后端再次添加。

SQL 注入

没有人希望他们的数据被暴露或被没有权限的人访问,而这种针对您的应用程序的攻击是由于输入的错误过滤或验证造成的。假设您使用一个字段存储未正确过滤的用户名,恶意用户可以使用此字段执行 SQL 查询。要帮助您避免此问题,请在处理数据库时使用 ORM 筛选方法或您喜爱的框架中提供的任何筛选方法。

跨站点脚本 XSS

这是针对您的应用程序的另一种攻击类型,是由于错误的筛选造成的。如果您允许用户在页面上发布任何类型的内容,则某些恶意用户可能会在未经您允许的情况下向页面添加脚本。假设你的页面上有一个评论部分,而你的输入过滤不是最好的,恶意用户可以添加一个脚本作为评论,打开一个垃圾邮件弹出窗口。记住我们之前告诉过你的——永远不要相信你的用户——过滤和验证一切。

会话劫持

在此攻击中,恶意用户窃取另一用户的会话密钥,从而使恶意用户有机会与另一用户相似。假设您的应用程序处理财务信息,恶意用户可以窃取管理员会话密钥,现在该用户可以获得他们需要的所有信息。大多数情况下,会话是使用 XSS 攻击窃取的,因此首先,请尝试避免任何 XSS 攻击。缓解此问题的另一种方法是阻止 JavaScript 访问会话 ID;您可以使用session.cookie.httponly设置在php.ini中执行此操作。

远程文件

从应用程序中包含远程文件可能非常危险,您永远无法 100%确定所包含的远程文件是否可信。如果在某个时候,包含的远程文件被破坏,攻击者可以做他们想做的事情,例如,从应用程序中删除所有数据。

避免这种情况的一个简单方法是禁用php.ini中的远程文件。打开它并禁用以下设置:

  • allow_url_fopen:默认启用
  • allow_url_include:默认禁用;如果禁用allow_url_fopen设置,也会强制禁用此设置

密码存储

切勿以纯文本形式存储任何密码。当我们说永远,我们的意思是永远。如果您认为需要检查用户密码是否有误,则任何类型的恢复或重新提供丢失的密码都需要经过恢复系统。当您存储密码时,您将存储与一些随机盐混合的密码哈希。

密码策略

如果您保留敏感数据,并且不希望您的应用程序被用户的密码暴露,请制定一个非常严格的密码策略。例如,您可以创建以下密码策略以减少破解和字典攻击:

  • 至少 18 个字符
  • 至少 1 个大写字母
  • 至少 1 个
  • 至少 1 个特殊字符
  • 以前没用过
  • 不是用户数据的串联,而是将元音改为数字
  • 每 3 个月到期一次

源代码泄露

不要让好奇的人看到源代码,如果由于某种原因您的服务器坏了,所有源代码都将以纯文本的形式公开。避免这种情况的唯一方法是仅将所需文件保留在 web 服务器根文件夹中。此外,请注意特殊文件,如composer.json。如果我们公开我们的composer.json,每个人都会知道我们每个库的不同版本,这使他们能够轻松了解任何可能的 bug。

目录遍历

这种攻击试图访问存储在 web 根文件夹之外的文件。大多数情况下,这是由于代码中存在错误,因此恶意用户可以操纵引用文件的变量。没有简单的方法可以避免这一点;但是,如果您使用外部框架或库,使它们保持最新将对您有所帮助。

这些是您需要注意的最明显的安全问题,但这并不是一个详尽的列表。订阅安全时事通讯,使您的所有代码保持最新,以将风险降至最低。

总结

在本章中,我们讨论了安全性和身份验证。我们向您展示了如何加密数据和通信层;我们甚至向您展示了如何构建一个健壮的登录系统,以及如何处理应用程序的秘密。安全性在任何项目中都是一个非常重要的方面,因此我们给了您一个需要注意的常见安全风险的小列表,当然,主要建议是——永远不要信任您的用户。