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; } }