在请求接口时,首先要验证用户是否拥有访问系统的权限。举个简单的例子,我心里有个秘密只想和亲密的朋友分享,这时候有个陌生人想问我这个秘密,我会选择拒绝。这里区分亲密朋友和陌生人的过程就可以称为鉴权。
EduSoho
有一个特定的 ApiBundle
来实现接口,其中就包含了鉴权的逻辑。
建立鉴权机制,只有通过鉴权才能调用接口。首先我们会验证 token
,然后验证权限(可选),全部通过后服务端才会去执行接口内具体业务逻辑。
将具体实现 Token
鉴权的监听方法利用依赖注入到 service_container
里,循环调用具体监听方法。
具体认证 Token 的监听方法有很多,此处以
XAuthTokenAuthenticationListener
方法为例。
class XAuthTokenAuthenticationListener extends BaseAuthenticationListener
{
const TOKEN_HEADER = 'X-Auth-Token';
public function handle(Request $request)
{
if (null !== $this->getTokenStorage()->getToken()) {
return;
}
if (null === $tokenInHeader = $request->headers->get(self::TOKEN_HEADER)) {
return;
}
if (null === $rawToken = $this->getUserService()->getToken('mobile_login', $tokenInHeader)) {
throw new UnauthorizedHttpException('X-Auth-Token', 'Token is not exist or token is expired', null, ErrorCode::EXPIRED_CREDENTIAL);
}
$token = $this->createTokenFromRequest($request, $rawToken['userId']);
$this->getTokenStorage()->setToken($token);
}
}
api_token_header_listener:
class: ApiBundle\Security\Firewall\XAuthTokenAuthenticationListener
arguments: ['@service_container']
api_basic_authentication_listener:
class: ApiBundle\Security\Firewall\BasicAuthenticationListener
arguments: ['@service_container']
api_firewall:
class: ApiBundle\Security\Firewall\Firewall
arguments:
- ['@api_basic_authentication_listener', '@api_token_header_listener']
class Firewall implements ListenerInterface
{
private $listeners;
public function __construct(array $listeners)
{
$this->listeners = $listeners;
}
public function addListener($listener)
{
$this->listeners[] = $listener;
}
/**
* @return TokenInterface
*/
public function handle(Request $request)
{
foreach ($this->listeners as $listener) {
$listener->handle($request);
}
return null;
}
}
如果产品大大又有新需求,那么可以新增
Token
鉴权方法,再新增到services.yml
的api_firewall
下arguments
里,去增加接口鉴权。
在 EduSoho 项目根目录执行。
app/console debug:container
部分接口是只有管理员权限才可以访问,这时候我们就要用到权限鉴权。
api_default_authentication:
class: ApiBundle\Security\Authentication\DefaultResourceAuthenticationProvider
arguments: ['@service_container']
api_authentication_manager:
class: ApiBundle\Security\Authentication\ResourceAuthenticationProviderManager
arguments:
- '@service_container'
- ['@api_default_authentication']
将我们需要的具体认证方式(例:api_default_authentication)作为参数传入该方法中,方便拓展。
class ResourceAuthenticationProviderManager implements ResourceAuthenticationInterface
{
private $providers;
private $container;
public function __construct(ContainerInterface $container, array $providers)
{
$this->container = $container;
$this->providers = $providers;
}
public function addProvider($provider)
{
$this->providers[] = $provider;
}
/**
* {@inheritdoc}
*/
public function authenticate(ResourceProxy $resourceProxy, $method)
{
foreach ($this->providers as $provider) {
$provider->authenticate($resourceProxy, $method);
}
}
}
class DefaultResourceAuthenticationProvider implements ResourceAuthenticationInterface
{
private $tokenStorage;
private $container;
/**
* @var CachedReader
*/
private $annotationReader;
public function __construct(ContainerInterface $container)
{
$this->annotationReader = $container->get('annotation_reader');
$this->tokenStorage = $container->get('security.token_storage');
$this->container = $container;
}
public function authenticate(ResourceProxy $resourceProxy, $method)
{
$annotation = $this->annotationReader->getMethodAnnotation(
new \ReflectionMethod(get_class($resourceProxy->getResource()), $method),
'ApiBundle\Api\Annotation\ApiConf'
);
if ($annotation && !$annotation->getIsRequiredAuth()) {
return;
}
$accessAnnotation = $this->annotationReader->getMethodAnnotation(
new \ReflectionMethod(get_class($resourceProxy->getResource()), $method),
'ApiBundle\Api\Annotation\Access'
);
$biz = $this->container->get('biz');
$currentUser = $biz['user'];
if ($accessAnnotation && !$accessAnnotation->canAccess($currentUser->getRoles())) {
throw new UnauthorizedHttpException('Role', 'Roles are not allow', null, ErrorCode::UNAUTHORIZED);
}
$token = $this->tokenStorage->getToken();
if (!$token instanceof TokenInterface || $token instanceof AnonymousToken) {
throw new UnauthorizedHttpException('Basic', 'Requires authentication', null, ErrorCode::UNAUTHORIZED);
}
}
}
细心的同学应该已经看到了上面提到的
DefaultResourceAuthenticationProvider
类中有一个getIsRequiredAuth
方法,现在具体分析一下这个方法。
// DefaultResourceAuthenticationProvider
...
if ($annotation && !$annotation->getIsRequiredAuth()) {
return;
}
...
namespace ApiBundle\Api\Annotation;
/**
* @Annotation
* @Target({"METHOD"})
*/
class ApiConf
{
/**
* @var boolean
*/
private $isRequiredAuth;
public function __construct(array $data)
{
foreach ($data as $key => $value) {
$method = 'set'.str_replace('_', '', $key);
if (!method_exists($this, $method)) {
throw new \BadMethodCallException(sprintf('Unknown property "%s" on annotation "%s".', $key, get_class($this)));
}
$this->$method($value);
}
}
public function getIsRequiredAuth()
{
return $this->isRequiredAuth;
}
}
在具体的API中,我们只需要将 $isRequiredAuth
参数写在注解里,设置 isRequiredAuth=false
,即表示该接口不需要权限认证,具体代码如下:
class XXX extends AbstractResource
{
/**
* @ApiConf(isRequiredAuth=false)
*/
public function get(ApiRequest $request, $paramId)
{
......
}
}
在请求接口时得先去访问防火墙。
EduSoho
对于以/api
开头的请求,统一请求同一个控制器。
api:
resource: "@ApiBundle/Controller/"
type: annotation
prefix: /api
利用注解形式配置路由,在控制器中去调用鉴权token、权限的方法。
class EntryPointController
{
/**
* @Route("/{res1}")
* @Route("/{res1}/{slug1}")
* @Route("/{res1}/{slug1}/{res2}")
* @Route("/{res1}/{slug1}/{res2}/{slug2}")
* @Route("/{res1}/{slug1}/{res2}/{slug2}/{res3}")
* @Route("/{res1}/{slug1}/{res2}/{slug2}/{res3}/{slug3}")
* @Route("/{res1}/{slug1}/{res2}/{slug2}/{res3}/{slug3}/{res4}")
* @Route("/{res1}/{slug1}/{res2}/{slug2}/{res3}/{slug3}/{res4}/{slug4}")
*/
public function startAction(Request $request)
{
...
$this->container->get('api_firewall')->handle($request);
$this->container->get('api_authentication_manager')->authenticate();
...
}
}
我们正在寻求合作伙伴
EduSoho官方开发文档地址
EduSoho官网 https://www.edusoho.com/
EduSoho开源地址 https://github.com/edusoho/edusoho