Swoft series of tutorials: (2) authentication services and components

Time:2020-7-14

Swoft provides a complete set of authentication service components, which can be used out of the box after configuration. Users only need to implement the corresponding login authentication logic according to their own business, and the framework authentication component will call your login businesstokenAnd then in the requesttokenParsing and validation are also provided by the framework, and the framework is opentokenAuthority authentication interface to users, we need to implement according to our own businesstokenAuthentication of current access to resources. Let’s talk about the process of JWT’s signing, verification and access control in detail.


Token issuing

tokenThe basic process of issuing is to request the user to log in to the authentication service. If the authentication is passed, it will be issuedtoken。 Swoft’s authentication component is done for ustokenAt the same time, swoft has agreed on aSwoft\Auth\Mapping\AuthManagerInterface::loginMethod is used as the entrance of user authentication service.

Components and services used:

#Authentication component service, use this interface class name as the service name to register in the framework service
`Swoft\Auth\Mapping\AuthManagerInterface::class`
#The concrete implementer of framework authentication component: sign and issue, legal verification and analysis of token
`Swoft\Auth\AuthManager`
#The session carrier of token stores token information
`Swoft\Auth\Bean\AuthSession`

#The authentication service of the contracted user needs to implement the 'Login' method of 'soft' and 'authenticate' method of 'bool'
`Swoft\Auth\Mapping\AccountTypeInterface`
#The necessary data carrier ISS / sub / IAT / exp / data used to sign and issue token is transferred to 'soft / auth / authmanager'
`Swoft\Auth\Bean\AuthResult`

Configuration item:
config/properties/app.phpset upauthpatternjwt

return [
    ...
    'auth' => [
        'jwt' => [
            'algorithm' => 'HS256',
            'secret' => 'big_cat'
        ],
    ]
    ...
];

config/beans/base.phpby\Swoft\Auth\Mapping\AuthManagerInterface::classServices are bound to specific service providers

return [
    'serverDispatcher' => [
        'middlewares' => [
            ...
        ],
        ...
    ],
    //Token issuing and validity verification service
    \Swoft\Auth\Mapping\AuthManagerInterface::class => [
        'class' => \App\Services\AuthManagerService::class
    ],
];

App\Models\Logic\AuthLogic

To realize the authentication of user business, andSwoft\Auth\Mapping\AccountTypeInterfaceInterface is implementedlogin/authenticatemethod.

loginMethod returnSwoft\Auth\Bean\AuthResultObject, stored forjwtCertificate issued:

  • setIdentitycorrespondingsub, i.ejwtGenerally, uid can be used
  • setExtendedDatacorrespondingpayload, i.ejwtIt can store some non sensitive information

authenticateMethod is not used when issuing, but mainly used to verify the validity of the token of the request, i.e. detectionjwtOfsubIs this platform legal user

<?php
namespace App\Models\Logic;

use Swoft\Auth\Bean\AuthResult;
use Swoft\Auth\Mapping\AccountTypeInterface;

class AuthLogic implements AccountTypeInterface
{
    /**
     *User login authentication needs to return the authresult object
     *Returns the swoft / auth / bean / authresult object
     * @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();

        //If the user authentication is successful, a token will be issued
        if ($user instanceof User && $this->userDao->verifyPassword($user, $password)) {
            //The authresult primary ID corresponds to the sub field in JWT
            $authResult->setIdentity($user->getId());
            //Payload of additional data JWT of authresult
            $authResult->setExtendedData([self::ID => $user->getId()]);
        }

        return $authResult;
    }
    
    /**
     *Verify whether the issuing object is legal. Here we simply verify whether the issuing object is the user of the platform
     *$identity is the sub field of JWT
     * @override Swoft\Auth\Mapping\AccountTypeInterface
     *@ param string $identity token sub field
     * @return bool
     */
    public function authenticate(string $identity): bool
    {
        return $this->userDao->exists($identity);
    }
}

