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合法性时用到,即检测jwt
的sub
是否为本平台合法用户
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
会话数据。
login(AuthLogic::class, [
'account' => $account,
'password' => $password
]);
}
}
App\Controllers\AuthController
处理用户的登录请求
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
为空的话则视为普通请求。
执行流程:
-
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
],
];
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
-
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
orfalse
给AclMiddleware
中间件。 -
AclMiddleware
中间件获取到用户业务下的auth
为false
(请求没有携带合法token401
或无权访问当前资源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
鉴权。
getUserIdentity();
// token载荷
$payload = $this->getUserExtendData();
// 验证当前token是否有权访问业务资源 aclAuth为自己的认证逻辑
if ($this->aclAuth($sub, $payload)) {
return true;
}
return false;
}
}