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` 签发 token
`Swoft\Auth\Bean\AuthResult`

配置项:
config/properties/app.php设定auth模式jwt

return [...'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合法性时用到,即检测jwtsub是否为本平台合法用户

<?php
namespace 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();// 用户验证成功则签发tokenif ($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\AuthManager`

Swoft\Auth\Middleware\AuthMiddleware负责拦截请求并调用token解析及验证服务。会尝试获取请求头中的Authorization字段值,根据类型Basic/Bearer来选择相应的权限认证服务组件对token做合法性的校验并生成token会话。但并不涉及业务访问权限ACL的验证,即只保证某个token 是本平台合法签发的,不保证此token对当前资源有合法的访问权限。如果Authorization为空的话则视为普通请求。

执行流程:

  1. Swoft\Auth\Middleware\AuthMiddleware调用 Swoft\Auth\Mapping\AuthorizationParserInterface::class 服务,服务具体由 Swoft\Auth\Parser\AuthorizationHeaderParser实现。
  2. 服务AuthorizationHeaderParser尝试获取请求头中的Authorization字段值,如果获取到token,则根据token的类型:BasicorBearer来调用具体的解析器。Basic的解析器为`Swoft\Auth\Parser\Handler::BasicAuthHandlerBearer的解析器为 Swoft\Auth\Parser\Handler::BearerTokenHandler,下面我们具体以Bearer模式的jwt为示例。
  3. 在获取到类型为Bearertoken后,BearerTokenHandler将会调用Swoft\Auth\Mapping\AuthManagerInterface::class服务的authenticateToken方法来对token进行合法性的校验和解析,即判断此token的签名是否合法,签发者是否合法,签发对象是否合法(注意:调用了App\Models\Logic\AuthLogic::authenticate方法验证),是否过期等。
  4. token解析验证非法,则抛出异常中断请求处理。
  5. token解析验证合法,则将payload载入本次会话并继续执行。

所以我们可以将此中间件注册到全局,请求携带token则解析验证,不携带token则视为普通请求。


#config/beans/base.php
return ['serverDispatcher' => ['middlewares' => [...\Swoft\Auth\Middleware\AuthMiddleware::class],...],// token签发及合法性验证服务\Swoft\Auth\Mapping\AuthManagerInterface::class => ['class' => \App\Services\AuthManagerService::class],
];

<?php
namespace 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\AuthUserService
  1. Swoft\Auth\Middleware\AclMiddleware中间件会调用Swoft\Auth\Mapping\AuthServiceInterface::class服务,此服务主要用于Acl认证,即验证当前请求是否携带了合法token,及token是否对当前资源有访问权限。
  2. Swoft\Auth\Mapping\AuthServiceInterface::class服务由框架的Swoft\Auth\AuthUserService组件实现获取token会话的部分功能,auth方法则交由用户层重写,所以我们需继承Swoft\Auth\AuthUserService并根据自身业务需求实现auth方法。
  3. 在继承了Swoft\Auth\AuthUserService的用户业务认证组件中,我们可以尝试获取token会话的签发对象及payload数据:getUserIdentity/getUserExtendData。然后在auth方法中判断当前请求是否有token会话及是否对当前资源有访问权限,来决定返回true or falseAclMiddleware中间件。
  4. AclMiddleware中间件获取到用户业务下的authfalse(请求没有携带合法token 401 或无权访问当前资源 403),则终端请求处理。
  5. AclMiddleware中间件获取到在用户业务下的authtrue,则说明请求携带合法token,且token对当前资源有权访问,继续请求处理。

config/bean/base.php

return ['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

tokenAcl鉴权。

<?php
namespace 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;}
}

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

  1. Node-Red系列教程——NodeRed使用node-red-node-email组件发送QQ邮件

    参考文章<node-red教程 8.2 node-red收发邮件(基于QQ邮箱)_yummy说电子的博客-CSDN博客> 目录 1.开启QQ邮件的IMAP/SMTP服务,获取授权码. 2. ...

  2. 【REACT NATIVE 系列教程之四】刷新组件RENDER(重新渲染)的三种方式详解

    本站文章均为 李华明Himi 原创,转载务必在明显处注明:  转载自[黑米GameDev街区] 原文链接: http://www.himigame.com/react-native/2242.html ...

  3. 【React系列教程五】父子组件传值、defaultProps和propTypes

    一.父组件给子组件传值(即子组件调用父组件数据和方法) 1.在调用子组件的时候,定义一个属性  <Header msg='首页'></Header> 2.子组件中通过 this ...

  4. 【REACT NATIVE 系列教程之十三】利用LISTVIEW与TEXTINPUT制作聊天/对话框获取组件实例常用的两种方式...

    本站文章均为 李华明Himi 原创,转载务必在明显处注明:  转载自[黑米GameDev街区] 原文链接: http://www.himigame.com/react-native/2346.html ...

  5. appinventor java,(1) AppInventor高级组件之JavaReflector系列教程 概述

    了解过Android开发的同学们对Java语言一定不会陌生,在原生的Android开发中我们几乎可以用Java实现任何功能,甚至通过Java的jni调用一些基于C/C++所写的库. 那么ai中有没有可 ...

  6. asp.NET自定义服务器控件内部细节系列教程四

    如大家要转载,请保留本人的版权: /* *Description:asp.NET自定义服务器控件内部细节系列教程 *Auther:崇崇-天真的好蓝 *MSN:chongchong2008@msn.co ...

  7. Go 系列教程 —— 20. 并发入门

    欢迎来到我们 Golang 系列教程的第 20 篇. Go 是并发式语言,而不是并行式语言.在讨论 Go 如何处理并发之前,我们必须理解何为并发,以及并发与并行的区别. 并发是什么? 并发是指立即处理 ...

  8. 【Python3.6+Django2.0+Xadmin2.0系列教程之一(入门篇-上)】环境搭建及项目创建

    由于工作需要,接触了大半年时间的Django+xadmin框架,一直没空对这块对进行相关的梳理.最近在同事的怂恿下,就在这分享下笔者的学习及工作经验吧. 好了,话不多说,下面开始进入正题: 转载请注明 ...

  9. SCCM 2007系列教程之二客户端安装之客户端请求安装

    一.客户端安装方法:     客户端请求安装.软件更新点安装. 组策略安装.登录脚本安装.手动安装.升级安装.客户端映像 客户端成功安装客户端之后,它将尝试分配到某个站点,并找到该站点的默认管理点以下 ...

最新文章

  1. 谷歌兄弟公司Wing将于10月开始试点无人机配送
  2. expect简单教程
  3. 各种资源思科、gns3……
  4. iOS:多个单元格的删除(方法二):
  5. api商品分享源码_SSM框架高并发和商品秒杀项目高并发秒杀API源码免费分享
  6. GIT的 .gitignore 配置
  7. CSDN10大博客栏目火热评选中
  8. spring学习--完全注解开发(不需要xml)
  9. 基于jQuery或Zepto实现实时监控用户浏览信息
  10. 禅道---Bug管理模块
  11. MySQL-第十三篇使用ResultSetMetaData分析结果集
  12. Vuforia的ARcamera通过识别人工标识码出现虚拟物体后在其相机视野中不显示人工标识码
  13. GD32F303修改外部25M晶振
  14. 阿里云 CentOS 7.4 下部署基于 Node.js 的微信小程序商城
  15. python语言是不是多模型语言_Python模型转换为Modelica模型的方法与流程
  16. 最近抖音上虚拟元宇宙项目-猜歌名,代码解析
  17. 泛微工作流程管理解决方案(转载)
  18. 反射+自定义属性拼接sql
  19. Oliver的救援 题解
  20. 用户活跃、留存、流失,终于讲清楚了!

热门文章

  1. 化工人员定位系统实现高效智能管理
  2. 犀牛4.0鞋业插件orang1.2
  3. GPFS 并行文件系统
  4. JBookMaker免费下载(能把TXT文档转化为手机即时阅读的小工具)
  5. 校园一卡通系统发展概况及未来趋势
  6. 基于单片机的多功能出租车计价器设计资料
  7. pdf编辑阅读器PDF Reader Pro for Mac
  8. mysql8.0.17下载教程_Mysql8.0.17安装教程【推荐】
  9. csc_matix函数
  10. 温度传感器——热电偶