2016-02-11 8 views
6

Sto sviluppando un'applicazione Web che coinvolge Symfony2 e AngularJs. Ho una domanda sul modo corretto di autenticare gli utenti nel sito.Symfony2 e Angular. Autenticazione utente

Ho costruito una funzione nel mio API REST (costruito in Symfony) che autentica un utente attraverso i parametri passati nella richiesta.

/** 
* 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; 
} 

Come si può vedere, la funzione imposta il token e tutto funziona correttamente.

Ma ieri, ho letto che è preferibile utilizzare un sistema di apolide, utilizzando per questo un JSON Token come la fornita da questo bundle:

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

Quindi la mia domanda è quello che dei due le opzioni sono migliori

Grazie!

risposta

18

Come ho fatto di recente un'implementazione dell'autenticazione con Symfony2 e Angular, e dopo molte ricerche effettuate nel modo migliore ho finalmente scelto API-Platform (che utilizza il nuovo vocabolario JSON-LD/Hydra per fornire REST-API, invece di FOSRest che suppongo tu usi) e restangular dall'app Angular front.

Per quanto riguarda gli apolidi, è vero che è una soluzione migliore ma è necessario creare uno scenario di accesso per scegliere la migliore tecnologia.

Il sistema di accesso e JWT non è incompatibile insieme ed entrambe le soluzioni potrebbero essere utilizzate. Prima di andare con JWT, ho fatto molte ricerche con OAuth ed è chiaramente un problema per implementare e richiedere un team di sviluppatori completo. JWT offre il modo migliore e più semplice per raggiungere questo obiettivo.

È consigliabile considerare prima l'utilizzo del pacchetto FOSUser come suggerisce @chalasr. Inoltre, utilizzando API-Platform e JWT Bundle from Lexik e sarà necessario NelmioCors per errori crossdomain che dovrebbe appare:

(Leggere documenti di questo bundle con attenzione)

protocollo HTTPS è OBBLIGATORIO per comunicare tra api e frontale!

Nel seguente codice di esempio, ho utilizzato una mappatura di entità specifiche. Contact Entity ha ottenuto CommunicationWays astratti che hanno ottenuto Phones. Metterò la mappatura completa e gli esempi di classe più tardi).

Adattarsi in base alle proprie esigenze.

# 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(), 
      // ... 
     ); 

Poi aggiornare il config:

# 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: 
     '^/': ~ 
// ... 

e parametri del file dist:

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 

Creare classe utente che si estende baseUser con il file ORM yml:

# 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 

Poi metti 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 } 

E services.yml:

# app/config/services.yml 

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

e il file di routing, infine:

# app/config/routing.yml 

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

api_login_check: 
    path: "/login_check" 

A questo punto, l'aggiornamento compositore, creare lo schema del database/aggiornamento con i comandi della console dottrina, creare un utente fosuser e generare file SSL pubblici e privati ​​richiesti dal pacchetto JWT Lexik (see doc).

Dovreste essere in grado (usando POSTINO per esempio) per inviare le chiamate API ora o generare un token utilizzando una richiesta POST a http://your_vhost/login_check

Abbiamo finito per parte Symfony api normalmente qui. Fai i tuoi test!

Ora, come verrà gestita l'api da Angular?

Qui è venuto il nostro scenario:

  1. Throught un form di login, inviare una richiesta POST per Symfony login_check url, che restituirà un JSON Web Token
  2. Conservare quella pedina in sessione/localStorage
  3. Passa questo token memorizzato in tutte le chiamate API alle intestazioni e accedi ai nostri dati

Ecco la parte angolare:

Prima hanno richiesto moduli globali angolari installati:

$ 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 

lancio installazione angolare con Yeoman:

$ yo angular 

risposta domande:

  • ... Gulp ................... . No
  • ... Sass/bussola ...  Si
  • ... Bootstrap ......... . Sì
  • ... Bootstrap-Sass. Sì

e deselezionare tutti gli altri moduli chiesto.

Installare pacchetti NPM locali:

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

E infine bower pacchetti:

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

Aprire il file index.html e impostarlo come segue:

# 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> 

Configura 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; 
     }); 
    }]) 
; 

configurare il controller:

# 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'); 
}); 

E infine abbiamo bisogno il nostro file main.html con forme:

<!—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> 

Beh, so che è un sacco di codice condensato, ma avete tutte le armi per avviare un sistema API completo utilizzando Symfony & Angolare qui. Farò un post sul blog un giorno per questo per essere più chiaro e aggiornare questo post alcune volte.

Spero solo che sia d'aiuto.

I migliori saluti.

+0

risposta stupenda. Grazie! – xger86x

+0

Prego, sentitevi liberi di votare :) – NoX

+0

Grazie mille! Mi hai portato fuori dall'oscurità! – grogro

3

Il pacchetto che hai collegato è una soluzione migliore della tua attuale.
È a causa delle differenze tra le esigenze di sicurezza di AEST REST e una classica applicazione basata su moduli.

Osservare l'introduzione al token JSON Web jwt.io, e dopo, si dovrebbe provare ad implementare il LexikJWTAuthenticationBundle che è molto pulito, facile da usare, sicuro e potente.

JWT fornirà più sicurezza e un processo di accesso pronto all'uso, richiede solo poche righe di configurazione. Naturalmente, puoi facilmente gestire, registrare e creare token da utenti recuperati/registrati dal tuo fornitore di servizi (per me è lo FOSUserBundle).

Un JWT è una firma reale che rappresenta il tuo utente. Leggi di più nel link che ti ho dato.

Vedere anche questo JWTAuthenticationBundle Sandbox per un esempio reale con AngularJS.

0

È possibile controllare i seguenti repository. Contiene setup e configurazione di base per Symfony + Angular (contiene anche alcuni bundle come FOSUser, NelmioApiDocBundle e pure Auth). Il setup angolare supporta il rendering lato server. alcuni ban funzionano come scheletro predefinito per Symfony + Progetti angolari https://github.com/vazgen/sa-standard-be e https://github.com/vazgen/sa-standard-fe