Symfony 2.0のプロジェクトでの非標準認証

この記事の目的は、Symfony 2.0に基づいたプロジェクトの非標準認証の編成について話すことです。



なぜこれが必要なのでしょうか?



これは、ORMを介してユーザーアカウントへのアクセスを整理することが困難な場合、またはソーシャルネットワークなどの外部リソースを使用して承認が実行される場合に必要になることがあります。





挑戦する



私にとって、タスクは次のように見えました:Symfony 1.4でプロジェクトを作成し、2番目のバージョンのリリースで、システムの基盤を変更することなく、「手に傷」を付けて2.0に変換しました。 データベースとそれにアクセスする原理。



私のプロジェクトのデータはMongoDBに保存されており、アクセスは独自の単純なラッパーを介して行われました。これにより、クラスを必要なコレクションにバインドできます。 ラッパーをSymfony 2にバインドするのは簡単でした。



ユーザー認証に関するタスクは、最初はMongoDBからデータを取得することまで削減されました。 改善の課題は、ソーシャルネットワークアカウントでの承認の可能性を実現することです。



理論のビット



Symfony 2.0の独自のセキュリティ拡張機能の作業を開始する前に、フレームワークの一般的な動作、特にセキュリティコンポーネントの動作を理解する必要があります。



フレームワークの2番目のバージョンに興味を持っている人は誰でも、着信要求がイベントまたはイベントとして扱われ、その後処理のためにリスナーによって受信されることを知っていると思います。 この処理中に、応答(別名応答)が表示され、クライアントに返されます。



一般的に、すべては次のように発生します。

  1. カーネルはリクエストを受け取ります。
  2. 要求はセキュリティシステムによって処理されます。
  3. 要求は「ルーター」によって処理され、要求されたコントローラーが決定されます。
  4. アクションおよび関連する応答準備プロセスが実行されます。
  5. 生成された回答は、最新の調整が行われるハンドラーの一部を通過します。


ステージ2に関心があり、一部は3です。



セキュリティシステムの仕組みは別の歌です。 プロセスは次のようになります。

  1. 一般的なセキュリティリスナーは要求イベントを受信し、それを「子」認証リスナーに配信します。
  2. リスナーはトークンを形成し、AuthenticationManagerを使用してトークンを認証しようとします。
  3. AuthenticationManagerは適切な認証プロバイダーを選択し、検証のためにトークンを提供します。
  4. 認証プロバイダーはマネージャーに戻ります。そうでない場合、リスナーは順番に承認済み(または未承認)トークンになります。 この段階で、UserProviderがアクティブになり、ユーザーデータが機能します。
  5. リスナは、AuthenticationManagerとその独自のコードによって返された結果に応じて、許可されたトークンをSecurityContexに「パームオフ」するか、応答(Response)を生成するか、またはその両方を行います。 リスナーが応答を生成する場合、要求イベントは「ルーター」に送信されず、コントローラーへの呼び出しはありません。
  6. その後、承認が行われますが、これ以降の手順は実行しませんでしたが、原則としては、理解しているとおり、認証に似ています。


説明されているすべてのプロセスがアクションで直接行われた1.xと比較すると、このスキームは過負荷になり、不必要に複雑になります。 ただし、このような構造により、アプリケーションでセキュリティを整理するすべての作業を行わず、必要な部分のみを変更し、残りの作業をフレームワークに委ねることができます。



ステップ0-ドキュメントを調べます。



Symfony 2.0セキュリティから何を取得したい場合でも、ビジネスに取り掛かる前に関連ドキュメントを絞り込む必要があります。 セキュリティサービスコンテナ というの章なしではできません。 自分の言語で読みたい場合は、この記事をご覧ください



実際、「奇妙な」ソースからユーザーに関する情報を受け取りたくない場合は、さらに読む必要はありません。 Doctrineを介したデータベースの操作については、リストされている記事で詳しく説明されています。



