Swoft 系列教程:(2)認證服務及組件

Swoft 提供了一整套認證服務組件,基本作到了配置後開箱即用。用戶只需根據自身業務實現相應的登陸認證邏輯,框架認證組件會調用你的登陸業務進行token的簽發,然後的請求中token解析、合法性驗證也都由框架提供,同時框架開放了token權限認證接口給用戶,咱們需根據自身業務實現token對當前訪問資源權限的認證。下面咱們詳細講一下 jwt 的簽發及驗證、訪問控制的流程。php


token 簽發

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模式jwtjson

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合法性時用到,即檢測jwtsub是否爲本平臺合法用戶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`,並調用`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;
    }
}
相關文章
相關標籤/搜索