Swoft\Auth\AuthManager::loginThe authentication class of the user service and the corresponding authentication field are required to be passed inSwoft\Auth\Bean\AuthResultObject to determine whether the login authentication is successful or not. If successful, it will be issuedtoken, return toSwoft\Auth\Bean\AuthSessionObject.


App\Services\AuthManagerService

User authentication management service, inheritance frameworkSwoft\Auth\AuthManagerDo custom extension. For example, we implement one hereauthMethod is called by the login request,authMethod, the user business authentication module is passed to verify and signtoken, gettokenSession data.

<?php
/**
 *User authentication service
 * 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
{
    /**
     *Cache class
     * @var string
     */
    protected $cacheClass = Redis::class;

    /**
     *JWT has a self-contained feature, which can describe when it will expire, but it can only be issued once
     *JWT cannot be disabled immediately after the user logs off, so we can set a TTL of JWT key name
     *Here we use cacheenable to decide whether to do secondary verification
     *When the token is obtained and parsed, the algorithm layer of the token is correct, but if the JWT key name in redis has expired
     *If the user logs off JWT, it is still considered illegal
     *Therefore, we need to update the JWT key name in redis to be invalid immediately when the user logs off
     *At the same time, the token refresh is verified to ensure that the current user has only one legal token. After refreshing, the previous token will be invalid immediately
     *@ var bool enable cache
     */
    protected $cacheEnable = true;

    //Token is valid for 7 days
    protected $sessionDuration = 86400 * 7;

    /**
     *Define the login authentication method to call the [email protected]  Method to sign the token
     * @param string $account
     * @param string $password
     * @return AuthSession
     */
    public function auth(string $account, string $password): AuthSession
    {
        //Authlogic needs to implement the login / authenticate method of the accounttypeinterface interface
        return $this->login(AuthLogic::class, [
            'account' => $account,
            'password' => $password
        ]);
    }
}

App\Controllers\AuthController

Processing user login requests

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

/**
 *Login authentication module
 * @Controller("/v1/auth")
 * @package App\Controllers
 */