ステップ1-独自のユーザーベース。



すぐに使えるSymfony 2.0には、かなり多数の承認ツールがあります。 ログイン/パスワードペア認証を使用する最も単純なオプションと、証明書認証などのよりエキゾチックなオプションの両方があります。



まず、ログインとパスワードで最も一般的な認証を整理する必要がありますが、MongoDB上の独自のデータベースから、独自のラッパーを通じてデータを取得します。



これは実際にはタスクの最も簡単な部分です。 まず、UserInterfaceインターフェースを実装するクラスが必要です。



namespace MyBundle\Models; use Mh\Mongo\Model\Base; use Symfony\Component\Security\Core\User\AdvancedUserInterface; use Symfony\Component\Security\Core\User\UserInterface; class User extends Base implements UserInterface { public function getRoles() { return $this->credentials; } public function getPassword() { return $this->passw; } public function getSalt() { return $this->salt; } public function getUsername() { return $this->uname; } public function eraseCredentials() { } public function equals(UserInterface $user) { return $this->getUsername() === $user->getUsername(); } public function __toString() { return $this->uname; } }
      
      







クラスは非常にシンプルです。この場合、MongoDBからドキュメントデータにアクセスするためのすべての機能は親クラスに表示されますが、この記事のトピックではありません。 UserInterfaceインターフェースの実装により、フレームワークセキュリティシステムは、ユーザーデータに必要なデータを受信し、ユーザーオブジェクトを相互に比較することができます。



getRolesメソッドに注意を払う価値があります。上記の記事では、このメソッドはRoleエンティティを返しますが、出力には単純な文字列を持つ配列があり、これはシステムに受け入れられます。 役割を変数の可変セットを含むことができるグループと見なす場合、RoleInterfaceと互換性のあるオブジェクトを返す必要があります。この場合、役割は権利のセットであるため、文字列値で十分です。



