Symfony2 and Angular. User authentication

后端 未结 3 1458
滥情空心
滥情空心 2021-02-04 17:40

I am developing a web application that involves Symfony2 and AngularJs. I have a question about the right way of authenticate users in the site.

I have built a function

3条回答
  •  别那么骄傲
    2021-02-04 17:56

    As I done recently an authentication implementation with Symfony2 and Angular, and after a lot of research doing this the best way I finally chosen API-Platform (that uses JSON-LD / Hydra new vocabulary to provide REST-API, instead of FOSRest that I suppose you use) and restangular from Angular front app.

    Regarding stateless, it's true it's a better solution but you have to build up your login scenario to choose the best technology.

    Login system and JWT is not incompatible together and both solutions could be used. Before going with JWT, I made a lot of research with OAuth and it's clearly a pain to implements and require a full developers team. JWT offers best and simple way to achieve this.

    You should consider first using FOSUser bundle as @chalasr suggests. Also, using API-Platform and JWT Bundle from Lexik and you will need NelmioCors for CrossDomain errors that should appears :

    (Read docs of this bundles carefully)

    HTTPS protocol is MANDATORY to communicate between api and front !

    In the following example code, I used a specific entities mapping. Contact Entity got abstract CommunicationWays which got Phones. I'll put full mapping and class examples later).

    Adapt following your needs.

    # composer.json
    
    // ...
        "require": {
            // ...
            "friendsofsymfony/user-bundle": "~2.0@dev",
            "lexik/jwt-authentication-bundle": "^1.4",
            "nelmio/cors-bundle": "~1.4",
            "dunglas/api-bundle": "~1.1@beta"
    // ...
    
    
    # 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(),
                // ...
            );
    

    Then update your 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:
            '^/': ~
    // ...
    

    And parameters dist file :

    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
    

    Create user class that extends baseUser with ORM yml file :

    # src/AppBundle/Entity/User.php
    
    

    Then put security.yml config :

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

    And services.yml :

    # app/config/services.yml
    
    services:
        // ...
        fos_user.doctrine_registry:
            alias: doctrine
    

    And finally routing file :

    # app/config/routing.yml
    
    api:
        resource: "."
        type:     "api"
        prefix: "/api"
    
    api_login_check:
        path: "/login_check"
    

    At this point, composer update, create database / update schema with doctrine console commands, create a fosuser user and generate SSL public and private files required by JWT Lexik bundle (see doc).

    You should be able (using POSTMAN for example) to send api calls now or generate a token using a post request to http://your_vhost/login_check

    We are done for Symfony api part normally here. Do your tests !

    Now, how the api will be handled from Angular ?

    Here's come our scenario :

    1. Throught a login form, send a POST request to Symfony login_check url, that will return a JSON Web Token
    2. Store that token in session / localstorage
    3. Pass this stored token in every api calls we make to headers and access our data

    Here is the angular part :

    First have required angular global modules installed :

    $ 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
    

    Launch angular installation with yeoman :

    $ yo angular
    

    Answer asked questions :

    • … Gulp……………….. No
    • … Sass/Compass… Yes
    • … Bootstrap………. Yes
    • … Bootstrap-Sass. Yes

    and uncheck all other asked modules.

    Install local npm packages :

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

    And finally bower packages :

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

    Open index.html file and set it as follow :

    # app/index.html
    
    
    
      
        
        
        
        
        
      
      
        

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

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

    And finally we need our main.html file with forms :

    
    {{message}}




    Liste des Contacts

    {{ contact.nom }}

    Tél : {{ moyenComm.numero }}


    Création d'un nouveau contact

    Création d'un nouveau téléphone

    Well, I know it's a lot of condensed code, but you have all weapons to kick-off a full api system using Symfony & Angular here. I'll make a blog post one day for this to be more clear and update this post some times.

    I just hope it helps.

    Best Regards.

提交回复
热议问题