Swoft 系列教程:(2)认证服务及组件

Swoft 提供了一整套认证服务组件,基本做到了配置后开箱即用。用户只需根据自身业务实现相应的登录认证逻辑,框架认证组件会调用你的登录业务进行token的签发,而后的请求中token解析、合法性验证也都由框架提供,同时框架开放了token权限认证接口给用户,我们需根据自身业务实现token对当前访问资源权限的认证。下面我们详细讲一下 jwt 的签发及验证、访问控制的流程。token 签发token 签发的基本流程为请求用户登录认证服务,认证通过则签发token。Swoft 的认证组件为我们完成了token签发工作,同时 Swoft 约定了一个Swoft\Auth\Mapping\AuthManagerInterface::login方法作为用户的认证业务的入口。使用到的组件及服务:#认证组件服务,使用此接口类名作为服务名注册到框架服务中Swoft\Auth\Mapping\AuthManagerInterface::class#框架认证组件的具体实现者 token 的签发、合法校验、解析Swoft\Auth\AuthManager#token的会话载体 存储着token的信息Swoft\Auth\Bean\AuthSession#约定用户的认证业务需实现返回Swoft\Auth\Bean\AuthResult的login方法和bool的authenticate的方法Swoft\Auth\Mapping\AccountTypeInterface#用于签发token的必要数据载体 iss/sub/iat/exp/data 传递给 Swoft\Auth\AuthManager 签发 tokenSwoft\Auth\Bean\AuthResult配置项:config/properties/app.php设定auth模式jwtreturn [ … ‘auth’ => [ ‘jwt’ => [ ‘algorithm’ => ‘HS256’, ‘secret’ => ‘big_cat’ ], ] …];config/beans/base.php为\Swoft\Auth\Mapping\AuthManagerInterface::class服务绑定具体的服务提供者return [ ‘serverDispatcher’ => [ ‘middlewares’ => [ … ], … ], // token签发及合法性验证服务 \Swoft\Auth\Mapping\AuthManagerInterface::class => [ ‘class’ => \App\Services\AuthManagerService::class ],];App\Models\Logic\AuthLogic实现用户业务的认证,以 Swoft\Auth\Mapping\AccountTypeInterface 接口的约定实现了 login/authenticate方法。login方法返回Swoft\Auth\Bean\AuthResult对象,存储用于jwt签发的凭证:setIdentity 对应 sub,即jwt的签发对象,一般使用uid即可setExtendedData 对应 payload, 即jwt的载荷,存储一些非敏感信息即可authenticate方法签发时用不到,主要在验证请求的token合法性时用到,即检测jwt的sub是否为本平台合法用户<?phpnamespace App\Models\Logic;use Swoft\Auth\Bean\AuthResult;use Swoft\Auth\Mapping\AccountTypeInterface;class AuthLogic implements AccountTypeInterface{ /** * 用户登录认证 需返回 AuthResult 对象 * 返回 Swoft\Auth\Bean\AuthResult 对象 * @override Swoft\Auth\Mapping\AccountTypeInterface * @param array $data * @return AuthResult / public function login(array $data): AuthResult { $account = $data[‘account’]; $password = $data[‘password’]; $user = $this->userDao->getByConditions([‘account’ => $account]); $authResult = new AuthResult(); // 用户验证成功则签发token if ($user instanceof User && $this->userDao->verifyPassword($user, $password)) { // authResult 主标识 对应 jwt 中的 sub 字段 $authResult->setIdentity($user->getId()); // authResult 附加数据 jwt 的 payload $authResult->setExtendedData([self::ID => $user->getId()]); } return $authResult; } /* * 验证签发对象是否合法 这里我们简单验证签发对象是否为本平台用户 * $identity 即 jwt 的 sub 字段 * @override Swoft\Auth\Mapping\AccountTypeInterface * @param string $identity token sub 字段 * @return bool / public function authenticate(string $identity): bool { return $this->userDao->exists($identity); }}Swoft\Auth\AuthManager::login 要求传入用户业务的认证类,及相应的认证字段,根据返回Swoft\Auth\Bean\AuthResult对象判断登录认证是否成功,成功则签发token,返回Swoft\Auth\Bean\AuthSession对象。App\Services\AuthManagerService用户认证管理服务,继承框架Swoft\Auth\AuthManager做定制扩展。比如我们这里实现一个auth方法供登录请求调用,auth 方法中则传递用户业务认证模块来验证和签发token,获取token会话数据。<?php/* * 用户认证服务 * User: big_cat * Date: 2018/12/17 0017 * Time: 16:36 /namespace App\Services;use App\Models\Logic\AuthLogic;use Swoft\Redis\Redis;use Swoft\Bean\Annotation\Bean;use Swoft\Bean\Annotation\Inject;use Swoft\Auth\AuthManager;use Swoft\Auth\Bean\AuthSession;use Swoft\Auth\Mapping\AuthManagerInterface;/* * @Bean() * @package App\Services /class AuthManagerService extends AuthManager implements AuthManagerInterface{ /* * 缓存类 * @var string / protected $cacheClass = Redis::class; /* * jwt 具有自包含的特性 能自己描述自身何时过期 但只能一次性签发 * 用户主动注销后 jwt 并不能立即失效 所以我们可以设定一个 jwt 键名的 ttl * 这里使用是否 cacheEnable 来决定是否做二次验证 * 当获取token并解析后,token 的算法层是正确的 但如果 redis 中的 jwt 键名已经过期 * 则可认为用户主动注销了 jwt,则依然认为 jwt 非法 * 所以我们需要在用户主动注销时,更新 redis 中的 jwt 键名为立即失效 * 同时对 token 刷新进行验证 保证当前用户只有一个合法 token 刷新后前 token 立即失效 * @var bool 开启缓存 / protected $cacheEnable = true; // token 有效期 7 天 protected $sessionDuration = 86400 * 7; /* * 定义登录认证方法 调用 Swoft的AuthManager@login 方法进行登录认证 签发token * @param string $account * @param string $password * @return AuthSession / public function auth(string $account, string $password): AuthSession { // AuthLogic 需实现 AccountTypeInterface 接口的 login/authenticate 方法 return $this->login(AuthLogic::class, [ ‘account’ => $account, ‘password’ => $password ]); }}App\Controllers\AuthController处理用户的登录请求<?php/* * Created by PhpStorm. * User: big_cat * Date: 2018/12/10 0010 * Time: 17:05 /namespace App\Controllers;use App\Services\AuthManagerService;use Swoft\Http\Message\Server\Request;use Swoft\Http\Server\Bean\Annotation\Controller;use Swoft\Http\Server\Bean\Annotation\RequestMapping;use Swoft\Http\Server\Bean\Annotation\RequestMethod;use Swoft\Bean\Annotation\Inject;use Swoft\Bean\Annotation\Strings;use Swoft\Bean\Annotation\ValidatorFrom;/* * 登录认证模块 * @Controller("/v1/auth") * @package App\Controllers /class AuthController{ /* * 用户登录 * @RequestMapping(route=“login”, method={RequestMethod::POST}) * @Strings(from=ValidatorFrom::POST, name=“account”, min=6, max=11, default="", template=“帐号需{min}{max}位,您提交的为{value}”) * @Strings(from=ValidatorFrom::POST, name=“password”, min=6, max=25, default="", template=“密码需{min}{max}位,您提交的为{value}”) * @param Request $request * @return array / public function login(Request $request): array { $account = $request->input(‘account’) ?? $request->json(‘account’); $password = $request->input(‘password’) ?? $request->json(‘password’); // 调用认证服务 - 登录&签发token $session = $this->authManagerService->auth($account, $password); // 获取需要的jwt信息 $data_token = [ ’token’ => $session->getToken(), ’expired_at’ => $session->getExpirationTime() ]; return [ “err” => 0, “msg” => ‘success’, “data” => $data_token ]; }}POST /v1/auth/login 的结果{ “err”: 0, “msg”: “success”, “data”: { “token”: “eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJBcHBcXE1vZGVsc1xcTG9naWNcXEF1dGhMb2dpYyIsInN1YiI6IjgwIiwiaWF0IjoxNTUxNjAyOTk4LCJleHAiOjE1NTIyMDc3OTgsImRhdGEiOnsidWlkIjo4MH19.u2g5yU9ir1-ETVehLFIIZZgtW7u9aOvH2cndMsIY98Y”, “expired_at”: 1552207798 }}这里提及一下为什么要提供在服务端缓存token的选项$cacheEnable。普通的token不像jwt具有自我描述的特性,我们为维护token的有效期只能在服务端缓存其有效期,防止过期失效的token被滥用。jwt可以自我描述过期时间,为什么也要缓存呢?因为jwt自身的描述是只读的,即我们无法让jwt提前过期失效,如果用户退出登录,则销毁token是个不错的安全开发习惯,所以只有在服务端也维护了一份jwt的过期时间,用户退出时过期此token,那么就可以自由控制jwt的过期时间。/* * @param string $token * @return bool /public function authenticateToken(string $token): bool{ … // 如果开启了服务端缓存选项 则验证token是否过期 可变向控制jwt的有效期 if ($this->cacheEnable === true) { try { $cache = $this->getCacheClient() ->get($this->getCacheKey($session->getIdentity(), $session->getExtendedData())); if (! $cache || $cache !== $token) { throw new AuthException(ErrorCode::AUTH_TOKEN_INVALID); } } catch (InvalidArgumentException $e) { $err = sprintf(‘Identity : %s ,err : %s’, $session->getIdentity(), $e->getMessage()); throw new AuthException(ErrorCode::POST_DATA_NOT_PROVIDED, $err); } } $this->setSession($session); return true;}token 解析、验证token的解析及合法性验证实现流程,注意只是验证token的合法性,即签名是否正确,签发者,签发对象是否合法,是否过期。并未对 token 的访问权限做认证。使用到的组件及服务:#调用token拦截服务尝试获取token,并调用token管理服务做解析及合法性验证Swoft\Auth\Middleware\AuthMiddleware#token拦截服务``Swoft\Auth\Mapping\AuthorizationParserInterface::class#token拦截服务提供者,根据token类型调用相应的token解析器``Swoft\Auth\Parser\AuthorizationHeaderParser#token管理服务,由token管理服务提供者提供基础服务,被token解析器调用Swoft\Auth\Mapping\AuthManagerInterface::class#token管理服务提供者,负责签发、解析、合法性验证Swoft\Auth\AuthManagerSwoft\Auth\Middleware\AuthMiddleware负责拦截请求并调用token解析及验证服务。会尝试获取请求头中的Authorization字段值,根据类型Basic/Bearer来选择相应的权限认证服务组件对token做合法性的校验并生成token会话。但并不涉及业务访问权限ACL的验证,即只保证某个token 是本平台合法签发的,不保证此token对当前资源有合法的访问权限。如果Authorization为空的话则视为普通请求。执行流程:Swoft\Auth\Middleware\AuthMiddleware调用 Swoft\Auth\Mapping\AuthorizationParserInterface::class 服务,服务具体由 Swoft\Auth\Parser\AuthorizationHeaderParser实现。服务AuthorizationHeaderParser尝试获取请求头中的Authorization字段值,如果获取到token,则根据token的类型:BasicorBearer来调用具体的解析器。Basic的解析器为`Swoft\Auth\Parser\Handler::BasicAuthHandler,Bearer的解析器为 Swoft\Auth\Parser\Handler::BearerTokenHandler,下面我们具体以Bearer模式的jwt为示例。在获取到类型为Bearer的token后,BearerTokenHandler将会调用Swoft\Auth\Mapping\AuthManagerInterface::class服务的authenticateToken方法来对token进行合法性的校验和解析,即判断此token的签名是否合法,签发者是否合法,签发对象是否合法(注意:调用了App\Models\Logic\AuthLogic::authenticate方法验证),是否过期等。token解析验证非法,则抛出异常中断请求处理。token解析验证合法,则将payload载入本次会话并继续执行。所以我们可以将此中间件注册到全局,请求携带token则解析验证,不携带token则视为普通请求。#config/beans/base.phpreturn [ ‘serverDispatcher’ => [ ‘middlewares’ => [ … \Swoft\Auth\Middleware\AuthMiddleware::class ], … ], // token签发及合法性验证服务 \Swoft\Auth\Mapping\AuthManagerInterface::class => [ ‘class’ => \App\Services\AuthManagerService::class ],];<?phpnamespace AppModelsLogic;use SwoftAuthBeanAuthResult;use SwoftAuthMappingAccountTypeInterface;class AuthLogic implements AccountTypeInterface{…/* * 验证签发对象是否合法 这里我们简单验证签发对象是否为本平台用户 * $identity 即 jwt 的 sub 字段 * @override Swoft\Auth\Mapping\AccountTypeInterface * @param string $identity token sub 字段 * @return bool /public function authenticate(string $identity): bool{ return $this->userDao->exists($identity);}}acl鉴权token 虽然经过了合法性验证,只能说明token是本平台签发的,还无法判断此token是否有权访问当前业务资源,所以我们还要引入Acl认证。使用到的组件及服务:#Acl认证中间件Swoft\Auth\Middleware\AclMiddleware#用户业务权限auth服务Swoft\Auth\Mapping\AuthServiceInterface::class#token会话访问组件Swoft\Auth\AuthUserServiceSwoft\Auth\Middleware\AclMiddleware中间件会调用Swoft\Auth\Mapping\AuthServiceInterface::class服务,此服务主要用于Acl认证,即验证当前请求是否携带了合法token,及token是否对当前资源有访问权限。Swoft\Auth\Mapping\AuthServiceInterface::class服务由框架的Swoft\Auth\AuthUserService组件实现获取token会话的部分功能,auth方法则交由用户层重写,所以我们需继承Swoft\Auth\AuthUserService并根据自身业务需求实现auth方法。在继承了Swoft\Auth\AuthUserService的用户业务认证组件中,我们可以尝试获取token会话的签发对象及payload数据:getUserIdentity/getUserExtendData。然后在auth方法中判断当前请求是否有token会话及是否对当前资源有访问权限,来决定返回true or false给AclMiddleware中间件。AclMiddleware中间件获取到用户业务下的auth为false(请求没有携带合法token 401 或无权访问当前资源 403),则终端请求处理。AclMiddleware中间件获取到在用户业务下的auth为true,则说明请求携带合法token,且token对当前资源有权访问,继续请求处理。config/bean/base.phpreturn [ ‘serverDispatcher’ => [ ‘middlewares’ => [ …. //系统token解析中间件 \Swoft\Auth\Middleware\AuthMiddleware::class, … ] ], // token签发及合法性验证服务 \Swoft\Auth\Mapping\AuthManagerInterface::class => [ ‘class’ => \App\Services\AuthManagerService::class ], // Acl用户资源权限认证服务 \Swoft\Auth\Mapping\AuthServiceInterface::class => [ ‘class’ => \App\Services\AclAuthService::class, ‘userLogic’ => ‘${’ . \App\Models\Logic\UserLogic::class . ‘}’ // 注入UserLogicBean ],];App\Services\AclAuthService对token做Acl鉴权。<?phpnamespace App\Services;use Swoft\Auth\AuthUserService;use Swoft\Auth\Mapping\AuthServiceInterface;use Psr\Http\Message\ServerRequestInterface;/* * Bean 因在 config/beans/base.php 中已经以参数配置的方式注册,故此处不能再使用Bean注解声明 * Class AclAuthService * @package App\Services /class AclAuthService extends AuthUserService implements AuthServiceInterface{ /* * 用户逻辑模块 * 因本模块是以参数配置的方式注入到系统服务的 * 所以其相关依赖也需要使用参数配置方式注入 无法使用Inject注解声明 * @var App\Models\Logic\UserLogic / protected $userLogic; /* * 配合 AclMiddleware 中间件 验证用户请求是否合法 * true AclMiddleware 通过 *false AclMiddleware throw AuthException * @override AuthUserService * @param string $requestHandler * @param ServerRequestInterface $request * @return bool */ public function auth(string $requestHandler, ServerRequestInterface $request): bool { // 签发对象标识 $sub = $this->getUserIdentity(); // token载荷 $payload = $this->getUserExtendData(); // 验证当前token是否有权访问业务资源 aclAuth为自己的认证逻辑 if ($this->aclAuth($sub, $payload)) { return true; } return false; }} ...

