【Symfony2.3/AngularJS】FOSRestBundleとFOSUserBundleで構築するRESTログインAPI。

説明が皆無なのでかなり不親切です。超絶自分用まとめ。もしくはある程度解る人用。

「/」以下をフロント、「/admin」以下を管理画面(要ログイン)とする、よくあるアプリケーションの構築テンプレート。

□□サーバーサイド

■SecurityController.php

namespace Hoge\FugaBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Component\Security\Core\Authentication\Token\AnonymousToken;

use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;

use Hoge\FugaBundle\Entity\User;

/**
 * @Route("api")
 */
class SecurityController extends Controller
{
    protected function getUserManager()
    {
        return $this->get('fos_user.user_manager');
    }

    protected function loginUser(User $user)
    {
        $security = $this->get('security.context');
        $providerKey = $this->container->getParameter('fos_user.firewall_name');
        $roles = $user->getRoles();
        $token = new UsernamePasswordToken($user, null, $providerKey, $roles);
        $security->setToken($token);
    }

    protected function logoutUser()
    {
        $security = $this->get('security.context');
        $token = new AnonymousToken(null, new User());
        $security->setToken($token);
        $this->get('session')->invalidate();
    }
    
    protected function checkUser()
    {
        $security = $this->get('security.context');
        
        if ($token = $security->getToken()) {
            $user = $token->getUser();
            if ($user instanceof User) {
                return $user;
            }
        }
        
        return false;
    }

    protected function checkUserPassword(User $user, $password)
    {
        $factory = $this->get('security.encoder_factory');
        $encoder = $factory->getEncoder($user);
        if(!$encoder){
            return false;
        }
        return $encoder->isPasswordValid($user->getPassword(), $password, $user->getSalt());
    }

    /**
     * @Route("/login.{_format}", name="hoge_fuga_admin_api_security_login", defaults={"_format" = "json"})
     * @Method("POST")
     */
    public function loginAction()
    {
        $request = $this->getRequest();
        $username = $request->get('username');
        $password = $request->get('password');
        
        $um = $this->getUserManager();
        $user = $um->findUserByUsername($username);
        if(!$user){
            $user = $um->findUserByEmail($username);
        }
        
        if(!$user instanceof User){
            throw new NotFoundHttpException('ユーザーIDが存在しません');
        }
        if(!$this->checkUserPassword($user, $password)){
            throw new AccessDeniedException('パスワードが不正です');
        }
        
        $this->loginUser($user);
        return [
            'success' => true,
            'user' => $user
        ];
    }

    /**
     * @Route("/logout.{_format}", name="hoge_fuga_admin_api_security_logout", defaults={"_format" = "json"})
     * @Method("POST")
     */
    public function logoutAction()
    {
        $this->logoutUser();
        
        return [
            'success' => true
        ];
    }
    
    /**
     * @Route("/loginCheck.{_format}", name="hoge_fuga_admin_api_security_login_check", defaults={"_format" = "json"})
     * @Method("POST")
     */
    public function loginCheckAction()
    {
        if ($user = $this->checkUser()) {
            return [
                'success' => true,
                'user' => $user
            ];
        } else {
            throw new AccessDeniedException();
        }
    }
}

■app/config/config.yml

fos_user:
    db_driver: orm 
    firewall_name: admin_area
    user_class: Hoge\FugaBundle\Entity\User

fos_rest:
    view:
        view_response_listener: force
        force_redirects:
            html: true
        formats:
            jsonp: true
            json: true
            xml: true
            rss: false
        failed_validation: HTTP_BAD_REQUEST
        default_engine: twig

■app/config/security.yml

security:
    encoders:
        Symfony\Component\Security\Core\User\User: plaintext
        FOS\UserBundle\Model\UserInterface: sha512

    role_hierarchy:
        ROLE_ADMIN: ROLE_USER
        ROLE_SUPER_ADMIN: [ ROLE_USER, ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH ]

    providers:
        in_memory:
            memory:
                users:
                    user:  { password: userpass, roles: [ 'ROLE_USER' ] }
                    admin: { password: adminpass, roles: [ 'ROLE_ADMIN' ] }
        fos_userbundle:
            id: fos_user.user_provider.username_email

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

        admin_area:
            pattern: ^/admin
            anonymous: ~
            form_login:
                login_path: /admin/login
                check_path: /admin/login_check

    access_control:
        - { path: ^/admin/login$, roles: IS_AUTHENTICATED_ANONYMOUSLY }
        - { path: ^/admin/api/login.*$, roles: IS_AUTHENTICATED_ANONYMOUSLY }
        - { path: ^/admin, roles: [ ROLE_ADMIN ] }

■app/config/routing.yml
※FOSUserBundleのインストールに必要

fos_user:
    resource: "@FOSUserBundle/Resources/config/routing/all.xml"

■src/Hoge/FugaBundle/Resources/config/routing.yml

hoge_fuga_admin_index:
    resource: "@HogeFugaBundle/Controller/DefaultController.php"
    type:     annotation
    prefix:   /admin

hoge_fuga_admin_security:
    resource: "@HogeFugaBundle/Controller/SecurityController.php"
    type:     annotation
    prefix:   /admin

■DefaultController.php

namespace Hoge\FugaBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;

use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;

class DefaultController extends Controller
{
    
    /**
     * @Route("")
     * @Route("/{_all}", name="affiliate_cms_admin_index_all", requirements={"_all" = "^(?!.*(api)).*$"})
     * @Method("GET")
     */
    public function indexAction()
    {
        return $this->render('HogeFugaBundle:Default:index.html.twig');
    }
}

「api」を含まないURLを全てDefaultControllerのIndexActionで拾い、あとはAngularのルーターに任せる。

□□フロントサイド

■adminLoginController.js(ログイン画面で用いるコントローラー)

void function() {
    
    var adminLoginController = function(securityResource, $state, UserService) {
        
        var that = this;
        
        this.data = {};
        
        this.loginFailure = false;
        
        this.login = function() {
            securityResource.save({action: 'login.json'}, {
                username: that.data.username,
                password: that.data.password
            }, function(response) {
                UserService.setUser(response.user);
                $state.go('admin.main.dashboard');
            }, function(response) {
                if (response.status >= 400) {
                    that.loginFailure = true;
                }
            });
        }
    }

    angular
        .module('hogeControllers')
        .controller('adminLoginController', ['securityResource', '$state', 'UserService', adminLoginController])
    ;
    
}();

■adminController.js(ログイン画面を含むログイン後の全てのコントローラーが継承している基底コントローラー)

void function() {
    
    var adminController = function(securityResource, $state, UserService) {

        this.user = null;
        
        var that = this;
        
        this.logout = function() {
            securityResource.save({action: 'logout.json'}, {}, function() {
                UserService.clear();
                $state.go('admin.login');
            });
        }
        
        this.initUser = function() {
            that.user = UserService.getUser();
            if (!that.user) {
                securityResource.save({action: 'loginCheck.json'}, {}, function(response) {
                    that.user = response.user;
                }, function() {
                    UserService.clear();
                    $state.go('admin.login');
                });
            }
        }
    }
    
    angular
        .module('hogeControllers')
        .controller('adminController', ['securityResource', '$state', 'UserService', adminController])
    ;
    
}();

■SecurityResource.js

void function() {
    
    angular
        .module('hogeResources')
        .factory('securityResource', ['$resource', function($resource) {
            return $resource('/admin/api/:action', {
                action: '@action',
            }, {
            }
        )}])
        ;
    
}();

■UserService.js

void function() {
    
    var UserService = function() {
        
        this.user = null;
        
        this.getUser = function() {
            return this.user;
        }
        
        this.setUser = function(user) {
            this.user = user;
        }
        
        this.clear = function() {
            this.user = null;
        }
        
    }
    
    angular
        .module('hogeServices')
        .service('UserService', [UserService])
    ;
    
}();

■app.js

void function() {
    
    angular.module('hogeApp', [
        // コア
        'ngAnimate',
        'ngResource',
        'ngSanitize',
        'ngTouch',
        
        // 必要に応じて
        'ui',
        'ui.utils',
        'ui.router',
        'ui.bootstrap',
        'ui.bootstrap.datetimepicker',
        'kendo.directives',
        'ngCookies',
        'angular-loading-bar',
        'angularFileUpload',
        'cgNotify',
        
        // 自分のやつら
        'hogeConstants',
        'hogeControllers',
        'hogeDirectives',
        'hogeFactories',
        'hogeFilters',
        'hogeResources',
        'hogeServices'
    ]);
    angular.module('hogeConstants', []);
    angular.module('hogeControllers', []);
    angular.module('hogeDirectives', []);
    angular.module('hogeFactories', []);
    angular.module('hogeFilters', []);
    angular.module('hogeResources', []);
    angular.module('hogeServices', []);
    
    /**
     * config section
     */
    angular
        .module('hogeApp')
        .config(['$urlRouterProvider', '$stateProvider', '$locationProvider',
            function($urlRouterProvider, $stateProvider, $locationProvider) {
            $stateProvider
                
                /**
                 * admin
                 */
                .state('admin', {
                    url: '/admin',
                    views: {
                        main: {
                            templateUrl: '/admin.html',
                            controller: 'adminController',
                            controllerAs: 'admin'
                        }
                    }
                })
                
                /**
                 * admin login
                 */
                .state('admin.login', {
                    url: '/login',
                    views: {
                        main: {
                            templateUrl: '/login.html',
                            controller: 'adminLoginController',
                            controllerAs: 'adminLogin'
                        }
                    }
                })
                
                /**
                 * admin main (base template)
                 */
                .state('admin.main', {
                    url: '/',
                    views: {
                        main: {
                            templateUrl: '/main.html',
                            controller: 'adminMainController',
                            controllerAs: 'adminMain'
                        }
                    }
                })
                
                /**
                 * admin main dashboard
                 */
                .state('admin.main.dashboard', {
                    url: 'dashboard',
                    views: {
                        main: {
                            templateUrl: '/dashboard/index.html',
                            controller: 'adminMainDashboardController',
                            controllerAs: 'adminMainDashboard'
                        }
                    }
                })
            ;
            
            $urlRouterProvider.otherwise('/admin/dashboard');
            
            $locationProvider.html5Mode(true).hashPrefix('!');
            
        }]);
    
}();

こんな感じでベースが完成。

めでたしめでたし。