2016-02-11 15 views
6

私はSymfony2とAngularJsを含むWebアプリケーションを開発しています。サイトのユーザーを認証する正しい方法について質問があります。Symfony2と角。ユーザー認証

私はリクエストで渡されたparamsを通してユーザーを認証するAPI REST(symfonyで構築された)に関数を構築しました。

/** 
* Hace el login de un usuario 
* 
* @Rest\View() 
* @Rest\Post("/user/login") 
* @RequestParam(name="mail", nullable=false, description="user email") 
* @RequestParam(name="password", nullable=false, description="user password") 
*/ 
public function userLoginAction(Request $request, ParamFetcher $paramFetcher) { 
    $mail = $paramFetcher->get('mail'); 
    $password = $paramFetcher->get("password"); 
    $response = []; 
    $userManager = $this->get('fos_user.user_manager'); 
    $factory = $this->get('security.encoder_factory'); 
    $user = $userManager->findUserByUsernameOrEmail($mail);   
    if (!$user) { 
     $response = [ 
      'error' => 1, 
      'data' => 'No existe ese usuario' 
     ]; 
    } else { 
     $encoder = $factory->getEncoder($user); 
     $ok = ($encoder->isPasswordValid($user->getPassword(),$password,$user->getSalt())); 

     if ($ok) { 
      $token = new UsernamePasswordToken($user, null, "main", $user->getRoles()); 
      $this->get("security.context")->setToken($token); 
      $event = new InteractiveLoginEvent($request, $token); 
      $this->get("event_dispatcher")->dispatch("security.interactive_login", $event); 
      if ($user->getType() == 'O4FUser') { 
       $url = $this->generateUrl('user_homepage'); 
      } else { 
       $url = $this->generateUrl('gym_user_homepage'); 
      } 
      $response = [ 
       'url' => $url 
      ]; 
     } else { 
      $response = [ 
       'error' => 1, 
       'data' => 'La contraseña no es correcta' 
      ]; 
     } 
    } 
    return $response; 
} 

ご覧のとおり、この関数はトークンを設定しており、すべて正常に機能します。

しかし、昨日、私はこのバンドルで提供さのようなもののためにJSONトークンを使用して、それはステートレスシステムを使用することが好ましい読んでされています:

https://github.com/lexik/LexikJWTAuthenticationBundle/blob/master/Resources/doc/index.md

だから私の質問は2のものですオプションが良いです。

ありがとうございます!

答えて

18

私はSymfony2とAngularを使って最近認証を行いましたが、これをやっている多くの研究の結果、API-Platform(JSON-LD/Hydraの新しいボキャブラリを使ってFOSRest私はあなたが使用すると思う)と角度のフロントアプリからrestangular。

ステートレスに関しては、これはより良い解決策ですが、ログインシナリオを構築して最高のテクノロジを選択する必要があります。

ログインシステムとJWTは互換性がなく、両方のソリューションを使用できます。 JWTに行く前に、私はOAuthで多くの研究を行いました。完全な開発チームを実装して要求するのは明らかに苦労しています。 JWTはこれを達成するための最良かつ簡単な方法を提供します。

最初にFOSUserバンドルを使用して@chalasrが示唆するように考慮する必要があります。 また、API-PlatformJWT Bundle from Lexikを使用して、あなたがすべきは、表示されるクロスドメインエラーのNelmioCorsが必要になります。

(これは慎重にバンドルのドキュメントを読む)

HTTPSプロトコルは、APIとフロントの間の通信に必須です!

次のコード例では、特定のエンティティマッピングを使用しました。 連絡先は、電話を持っている抽象的なCommunicationWaysを持っています。後ほど完全なマッピングとクラスの例を載せます)。

お客様のニーズに合わせて調整してください。

# composer.json 