March 5, 2019 · 4 min · jiezi

Swoft 系列教程:(1)使用 Docker 安装部署 Swoft

之前有写过一篇 Docker 安装部署 Swoft 的文章,但有些冗余混乱,故重写作为教程的开篇。要不读读看?Swoft项目:https://github.com/swoft-clou…Swoft文档:https://doc.swoft.org/Swoft镜像:https://hub.docker.com/r/swof…Swoft 简介首个基于 Swoole 原生协程的新时代 PHP 高性能协程全栈框架,内置协程网络服务器及常用的协程客户端,常驻内存,不依赖传统的 PHP-FPM,全异步非阻塞 IO 实现,以类似于同步客户端的写法实现异步客户端的使用,没有复杂的异步回调,没有繁琐的 yield, 有类似 Go 语言的协程、灵活的注解、强大的全局依赖注入容器、完善的服务治理、灵活强大的 AOP、标准的 PSR 规范实现等等,可以用于构建高性能的Web系统、API、中间件、基础服务等等。即异步非阻塞IO,EventLoop,事件驱动。cpu_num 个 worker 即可承载高并发请求,提供协程/异步IO客户端,数据库连接池,对象连接池,任务进程池。优雅的注解声明,IOC/DI容器,严格遵循PSR规范。Swoft 镜像的主要用途Swoft 官方提供了基于 Debine 的 Docker 镜像。镜像中已安装配置好运行 Swoft 的所需组件及依赖:PHP 7.0+ / Swoole / Composer / Pecl。虽然不使用镜像从头安装部署以上几项组件也不难,但镜像内置可以开箱即用,免去了这些略繁琐的工作,让我们尽可能快的投入到 Swoft 的开发中去。此外Swoft 镜像与开发的配合如果只是单纯的想快速体验 Swoft,使用 docker run -p 80:80 swoft/swoft 拉取创建容器访问即可。如何正确的在 Swoft 项目的开发中使用镜像呢?如果是要将镜像好好利用到开发工作中,则需要清楚一下几点。镜像内置完全安装的 Swoft 框架,但它只是用来快速演示的,并不是要你拿去修改,开发还是要对本地的 Swoft 项目开发。我们应该做的是将本地的 Swoft 框架 挂载到镜像的工作目录 /var/www/swoft 从而替换掉镜像自带的,这样启动 Swoft服务 就会启动映射到本地的 Swoft 项目了镜像的容器启动时默认会启动 Swoft 服务 作为前置进程,这就要求我们在挂载了本地 Swoft 项目时需要保证已完全安装了各项依赖(github 拉取下来的 Swoft 源码 并没有安装库依赖,需要使用 Composer install 一下)好像咬到尾巴了,为了开发需要挂载本地 Swoft 项目到镜像工作目录,因为容器启动时还会一并启动 Swoft 服务,所以要求挂载的本地 Swoft项目 必须使用 Composer 安装好依赖,嗯?这不还是得在本地装 PHP + Composer 嘛,镜像不是都提供了嘛,重复劳动了。修改 Swoft 镜像的 entrypoint 使得 Swoft 容器启动时不同时启动 Swoft 服务,这就不需要要求我们挂载的本地 Swoft 项目必须完全安装好依赖了。容器创建好后,登入容器 sh,使用镜像内置的 Composer 安装依赖启动 Swoft 服务这样就能充分利用镜像内置的环境和工具,快乐的开始 Swoft 的开发了工作了,下面给出具体的实例。Swoft 镜像的使用前面夸赞了那么多镜像的便利之处,下面如果不完全把镜像用到极致那就不太好了 O(∩_∩)O哈哈1、首先我们从 github 上拉取最新的 Swoft 源码到本地cd ~ && git clone git@github.com:swoft-cloud/swoft.git && cd swoft2、查看 swoft 镜像的 Dockerfile# 在文件尾设定了 entrypoint 命令为 启动 swoft服务ENTRYPOINT [“php”, “/var/www/swoft/bin/swoft”, “start”]entrypoint 就是我们后面需要改掉的参数3、直接使用镜像创建容器docker run -p 8081:80 \ #映射宿主机808-v $(pwd):/var/www/swoft #挂载本地 Swoft 项目到镜像工作目录-it -d \ #重要 开启 stdin tty 并以daemon模式运行–entrypoint="" #重要 覆盖掉镜像内设定的 entrypoint 参数–name my_swoft #容器命令–privileges=true #赋予权限swoft/swoft bash4、使用 docker-compose 更为简洁#编辑 docker-compose 编排文件vim docker-compose.yml#内容修改如下version: ‘3’services: swoft: image: swoft/swoft:latest container_name: my_swoft # 给容器自定义个名称便于管理 #build: ./ ports: - “8081:80” #端口映射 volumes: - ./:/var/www/swoft # 挂载本地swoft项目到镜像工作目录 stdin_open: true #打开标准输出 -i tty: true # 打开 tty 会话 -t privileged: true # 给与权限 比如创建文件夹之类的 #entrypoint: [“php”, “/var/www/swoft/bin/swoft”, “start”] # 入口启动命令 即启动 swoft 服务 entrypoint: [“bash”] 创建容器docker-compose up -d swoft ./5、登入容器,安装依赖,开启 Swoft 服务使用3或4创建的Swoft容器,便以 bash 作为启动的前置进程,而非启动 Swoft 服务,我们登入容器使用内置的 Composer 安装依赖后,启动Swoft服务即可。docker exec -it my_swoft bash# 安装框架依赖composer install# 启动/停止/重启 Swoft 服务php bin/swoft start|stop|restar6、开启热重载,关闭 daemon,让框架调试信息输出到 stderr 方便开发调试编辑本地的 Swoft 项目 .env 文件# ApplicationAPP_DEBUG=true# Server…AUTO_RELOAD=true…# Swoole Settings…DAEMONIZE=0…保存并重新启动 Swoft服务小提示:可以使用 PHPStorm IDE 配置 FTP/SFTP 文件改动自动上传的方式,开发起飞 ...

February 21, 2019 · 2 min · jiezi