次に、UserProviderInterfaceインターフェースを実装するクラスが必要です。これにより、上記のクラスのオブジェクトを取得できます。



 namespace MyBundle\Security; use Mh\Mongo\MongoBundle\ConnectionManager; use MyBundle\Models\User; use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Core\User\UserProviderInterface; use \Exception; use Symfony\Component\Security\Core\Exception\UsernameNotFoundException; use Symfony\Component\Security\Core\Exception\UnsupportedUserException; class UserProvider implements UserProviderInterface { protected $collection; protected $db; protected $field; protected $logger; //  ,  ,      //  ,          // . public function __construct(array $params, ConnectionManager $cm, LoggerInterface $logger) { //         $this->db = $cm->getConnection($params['confname']); $this->collection = $this->db->selectCollection($params['collection']); //       ,   $this->field = $params['field']; if (!$this->field || !$this->collection) { throw new Exception("Invalid parameters"); } //     logger',   DI $this->logger = $logger; } //        , //    . public function loadUserByUsername($uname) { //      . $this->logger->debug("user load request. name: $uname"); //         User //   . $user = $this->collection->findOne(array($this->field => $uname)); //  . ,    . if (!isset($user->uname) || $user->uname !== $uname) { throw new UsernameNotFoundException(sprintf('User "%s" does not exist.', $uname)); } //     return $user; } //       . //        . public function refreshUser(UserInterface $user) { if (!($user instanceof User)) throw new UnsupportedUserException(sprintf('Instances of "%s" are not supported.', get_class($user))); $this->logger->info("refresh from mongo"); $_user = $this->collection->findOne(array('_id' => $user->_id)); if ($_user && $_user instanceof User) $this->logger->info("roles: " .implode(', ',$_user->roles)); else throw new UsernameNotFoundException(sprintf('User "%s" does not exist.', $user->uname)); return $_user; } //    . public function supportsClass($class) { $this->logger->debug("support checking: $class"); if ($class == 'MyBundle\Models\User') return true; return false; } }
      
      







ここで、プロバイダーをサービスとして登録し、それを使用するためにセキュリティコンポーネントを「教える」必要があります。



バンドルのservices.ymlで、サービスに関するデータとそのパラメーターを追加します。



パラメータ:
   my.users:
     confname:デフォルト
    コレクション:ユーザー
    フィールド:uname

サービス:
   my.users.prov:
    クラス:MyBundle \ Security \ UserProvider
    引数:[%my.users%、@ mongo.manager、@ monolog.logger]




security.ymlアプリケーションでは、次のように記述します。



セキュリティ:
    エンコーダー:
         Symfony \コンポーネント\セキュリティ\コア\ユーザー\ユーザー:プレーンテキスト
         #起動を簡単にするために、暗号化されていないパスワードをデータベースに保存できます		
         MyBundle \ Models \ User:プレーンテキスト

    プロバイダー:
	 #新しいユーザーソースを宣言する(任意の名前を割り当てることができる)
	 #データを提供するサービスの名前をそれに書き込む 
         #ユーザーについて
         mongobase:
           id:my.userprov
 ...




どこにも明示的に言及されていない機能が1つあります(または注意を払っていません)。 security.ymlで複数のユーザープロバイダーが宣言されている場合、最初のプロバイダーが(そうでない場合)、認証のために直接指定されます。



実際には、MongoDBのデータを使用して既にログインを開始できます。 私の場合、データベースには既にプロジェクトの以前のバージョンからのデータがあります。 上記の記事で説明されているエントリポイントの作成方法については、再度説明することは意味がないと思います。



ステップ2-OAuth認証



プロジェクトの新バージョンの革新として、ソーシャルネットワークのアカウントからユーザーを承認する機能を追加することにしました。 ネットワーク。 例として、私はすべての熱い「愛されている」ソーシャルネットワークを介した認証と登録について話します。 VKontakteネットワーク。



Symfiny 2.0で独自の認証方法を作成することは、文書化されていると考えることができます。 WSSEを使用した認証の実装に関する公式記事があります。 必要な機能の作業を開始して、この記事に焦点を合わせました。 社会との仕事の原則。 この記事からハブのネットワークを取得しました



次に、受け取った情報の少しの分析。 公式記事の例で説明されている認証は、私が必要とするものとあまり似ていません。 VKontakteアカウントでユーザーを認証するプロセスは、通常の(ログイン/パスワードに基づく)認証に似ていますが、ログインまたはパスワードはありません。 これについて考えた後、Symfonyのソースコード、特にフォーム(form_login)を介した基本的な承認がどのように配置されているかを調べることにしました。



組み込みのAuthenticationListenersとAuthenticationProvidersは、Symfonyでの認証の直接管理、ユーザーセッションの操作などに関連するほとんどのルーチン作業を引き受ける抽象クラスから継承することが判明しました。これらのクラスを使用して作業を促進することもできます。 。



始めましょう。



公式記事のように、トークンから始めましょう:



 namespace MyBundle\Social\Authentication; use Symfony\Component\Security\Core\Authentication\Token\AbstractToken; class Token extends AbstractToken { //      . public $social; public $hash; public $add; //   ,    function __construct(array $roles = array()) { parent::__construct($roles); //    ,    //          . parent::setAuthenticated(count($roles) > 0); } // ,    TokenInterface public function getCredentials() { } //         , //      .     “” //     . public function serialize() { $pser = parent::serialize(); return serialize(array($this->social, $this->hash, $this->add, $pser)); } public function unserialize($serialized) { list($this->social, $this->hash, $this->add, $pser) = unserialize($serialized); parent::unserialize($pser); } }
      
      







トークンクラスは、ユーザークラスの場合のように、非常に単純です。 次に、今説明したクラスのトークンを作成するリスナーが必要です。



 namespace MyBundle\Social\Authentication; //       AbstractAuthenticationListener, //     “”   , //       use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Event\GetResponseEvent; use Symfony\Component\Security\Http\Firewall\ListenerInterface; use Symfony\Component\Security\Core\Exception\AuthenticationException; use Symfony\Component\Security\Core\SecurityContextInterface; use Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Http\Firewall\AbstractAuthenticationListener; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Security\Http\Session\SessionAuthenticationStrategyInterface; use Symfony\Component\Security\Http\HttpUtils; use Symfony\Component\HttpKernel\Log\LoggerInterface; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\Form\Extension\Csrf\CsrfProvider\CsrfProviderInterface; class AuthenticationListener extends AbstractAuthenticationListener { //      “”  .  protected $social; //    ,  //   public function __construct(SecurityContextInterface $securityContext, AuthenticationManagerInterface $authenticationManager, SessionAuthenticationStrategyInterface $sessionStrategy, HttpUtils $httpUtils, $providerKey, array $options = array(), AuthenticationSuccessHandlerInterface $successHandler = null, AuthenticationFailureHandlerInterface $failureHandler = null, LoggerInterface $logger = null, EventDispatcherInterface $dispatcher = null, array $social = array()) { parent::__construct($securityContext, $authenticationManager, $sessionStrategy, $httpUtils, $providerKey, array_merge(array( 'intention' => 'authenticate', ), $options), $successHandler, $failureHandler, $logger, $dispatcher); $this->social = $social; } //  attemptAuthentication   ,   //    ,    Token' public function attemptAuthentication(Request $request) { //      ,     //     if ($request->get('uid') && $request->get('hash') && $request->cookies->get("vk_app_{$this->social['vk']['id']}")) { $this->logger->debug("vk auth handled"); //     $uid = $request->get('uid'); $fn = $request->get('first_name'); $ln = $request->get('last_name'); $hash = $request->get('hash'); $this->logger->info("user $fn $ln [$uid] // $hash"); $avatars = array( 'sav' => $request->get('photo'), 'srav' => $request->get('photo_rec'), ); //   token     //   .  … $token = new Token(); $token->setUser("vk{$uid}"); // …     token' ... $token->social = 'vk'; $token->hash = $hash; $token->add = array( 'uid' => $uid, 'avatar' => $avatars, 'name' => "$fn $ln", ); // …     ,     return $this->authenticationManager->authenticate($token); } //        -   . } }
      
      







基本的に、リスナーの役割は、ユーザー認証に必要なデータをリクエストから抽出することです。 AbstractAuthenticationListenerは、リクエストの検証と解析コードのみの記述に限定する機会を提供し、URI、リダイレクト管理、およびAuthenticationListenerの「責任」の一部である他のルーチンによるリクエストのフィルタリングのための既製の機能を提供します。



Listenrを実装した後、トークンを直接確認し、成功した認証を「アナウンス」するAuthenticationProviderが必要です。



 namespace MyBundle\Social\Authentication; use Mh\Mongo\MongoBundle\ConnectionManager; use MyBundle\Models\User as User; use Symfony\Component\Security\Core\Authentication\Provider\AuthenticationProviderInterface; use Symfony\Component\Security\Core\User\UserProviderInterface; use Symfony\Component\Security\Core\Exception\AuthenticationException; use Symfony\Component\Security\Core\Exception\NonceExpiredException; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Exception\UsernameNotFoundException; use Symfony\Component\HttpKernel\Log\LoggerInterface; //    AuthenticationProvider'     // ,     UserAuthenticationProvider, //            . class Provider implements AuthenticationProviderInterface { protected $userProvider; protected $logger; //     .  protected $social; //       MongoDB  //    protected $mongo; //   ,     //  . public function __construct(UserProviderInterface $userProvider, array $social, ConnectionManager $cm, LoggerInterface $logger) { $this->userProvider = $userProvider; $this->social = $social; $this->mongo = $cm; $this->logger = $logger; } //    public function authenticate(TokenInterface $token) { $user = null; //      UserProvider'a //      ,      try { $user = $this->userProvider->loadUserByUsername($token->getUsername()); } catch (UsernameNotFoundException $ex) { $this->logger->debug("user ".$token->getUsername()." not yet registred"); } try { //    hash' if ($this->checkHash($token)) { $this->logger->info("hash is valid"); //  ,    hash,   //    -  “” if (!$user) { $this->logger->info("register new user"); $user = new User(array( 'uname' => $token->getUsername(), 'social' => $token->social, 'fullname' => $token->add['name'], 'avatar' => $token->add['avatar'], 'suid' => $token->add['uid'], 'roles' => array('ROLE_EXTUSER', strtoupper($token->social) ), )); $user->save($this->mongo); } //    . ,   Token //      $authenticatedToken = new Token($user->getRoles()); $authenticatedToken->social = $token->social; $authenticatedToken->hash = $token->hash; $authenticatedToken->add = $token->add; $authenticatedToken->setUser($user); //        return $authenticatedToken; } else { $this->logger->debug("hash is invalid."); } } catch (\Exception $ex) { $this->logger->err("auth internal exception: $ex"); } //   -     -  //  . throw new AuthenticationException('The Social authentication failed.'); } //   hesh',      //  . protected function checkHash(Token $token) { if ($token->social == 'vk') { return ($token->hash === md5( $this->social['vk']['id'] . $token->add['uid'] . $this->social['vk']['key'] )); } return false; } //  ,    token'  provider' public function supports(TokenInterface $token) { return $token instanceof Token; } }
      
      







AuthenticationProviderの作業を完了した後は、フレームワークに、作成したクラスを使用するように教えるだけです。 UserProviderとは異なり、これはかなりボリュームがあり、作業の「透明な」部分ではありません。



公式ドキュメントでは、コードを使用するために、SymfonyセキュリティシステムはSecurityFactoryInterfaceの実装を記述する必要があると述べています。 やってみましょう。



 namespace MyBundle\DependencyInjection\Security; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\DependencyInjection\DefinitionDecorator; use Symfony\Component\Config\Definition\Builder\NodeDefinition; use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\SecurityFactoryInterface; //   Symfony   AbstractFactory     . //    ,      ,   //    AuthenticationProvider' class SocialAuthFactory implements SecurityFactoryInterface { //      firewall'    //  .     form_login. protected $options = array( 'check_path' => '/login_check', 'login_path' => '/login', 'use_forward' => false, 'always_use_default_target_path' => false, 'default_target_path' => '/', 'target_path_parameter' => '_target_path', 'use_referer' => false, 'failure_path' => null, 'failure_forward' => false, ); //  ,       firewall  listener  // provider. //      , id firewall'  // ,       . public function create(ContainerBuilder $container, $id, $config, $userProvider, $defaultEntryPoint) { //      ,     //   . //      “”   // (  )    . //  id    AuthenticationProvider' $providerId = 'security.authentication.provider.social.'.$id; //    -    my.socialauth.prov //  (0)         //   $container ->setDefinition($providerId, new DefinitionDecorator('my.socialauth.prov')) ->replaceArgument(0, new Reference($userProvider)) ; //  listener'    ,    provider' $listenerId = 'security.authentication.listener.social.'.$id; $container ->setDefinition($listenerId, new DefinitionDecorator('my.socialauth.listener')) ->replaceArgument(4, $id) ->replaceArgument(5, array_intersect_key($config, $this->options)); //   id  . return array($providerId, $listenerId, $defaultEntryPoint); } //  ,       //   .      login_form. public function getPosition() { return 'form'; } //      ,      //     . public function getKey() { return 'mysocial'; } //   ,      // AbstractFactory public function addConfiguration(NodeDefinition $node) { $builder = $node->children(); $builder ->scalarNode('provider')->end() ; foreach ($this->options as $name => $default) { if (is_bool($default)) { $builder->booleanNode($name)->defaultValue($default); } else { $builder->scalarNode($name)->defaultValue($default); } } } }
      
      







Factoryを作成した後は、構成を補足するためだけに残ります。



security.ymlから始めましょう。 そこで、ニッチの工場出荷時設定ファイルへのリンクを追加する必要があります...



 セキュリティ:

    工場:
         -「%kernel.root_dir%/ .. / src / MyBundle / Resources / config / socialauth_factory.yml」
 ... 




...さらに、参照するファイルを作成して入力します。構文は通常のservices.ymlの構文に似ています。



サービス:
     security.authentication.factory.mysocial:
        クラス:MyBundle \ DependencyInjection \ Security \ Factory \ SocialFactory
        タグ:
             -{名前:security.listener.factory}




ここで、ファクトリで参照するサービスとともにservices.yml自体を追加する必要があります。



 パラメータ:
   my.users:
     confname:デフォルト
    コレクション:ユーザー
    フィールド:uname

   my.social:
     id:__YOUR_APP_ID__
    キー:__YOUR_APP_PRIVATE_KEY__

サービス:
   my.users.prov:
    クラス:MyBundle \ Security \ UserProvider
    引数:[%my.users%、@ mongo.manager、@ monolog.logger]

   #リスナーのサービスをAbstractListenerサービスの継承者として宣言します
   #ここで示された引数は、引数の後にコンストラクタに渡されます。   
   #親サービスで規定
   my.socialauth.listener:
    クラス:MyBundle \ Social \ Authentication \ Listener
    親:security.authentication.listener.abstract
    引数:[%my.social%]
    
   #プロバイダーのサービスでは、最初の引数は実際の値なしのままにします。
   #値は、Factoryで作成されたデコレータから渡されます。
   my.socialauth.prov:  
    クラス:MyBundle \ Social \ Authentication \ Provider
    引数:[''、%my.social%、@ mongo.manager、@ monolog.logger]




最後にすべきことは、アプリケーションの構成にサブシステムを含めることです。 これを行うには、security.ymlを再度編集し、その中でファイアウォールがブロックします:



  ...   
    ファイアウォール:
         myauth:
            パターン:^ /
             #ブロック内のファイアウォールを通常のform_loginに追加し、 
             #設定がほぼ同じであることを思い出してください。
             mysocial:  
                 check_path:/ login / socialauth
                 login_path:/ login / in
             #私たちのクラスは、ボックス化されたクラスやSymfonyと同等の条件で機能します
             #互いに干渉しないように、異なるcheck_pathを実行します
             form_login:
                 check_path:/ login / auth
                 login_path:/ login / in
	      #2つの方法で認証を許可します。出力は 
            標準的な手段によるユーザーへの両方のタイプの#
            ログアウト:
                パス:/ login / out
                ターゲット:/
                 invalidate_session:true
            匿名:〜
 ... 




以上です。 VKontakteを介して認証フォームをログインフォームテンプレートに追加することにより、このソーシャルネットワークを使用してログインできます。 ネットワーク。



まとめ



Symfony 2.0のセキュリティシステムは最初から複雑でわかりにくいように見えます。 一般に、これは非常に複雑でわかりにくいですが、2番目と1番目のバージョンのセキュリティ組織を比較すると、1つの傾向が見られます。



セキュリティシステムは、典型的なタスクをフレームワークの構成レベルで解決できるように編成されています。 ログイン/パスワードを確認するためにプログラムする必要はありません(フォームの描画を除く)。確認するアクションを実行する必要さえありません。 ログアウトについても同じです。 個々の要素の実装の複雑さは、主に、柔軟なセキュリティアーキテクチャに要素を埋め込む必要性に関連しています。



ご清聴ありがとうございました。



All Articles