Swoft 提供了一整套认证服务组件,基本作到了配置后开箱即用。用户只需根据自身业务实现相应的登陆认证逻辑,框架认证组件会调用你的登陆业务进行token
的签发,然后的请求中token
解析、合法性验证也都由框架提供,同时框架开放了token
权限认证接口给用户,咱们需根据自身业务实现token
对当前访问资源权限的认证。下面咱们详细讲一下 jwt 的签发及验证、访问控制的流程。php
token
签发的基本流程为请求用户登陆认证服务,认证经过则签发token
。Swoft 的认证组件为咱们完成了token
签发工做,同时 Swoft 约定了一个Swoft\Auth\Mapping\AuthManagerInterface::login
方法做为用户的认证业务的入口。redis
使用到的组件及服务:算法
#认证组件服务,使用此接口类名做为服务名注册到框架服务中 `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
json
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
签发的凭证:session
setIdentity
对应 sub
,即jwt
的签发对象,通常使用uid便可setExtendedData
对应 payload
, 即jwt
的载荷,存储一些非敏感信息便可authenticate
方法签发时用不到,主要在验证请求的token合法性时用到,即检测jwt
的sub
是否为本平台合法用户app
<?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(); // 用户验证成功则签发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
会话数据。ide
<?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管理服务`作解析及合法性验证 `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
为空的话则视为普通请求。
执行流程:
Swoft\Auth\Middleware\AuthMiddleware
调用 Swoft\Auth\Mapping\AuthorizationParserInterface::class
服务,服务具体由 Swoft\Auth\Parser\AuthorizationHeaderParser
实现。AuthorizationHeaderParser
尝试获取请求头中的Authorization
字段值,若是获取到token
,则根据token
的类型:Basic
orBearer
来调用具体的解析器。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.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); }
}
token
虽然通过了合法性验证,只能说明token
是本平台签发的,还没法判断此token
是否有权访问当前业务资源,因此咱们还要引入Acl认证
。
使用到的组件及服务:
#Acl认证中间件 Swoft\Auth\Middleware\AclMiddleware #用户业务权限auth服务 Swoft\Auth\Mapping\AuthServiceInterface::class #token会话访问组件 Swoft\Auth\AuthUserService
Swoft\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.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
对token
作Acl
鉴权。
<?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; } }