当前位置: 首页 > 工具软件 > EduSoho > 使用案例 >

EduSoho Api 鉴权

柯苗宣
2023-12-01

EduSoho Api 鉴权

在请求接口时,首先要验证用户是否拥有访问系统的权限。举个简单的例子,我心里有个秘密只想和亲密的朋友分享,这时候有个陌生人想问我这个秘密,我会选择拒绝。这里区分亲密朋友和陌生人的过程就可以称为鉴权。

EduSoho 有一个特定的 ApiBundle 来实现接口,其中就包含了鉴权的逻辑。

一、EduSoho如何实现接口鉴权

建立鉴权机制,只有通过鉴权才能调用接口。首先我们会验证 token,然后验证权限(可选),全部通过后服务端才会去执行接口内具体业务逻辑。

1.1 接口鉴权之 token 验证

将具体实现 Token 鉴权的监听方法利用依赖注入到 service_container 里,循环调用具体监听方法。

1.1.1 具体认证Token的监听方法

具体认证 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);
    }
}
1.1.2 将监听方法依赖注入到service_container
    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']
1.1.3 Firewall 对具体 token 鉴权方法的实现
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.ymlapi_firewallarguments 里,去增加接口鉴权。

Tip: 查看已注入symfony容器的服务的命令

在 EduSoho 项目根目录执行。

app/console debug:container

1.2 接口鉴权之权限认证

部分接口是只有管理员权限才可以访问,这时候我们就要用到权限鉴权。

    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']
1.2.1 ResourceAuthenticationProviderManager 方法

将我们需要的具体认证方式(例: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);
        }
    }

}
1.2.2 具体的权限鉴权方法 DefaultResourceAuthenticationProvider
<?php

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);
        }
    }
}
1.2.3 如何不进行权限鉴权

细心的同学应该已经看到了上面提到的 DefaultResourceAuthenticationProvider 类中有一个 getIsRequiredAuth 方法,现在具体分析一下这个方法。

    // DefaultResourceAuthenticationProvider
    ...
    if ($annotation && !$annotation->getIsRequiredAuth()) {
        return;
    }
    ...
<?php

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;
    }
}
接口取消权限认证设置 isRequiredAuth=false

在具体的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

 类似资料: