【问题标题】:Symfony2 and Angular. User authenticationSymfony2 和 Angular。用户认证
【发布时间】:2016-02-11 16:18:22
【问题描述】:

我正在开发一个涉及 Symfony2 和 AngularJs 的 Web 应用程序。我对在站点中验证用户的正确方式有疑问。

我在我的 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

所以我的问题是这两个选项中哪个更好。

谢谢!

【问题讨论】:

    标签: angularjs symfony


    【解决方案1】:

    正如我最近使用 Symfony2 和 Angular 完成的身份验证实现一样,经过大量研究后,我最终选择了API-Platform(使用 JSON-LD / Hydra 新词汇来提供 REST-API,而不是我想你使用的 FOSRest)和来自 Angular 前端应用程序的 restangular。

    关于无状态,确实它是一个更好的解决方案,但您必须构建您的登录场景以选择最佳技术。

    登录系统和 JWT 并非不兼容,两种解决方案都可以使用。在使用 JWT 之前,我对 OAuth 进行了大量研究,显然实施起来很痛苦,并且需要一个完整的开发团队。 JWT 提供了实现这一目标的最佳且简单的方法。

    您应该首先考虑使用@chalasr 建议的FOSUser 捆绑包。 此外,使用API-PlatformJWT Bundle from Lexik,您将需要NelmioCors 来处理应该出现的CrossDomain 错误:

    (仔细阅读此捆绑包的文档)

    HTTPS 协议是 api 和 front 之间通信的强制协议!

    在下面的示例代码中,我使用了特定的实体映射。 联系实体得到了抽象的通信方式,它得到了电话。稍后我会放完整的映射和类示例)。

    根据您的需要进行调整。

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

    然后更新你的配置:

    # 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文件:

    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
    

    使用 ORM yml 文件创建扩展 baseUser 的用户类:

    # 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
    

    然后把 security.yml 配置:

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

    此时,composer 更新,使用教义控制台命令创建数据库/更新架构,创建 fosuser 用户并生成 JWT Lexik 捆绑包 (see doc) 所需的 SSL 公有和私有文件。

    您现在应该能够(例如使用 POSTMAN)发送 api 调用或使用对 http://your_vhost/login_check 的 post 请求生成令牌

    我们通常在这里完成 Symfony api 部分。做你的测试!

    现在,如何从 Angular 处理 api?

    我们的场景来了:

    1. 通过登录表单,向 Symfony login_check url 发送 POST 请求,这将返回 JSON Web Token
    2. 将该令牌存储在会话/本地存储中
    3. 在我们对标头进行的每个 api 调用中传递这个存储的令牌并访问我们的数据

    这是角部分:

    首先安装了必需的角度全局模块:

    $ 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
    

    使用 yeoman 启动 Angular 安装:

    $ yo angular
    

    回答提问:

    • ……咕噜………………..不
    • …萨斯/罗盘… 是
    • ……引导程序…………. 是
    • … Bootstrap-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 和 Angular 启动一个完整的 api 系统。有一天我会写一篇博文,以便更清楚地说明这一点,并偶尔更新这篇博文。

    我只是希望它有所帮助。

    最好的问候。

    【讨论】:

    • 很棒的答案。谢谢!
    • 非常感谢!你带我走出黑暗!
    【解决方案2】:

    您链接的捆绑包比您当前的解决方案更好。
    这是因为 REST Api 和基于表单的经典应用程序的安全需求之间存在差异。

    查看jwt.io Json Web Token 的介绍,之后,你应该尝试实现非常干净、易于使用、安全且强大的 LexikJWTAuthenticationBundle。

    JWT 将提供更高的安全性和即用型登录过程,只需要几行配置。当然,您可以轻松地从您的用户提供商检索/注册的用户中管理、注册和创建令牌(对我来说是FOSUserBundle)。

    JWT 是代表您的用户的真实签名。在我给你的链接中阅读更多内容。

    另请参阅JWTAuthenticationBundle Sandbox,了解 AngularJS 的真实示例。

    【讨论】:

      【解决方案3】:

      您可以检查以下存储库。它包含 Symfony + Angular 的基本设置和配置(它还包含一些包,如 FOSUser、NelmioApiDocBundle 以及简单的 Auth)。 Angular 设置支持服务器端渲染。一些禁令作为 Symfony + Angular 项目 https://github.com/vazgen/sa-standard-behttps://github.com/vazgen/sa-standard-fe 的默认框架

      【讨论】:

        猜你喜欢
        • 2015-07-07
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2011-12-27
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2015-11-15
        相关资源
        最近更新 更多