// ... 
    "require": { 
     // ... 
     "friendsofsymfony/user-bundle": "[email protected]", 
     "lexik/jwt-authentication-bundle": "^1.4", 
     "nelmio/cors-bundle": "~1.4", 
     "dunglas/api-bundle": "[email protected]" 
// ... 


# app/AppKernel.php 

    public function registerBundles() 
    { 
     $bundles = array(
      // ... 
      new Symfony\Bundle\SecurityBundle\SecurityBundle(), 
      new FOS\UserBundle\FOSUserBundle(), 
      new Lexik\Bundle\JWTAuthenticationBundle\LexikJWTAuthenticationBundle(), 
      new Nelmio\CorsBundle\NelmioCorsBundle(), 
      new Dunglas\ApiBundle\DunglasApiBundle(), 
      // ... 
     ); 

次に、あなたの設定更新:次に

# src/AppBundle/Entity/User.php 

<?php 

namespace AppBundle\Entity; 

use Doctrine\ORM\Mapping as ORM; 
use FOS\UserBundle\Model\User as BaseUser; 

class User extends BaseUser 
{ 
    protected $id; 
    protected $username; 
    protected $email; 
    protected $plainPassword; 
    protected $enabled; 
    protected $roles; 
} 

# src/AppBundle/Resources/config/doctrine/User.orm.yml 

AppBundle\Entity\User: 
    type: entity 
    table: fos_user 
    id: 
     id: 
      type: integer 
      generator: 
       strategy: AUTO 

:ORMのYMLファイルでbaseUserを拡張するユーザークラスを作成します

parameters: 
    database_host:  127.0.0.1 
    database_port:  ~ 
    database_name:  symfony 
    database_user:  root 
    database_password: ~ 
    # You should uncomment this if you want use pdo_sqlite 
    # database_path: "%kernel.root_dir%/data.db3" 

    mailer_transport: smtp 
    mailer_host:  127.0.0.1 
    mailer_user:  ~ 
    mailer_password: ~ 

    jwt_private_key_path: %kernel.root_dir%/var/jwt/private.pem 
    jwt_public_key_path: %kernel.root_dir%/var/jwt/public.pem 
    jwt_key_pass_phrase : 'test' 
    jwt_token_ttl:  86400 

    cors_allow_origin: http://localhost:9000 

    api_name:   Your API name 
    api_description: The full description of your API 

    # A secret key that's used to generate certain security-related tokens 
    secret: ThisTokenIsNotSecretSoChangeIt 

# app/config/config.yml 

imports: 
    // ... 
    - { resource: security.yml } 
// ... 
framework: 
    // ... 
    csrf_protection: ~ 
    form: ~ 
    session: 
     handler_id: ~ 
    // ... 
fos_user: 
    db_driver: orm 
    firewall_name: main 
    user_class: AppBundle\Entity\User 
lexik_jwt_authentication: 
    private_key_path: %jwt_private_key_path% 
    public_key_path: %jwt_public_key_path% 
    pass_phrase:  %jwt_key_pass_phrase% 
    token_ttl:  %jwt_token_ttl% 
// ... 
dunglas_api: 
    title:  "%api_name%" 
    description: "%api_description%" 
    enable_fos_user: true 
nelmio_cors: 
    defaults: 
     allow_origin: ["%cors_allow_origin%"] 
     allow_methods: ["POST", "PUT", "GET", "DELETE", "OPTIONS"] 
     allow_headers: ["content-type", "authorization"] 
     expose_headers: ["link"] 
     max_age:  3600 
    paths: 
     '^/': ~ 
// ... 

とパラメータdistのファイルをput security.yml c onfig:

# app/config/security.yml 

security: 
    encoders: 
     FOS\UserBundle\Model\UserInterface: bcrypt 

    role_hierarchy: 
     ROLE_ADMIN:  ROLE_USER 
     ROLE_SUPER_ADMIN: ROLE_ADMIN 

    providers: 
     fos_userbundle: 
      id: fos_user.user_provider.username 

    firewalls: 
     dev: 
      pattern: ^/(_(profiler|wdt)|css|images|js)/ 
      security: false 

     api: 
      pattern: ^/api 
      stateless: true 
      lexik_jwt: 
       authorization_header: 
        enabled: true 
        prefix: Bearer 
       query_parameter: 
        enabled: true 
        name: bearer 
       throw_exceptions: false 
       create_entry_point: true 

     main: 
      pattern: ^/ 
      provider: fos_userbundle 
      stateless: true 
      form_login: 
       check_path: /login_check 
       username_parameter: username 
       password_parameter: password 
       success_handler: lexik_jwt_authentication.handler.authentication_success 
       failure_handler: lexik_jwt_authentication.handler.authentication_failure 
       require_previous_session: false 
      logout: true 
      anonymous: true 


    access_control: 
     - { path: ^/api, role: IS_AUTHENTICATED_FULLY } 

そしてservices.yml:

# app/config/services.yml 

services: 
    // ... 
    fos_user.doctrine_registry: 
     alias: doctrine 

をそして最後に、ルーティングファイル:この時点で

# app/config/routing.yml 

api: 
    resource: "." 
    type:  "api" 
    prefix: "/api" 

api_login_check: 
    path: "/login_check" 

、作曲更新、教義コンソールコマンドでデータベース/更新スキーマを作成し、 fosuserユーザを作成し、JWT Lexikバンドル(see doc)が必要とするSSLパブリックファイルとプライベートファイルを生成します。

あなたは今API呼び出しを送信したり、私たちは通常、ここでのSymfony APIの一部のために行われhttp://your_vhost/login_check

にPOSTリクエストを使用してトークンを生成する(例えばPOSTMANを使用して)ことができるはずです。あなたのテストをしてください!

ここで、apiはAngularからどのように処理されるのですか?

ここのは、私たちのシナリオを来る:セッション/のlocalStorageにトークンストア

  • ログインフォームThrought

    1. 、symfonyのlogin_check URLにPOSTリクエストを送信し、JSONウェブトークンを返すようヘッダーを呼び出してデータにアクセスするたびに、この格納されたトークンを渡します。

    ここは角の部分です:

    ヨーマンで

    $ npm install -g yo generator-angular bower 
    $ npm install -g ruby sass compass less 
    $ npm install -g grunt-cli karma-cli jshint node-gyp registry-url 
    

    打ち上げ角度のインストールを::

    まず、角グローバルモジュールがインストールされ必要としてきた

    • ...ガルプ..................:

      $ yo angular 
      

      回答は、質問をしました。 。ません

    • ...サス/コンパス...はい
    • ...ブートストラップ..........はい
    • ...ブートストラップ・Sass.はい

    、他のすべての尋ねたモジュールのチェックを外します。

    インストールローカルNPMパッケージ:

    $ npm install karma jasmine-core grunt-karma karma-jasmine --save-dev 
    $ npm install phantomjs phantomjs-prebuilt karma-phantomjs-launcher --save-dev 
    

    そして最後には、パッケージバウアー:

    $ bower install --save lodash#3.10.1 
    $ bower install --save restangular 
    

    オープンのindex.htmlファイルをし、以下のように設定します。

    # app/index.html 
    
    <!doctype html> 
    <html> 
        <head> 
        <meta charset="utf-8"> 
        <title></title> 
        <meta name="description" content=""> 
        <meta name="viewport" content="width=device-width"> 
        <link rel="stylesheet" href="styles/main.css"> 
        </head> 
        <body ng-app="angularApp"> 
        <div class="container"> 
        <div ng-include="'views/main.html'" ng-controller="MainCtrl"></div> 
        <div ui-view></div> 
    
        <script src="bower_components/jquery/dist/jquery.js"></script> 
        <script src="bower_components/angular/angular.js"></script> 
        <script src="bower_components/bootstrap-sass-official/assets/javascripts/bootstrap.js"></script> 
    
        <script src="bower_components/restangular/dist/restangular.js"></script> 
        <script src="bower_components/lodash/lodash.js"></script> 
    
        <script src="scripts/app.js"></script> 
        <script src="scripts/controllers/main.js"></script> 
        </body> 
    </html> 
    

    設定のrestangular:

    # app/scripts/app.js 
    
    'use strict'; 
    
    angular 
        .module('angularApp', ['restangular']) 
        .config(['RestangularProvider', function (RestangularProvider) { 
         // URL ENDPOINT TO SET HERE !!! 
         RestangularProvider.setBaseUrl('http://your_vhost/api'); 
    
         RestangularProvider.setRestangularFields({ 
          id: '@id' 
         }); 
         RestangularProvider.setSelfLinkAbsoluteUrl(false); 
    
         RestangularProvider.addResponseInterceptor(function (data, operation) { 
          function populateHref(data) { 
           if (data['@id']) { 
            data.href = data['@id'].substring(1); 
           } 
          } 
    
          populateHref(data); 
    
          if ('getList' === operation) { 
           var collectionResponse = data['hydra:member']; 
           collectionResponse.metadata = {}; 
    
           angular.forEach(data, function (value, key) { 
            if ('hydra:member' !== key) { 
             collectionResponse.metadata[key] = value; 
            } 
           }); 
    
           angular.forEach(collectionResponse, function (value) { 
            populateHref(value); 
           }); 
    
           return collectionResponse; 
          } 
    
          return data; 
         }); 
        }]) 
    ; 
    

    設定コントローラ:

    # app/scripts/controllers/main.js 
    
    'use strict'; 
    
    angular 
        .module('angularApp') 
        .controller('MainCtrl', function ($scope, $http, $window, Restangular) { 
         // fosuser user 
         $scope.user = {username: 'johndoe', password: 'test'}; 
    
         // var to display login success or related error 
         $scope.message = ''; 
    
         // In my example, we got contacts and phones 
         var contactApi = Restangular.all('contacts'); 
         var phoneApi = Restangular.all('telephones'); 
    
         // This function is launched when page is loaded or after login 
         function loadContacts() { 
          // get Contacts 
          contactApi.getList().then(function (contacts) { 
           $scope.contacts = contacts; 
          }); 
    
          // get Phones (throught abstrat CommunicationWays alias moyensComm) 
          phoneApi.getList().then(function (phone) { 
           $scope.phone = phone; 
          }); 
    
          // some vars set to default values 
          $scope.newContact = {}; 
          $scope.newPhone = {}; 
          $scope.contactSuccess = false; 
          $scope.phoneSuccess = false; 
          $scope.contactErrorTitle = false; 
          $scope.contactErrorDescription = false; 
          $scope.phoneErrorTitle = false; 
          $scope.phoneErrorDescription = false; 
    
          // contactForm handling 
          $scope.createContact = function (form) { 
           contactApi.post($scope.newContact).then(function() { 
            // load contacts & phones when a contact is added 
            loadContacts(); 
    
            // show success message 
            $scope.contactSuccess = true; 
            $scope.contactErrorTitle = false; 
            $scope.contactErrorDescription = false; 
    
            // re-init contact form 
            $scope.newContact = {}; 
            form.$setPristine(); 
    
            // manage error handling 
           }, function (response) { 
            $scope.contactSuccess = false; 
            $scope.contactErrorTitle = response.data['hydra:title']; 
            $scope.contactErrorDescription = response.data['hydra:description']; 
           }); 
          }; 
    
          // Exactly same thing as above, but for phones 
          $scope.createPhone = function (form) { 
           phoneApi.post($scope.newPhone).then(function() { 
            loadContacts(); 
    
            $scope.phoneSuccess = true; 
            $scope.phoneErrorTitle = false; 
            $scope.phoneErrorDescription = false; 
    
            $scope.newPhone = {}; 
            form.$setPristine(); 
           }, function (response) { 
            $scope.phoneSuccess = false; 
            $scope.phoneErrorTitle = response.data['hydra:title']; 
            $scope.phoneErrorDescription = response.data['hydra:description']; 
           }); 
          }; 
         } 
    
         // if a token exists in sessionStorage, we are authenticated ! 
         if ($window.sessionStorage.token) { 
          $scope.isAuthenticated = true; 
          loadContacts(); 
         } 
    
         // login form management 
         $scope.submit = function() { 
          // login check url to get token 
          $http({ 
           method: 'POST', 
           url: 'http://your_vhost/login_check', 
           headers: { 
            'Content-Type': 'application/x-www-form-urlencoded' 
           }, 
           data: $.param($scope.user) 
    
           // with success, we store token to sessionStorage 
          }).success(function(data) { 
           $window.sessionStorage.token = data.token; 
           $scope.message = 'Successful Authentication!'; 
           $scope.isAuthenticated = true; 
    
           // ... and we load data 
           loadContacts(); 
    
           // with error(s), we update message 
          }).error(function() { 
           $scope.message = 'Error: Invalid credentials'; 
           delete $window.sessionStorage.token; 
           $scope.isAuthenticated = false; 
          }); 
         }; 
    
         // logout management 
         $scope.logout = function() { 
          $scope.message = ''; 
          $scope.isAuthenticated = false; 
          delete $window.sessionStorage.token; 
         }; 
    
         // This factory intercepts every request and put token on headers 
        }).factory('authInterceptor', function($rootScope, $q, $window) { 
        return { 
         request: function (config) { 
          config.headers = config.headers || {}; 
    
          if ($window.sessionStorage.token) { 
           config.headers.Authorization = 'Bearer ' + $window.sessionStorage.token; 
          } 
          return config; 
         }, 
         response: function (response) { 
          if (response.status === 401) { 
           // if 401 unauthenticated 
          } 
          return response || $q.when(response); 
         } 
        }; 
    // call the factory ... 
    }).config(function ($httpProvider) { 
        $httpProvider.interceptors.push('authInterceptor'); 
    }); 
    

    そして最後に、我々は、フォームとのmain.htmlとファイルが必要になります。

    <!—Displays error or success messages--> 
    <span>{{message}}</span><br><br> 
    
    <!—Login/logout form--> 
    <form ng-show="!isAuthenticated" ng-submit="submit()"> 
        <label>Login Form:</label><br> 
        <input ng-model="user.username" type="text" name="user" placeholder="Username" disabled="true" /> 
        <input ng-model="user.password" type="password" name="pass" placeholder="Password" disabled="true" /> 
        <input type="submit" value="Login" /> 
    </form> 
    <div ng-show="isAuthenticated"> 
        <a ng-click="logout()" href="">Logout</a> 
    </div> 
    <div ui-view ng-show="isAuthenticated"></div> 
    <br><br> 
    
    <!—Displays contacts list--> 
    <h1 ng-show="isAuthenticated">Liste des Contacts</h1> 
    <article ng-repeat="contact in contacts" ng-show="isAuthenticated" id="{{ contact['@id'] }}" class="row marketing"> 
        <h2>{{ contact.nom }}</h2> 
        <!—Displays contact phones list--> 
        <h3 ng-repeat="moyenComm in contact.moyensComm">Tél : {{ moyenComm.numero }}</h3> 
    </article><hr> 
    
    <!—Create contact form--> 
    <form name="createContactForm" ng-submit="createContact(createContactForm)" ng-show="isAuthenticated" class="row marketing"> 
        <h2>Création d'un nouveau contact</h2> 
        <!—Displays error/success message on creating contact--> 
        <div ng-show="contactSuccess" class="alert alert-success" role="alert">Contact publié.</div> 
        <div ng-show="contactErrorTitle" class="alert alert-danger" role="alert"> 
         <b>{{ contactErrorTitle }}</b><br> 
         {{ contactErrorDescription }} 
        </div> 
        <div class="form-group"> 
         <input ng-model="newContact.nom" placeholder="Nom" class="form-control"> 
        </div> 
        <button type="submit" class="btn btn-primary">Submit</button> 
    </form> 
    
    <!—Phone form--> 
    <form name="createPhoneForm" ng-submit="createPhone(createPhoneForm)" ng-show="isAuthenticated" class="row marketing"> 
        <h2>Création d'un nouveau téléphone</h2> 
        <div ng-show="phoneSuccess" class="alert alert-success" role="alert">Téléphone publié.</div> 
        <div ng-show="phoneErrorTitle" class="alert alert-danger" role="alert"> 
         <b>{{ phoneErrorTitle }}</b><br> 
         {{ phoneErrorDescription }} 
        </div> 
        <div class="form-group"> 
         <input ng-model="newPhone.numero" placeholder="Numéro" class="form-control"> 
        </div> 
        <div class="form-group"> 
         <label for="contact">Contact</label> 
         <!—SelectBox de liste de contacts--> 
         <select ng-model="newPhone.contact" ng-options="contact['@id'] as contact.nom for contact in contacts" id="contact"></select> 
        </div> 
        <button type="submit" class="btn btn-primary">Submit</button> 
    </form> 
    

    まあ、私はそれが凝縮多くのコードだけど、あなたはすべての武器を持っていますSymfony &角を使用してフルAPIシステムを開始する。私はこれをより明確にし、この記事を何度か更新するために、ある日、ブログ投稿を作成します。

    私はそれが役に立ちそうです。

    よろしくお願いいたします。

    +0

    素晴らしい答え。ありがとう! – xger86x

    +0

    あなたは歓迎されています。投票を自由にしてください:) – NoX

    +0

    loooootありがとう!あなたは私を闇の中から連れて行った! – grogro

    3

    あなたがリンクしたバンドルは、あなたの現在のものより良い解決策です。
    これは、REST APIと従来のフォームベースのアプリケーションのセキュリティニーズの違いによるものです。

    Json Webトークンについては、jwt.ioの紹介をご覧ください。その後、非常にきれいで使いやすく、安全で強力なLexikJWTAuthenticationBundleを実装しようとする必要があります。

    JWTはより多くのセキュリティとすぐに使用できるログインプロセスを提供し、数行の設定だけが必要です。もちろん、ユーザープロバイダから検索/登録されたユーザーからトークンを簡単に管理、登録、作成することができます(私にとってはFOSUserBundleです)。

    JWTは、ユーザーを表す実際の署名です。私があなたに与えたリンクをもっと読む。

    また、JWTAuthenticationBundle Sandboxも参照してください。実際の例はAngularJSです。

    0

    以下のリポジトリを確認できます。 Symfony + Angular(FOSUser、NelmioApiDocBundle、単純なAuthなどのバンドルも含まれています)の基本的な設定と構成が含まれています。角度設定は、サーバー側のレンダリングをサポートします。 Symfony + Angularプロジェクトのデフォルトスケルトンとしてのいくつかの禁止作業https://github.com/vazgen/sa-standard-behttps://github.com/vazgen/sa-standard-fe