class AuthController
{
    /**
     *User login
     * @RequestMapping(route="login", method={RequestMethod::POST})
     *@ strings (from = validator from:: post, name = account ", min = 6, max = 11, default =, template = {min} ~ {Max} bits are required for account number, and {value} is submitted by you)
     *@ strings (from = validator from:: post, name = password ", min = 6, max = 25, default =, template = password requires {min} ~ {Max} bits, your submitted is {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');

        //Call authentication service - login & sign token
        $session = $this->authManagerService->auth($account, $password);

        //Get the required JWT information
        $data_token = [
            'token' => $session->getToken(),
            'expired_at' => $session->getExpirationTime()
        ];

        return [
            "err" => 0,
            "msg" => 'success',
            "data" => $data_token
        ];
    }
}

POST /v1/auth/loginThe results of

{
    "err": 0,
    "msg": "success",
    "data": {
        "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJBcHBcXE1vZGVsc1xcTG9naWNcXEF1dGhMb2dpYyIsInN1YiI6IjgwIiwiaWF0IjoxNTUxNjAyOTk4LCJleHAiOjE1NTIyMDc3OTgsImRhdGEiOnsidWlkIjo4MH19.u2g5yU9ir1-ETVehLFIIZZgtW7u9aOvH2cndMsIY98Y",
        "expired_at": 1552207798
    }
}

Here’s why you want to provide server-side cachingtokenOptions for$cacheEnable

  • ordinarytokenNojwtWith self describing characteristics, we maintaintokenThe validity period of the token can only be cached in the server to prevent the expired token from being abused.
  • jwtCan self describe the expiration time, why cache? becausejwtThe description of itself is read-only, that is, we can’t make itjwtIf the user logs out of login, it will be destroyedtokenIt’s a good security development habit, so only one is maintained on the server sidejwtWhen the user exits, thetokenThen you can control it freelyjwtThe expiration time of.
/**
 * @param string $token
 * @return bool
 */
public function authenticateToken(string $token): bool
{
    ...
    //If the server-side caching option is enabled, verify whether the token has expired and the validity of the variable direction control 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 parsing and verification

tokenThe implementation process of parsing and validation of validity is only validationtokenThat is, whether the signature is correct, the signer, the issuing object, and whether it is expired. It’s not truetokenAccess rights for authentication.


Components and services used:

#Call the 'token interception service' to try to get the 'token', and call the 'token management service' for resolution and legitimacy verification
`Swoft\Auth\Middleware\AuthMiddleware`

#`Token interception service`
`Swoft\Auth\Mapping\AuthorizationParserInterface::class`
#`The token intercepts the service provider and calls the corresponding token parser according to the token type`
`Swoft\Auth\Parser\AuthorizationHeaderParser`

#`The token management service is provided by the token management service provider and called by the token parser
`Swoft\Auth\Mapping\AuthManagerInterface::class`
#`The token management service provider is responsible for issuing, parsing and verifying the validity
`Swoft\Auth\AuthManager`

Swoft\Auth\Middleware\AuthMiddlewareResponsible for intercepting requests and callingtokenParsing and validation services. Will try to get theAuthorizationField values, based on typeBasic/BearerTo select the corresponding authority authentication service component pairtokenVerify the validity and generate ittokenconversation. But it does not involve business accessACLIn other words, only a certaintokenIt is legally signed and issued by this platform, and this is not guaranteedtokenHas legal access to the current resource. IfAuthorizationIf it is empty, it is regarded as a normal request.

Implementation process:

  1. Swoft\Auth\Middleware\AuthMiddlewarecallSwoft\Auth\Mapping\AuthorizationParserInterface::classService, service bySwoft\Auth\Parser\AuthorizationHeaderParserrealization.
  2. serviceAuthorizationHeaderParserAn attempt was made to get theAuthorizationField value, if thetokenAccording totokenType:BasicorBearerTo call the specific parser.BasicThe parser for is`Swoft\Auth\Parser\Handler::BasicAuthHandlerBearerThe parser for isSwoft\Auth\Parser\Handler::BearerTokenHandlerIn the following, we will specificallyBearerPatternedjwtIs an example.
  3. After getting the type ofBearerOftokenAfter that,BearerTokenHandlerWill be calledSwoft\Auth\Mapping\AuthManagerInterface::classServiceauthenticateTokenThe method is righttokenVerify and analyze the validity, that is to judge thistokenWhether the signature is legal, whether the signer is legal, and whether the signing object is legal (Note: theApp\Models\Logic\AuthLogic::authenticateMethod validation), whether it is expired, etc.
  4. tokenIf the parsing verification is illegal, an exception is thrown to interrupt the request processing.
  5. tokenIf the resolution is valid, thepayloadLoad this session and continue.

So we can register the middleware to the global and request to carry ittokenIt will be parsed and verified, and will not be carriedtokenIt is regarded as a normal request.


#config/beans/base.php
return [
    'serverDispatcher' => [
        'middlewares' => [
            ...
            \Swoft\Auth\Middleware\AuthMiddleware::class
        ],
        ...
    ],
    //Token issuing and validity verification service
    \Swoft\Auth\Mapping\AuthManagerInterface::class => [
        'class' => \App\Services\AuthManagerService::class
    ],
];

<?php
namespace AppModelsLogic;

use SwoftAuthBeanAuthResult;
use SwoftAuthMappingAccountTypeInterface;

class AuthLogic implements AccountTypeInterface
{

...
/**
 *Verify whether the issuing object is legal. Here we simply verify whether the issuing object is the user of the platform
 *$identity is the sub field of JWT
 * @override Swoft\Auth\Mapping\AccountTypeInterface
 *@ param string $identity token sub field
 * @return bool
 */
public function authenticate(string $identity): bool
{
    return $this->userDao->exists($identity);
}

}


ACL authentication

tokenAlthough it has been verified by legality, it can only be explainedtokenIt is signed and issued by this platform. It is impossible to judge thistokenWhether you have access to the current business resources, so we also need to introduceACL certification

Components and services used:

#ACL authentication Middleware
Swoft\Auth\Middleware\AclMiddleware

#User business authority auth service
Swoft\Auth\Mapping\AuthServiceInterface::class

#Token session access component
Swoft\Auth\AuthUserService
  1. Swoft\Auth\Middleware\AclMiddlewareThe middleware will callSwoft\Auth\Mapping\AuthServiceInterface::classService, which is mainly used forAclAuthentication, that is to verify whether the current request is legaltoken, andtokenWhether you have access to the current resource.
  2. Swoft\Auth\Mapping\AuthServiceInterface::classServices are provided by the framework’sSwoft\Auth\AuthUserServiceComponent implementation acquisitiontokenPart of the function of conversation,authMethods are overridden by the user layer, so we need to inheritSwoft\Auth\AuthUserServiceAnd according to their own business needs to achieveauthmethod.
  3. In successionSwoft\Auth\AuthUserServiceIn the user business authentication component, we can try to obtaintokenThe signing object of the session andpayloadData:getUserIdentity/getUserExtendData。 And then in theauthMethod to determine whether the current request hastokenSession and whether you have access to the current resource to determine the returntrue or falsetoAclMiddlewareMiddleware.
  4. AclMiddlewareThe middleware obtains theauthbyfalse(the request does not carry a legal token401Or do not have access to the current resource403), the terminal requests processing.
  5. AclMiddlewareThe middleware obtains theauthbytrue, indicating that the request is legaltoken, andtokenYou have access to the current resource. Continue processing the request.

config/bean/base.php

return [
    'serverDispatcher' => [
        'middlewares' => [
            ....
            //System token parsing Middleware
            \Swoft\Auth\Middleware\AuthMiddleware::class,
            ...
        ]
    ],
    //Token issuing and validity verification service
    \Swoft\Auth\Mapping\AuthManagerInterface::class => [
        'class' => \App\Services\AuthManagerService::class
    ],
    //ACL user resource authority authentication service
    \Swoft\Auth\Mapping\AuthServiceInterface::class => [
        'class' => \App\Services\AclAuthService::class,
        'userlogic' = > '${'. [app / Models / logic / userlogic:: class. '}' // inject userlogicbean
    ],
];

App\Services\AclAuthService

YestokendoAclAuthentication.

<?php
namespace App\Services;

use Swoft\Auth\AuthUserService;
use Swoft\Auth\Mapping\AuthServiceInterface;
use Psr\Http\Message\ServerRequestInterface;

/**
 *Bean in config / beans/ base.php  It has been registered in the form of parameter configuration, so bean annotation can no longer be used here
 * Class AclAuthService
 * @package App\Services
 */
class AclAuthService extends AuthUserService implements AuthServiceInterface
{
    /**
     *User logic module
     *Because this module is injected into the system service in the way of parameter configuration
     *Therefore, its related dependencies also need to be injected in parameter configuration mode, and cannot be declared with the inject annotation
     * @var App\Models\Logic\UserLogic
     */
    protected $userLogic;

    /**
     *Cooperate with aclmiddleware middleware to verify whether the user request is legal
     *True aclmiddleware passes
     *false AclMiddleware throw AuthException
     * @override AuthUserService
     * @param string $requestHandler
     * @param ServerRequestInterface $request
     * @return bool
     */
    public function auth(string $requestHandler, ServerRequestInterface $request): bool
    {
        //Identification of issuing object
        $sub = $this->getUserIdentity();
        //Token load
        $payload = $this->getUserExtendData();
        
        //Verify whether the current token has access to the business resource aclauth as its own authentication logic
        if ($this->aclAuth($sub, $payload)) {
            return true;
        }
        return false;
    }
}