2011年11月4日金曜日

[symfony2]認証(ファイアーウォール )と承認(アクセス制御)

日本Symfonyユーザー会のsymfony2ガイドブックのセキュリティデータベースと Doctrine (“The Model”)辺りを読みながらユーザー認証とアクセス制御の仕組みを作ってみました。
それだけでは難しかったので、他にもVOYAGE GROUPさんのブログにもお世話になりました。感謝感謝です。

作りたいものの整理

今回、作りたかったのは
  1. /loginでログイン
  2. /logoutでログアウト
  3. ログインフォームでログイン
  4. データベースからユーザーを取得
  5. 簡単にアクセス出来るように、security.ymlでの設定
  6. アクセス制御はsecurity.yml
  7. 「/」はアクセスフリー、「page/home」はユーザー権限でログインした時にアクセス可。

はまった箇所

何箇所かハマりました。
  1. security.ymlで「anonymous: ~」の「~」は多分tureの意
  2. 「UserInterface」でオーバーライドするメソッド
  3. Userテーブルとは別にRoleテーブルを作る

作業手順

最終的にはこの順序で作れば良い?という手順。
1.securityBundleを作る。
php app/console generate:bundle --namespace=Acme/SecurityBundle

取り敢えず何はともあれSecurityBundleを作る。ガイドブックには特に解説がありませんが、Symfonyのお作法的にはSecurityBundleを作ってその中に認証と承認のコードを書くのが良いみたいです。この時formatはアノテーションを選択します。これはデータベースの設定にアノテーションを使って説明が多いからです。.ymlでも指定できるならそっちでもいいのですが。

2.security.ymlを設定する。
ガイドブックだと、security.ymlとconfig.ymlでごちゃ混ぜになってますが、どうやらsecurity.ymlに書くのが正しいようです。
# app/config/security.yml
security:
 encoders:
  main1:
   class: Symfony\Component\Security\Core\User\User
   algorithm: plaintext
  main2:
   class: Acme\SecutityBundle\Entity\User
   algorithm: sha1
   iterations: 1
   encode_as_base64: false

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

 providers:
  chain_provider:
   providers: [in_memory,user_db]
  in_memory:
   users:
    user: { password: userpass, roles: [ 'ROLE_USER' ] }
    admin: { password: adminpass, roles: [ 'ROLE_ADMIN' ] }
  user_db:
   entity: { class: Acme\SecurityBundle\Entity\User, property: username}

 firewalls:
  login:
   pattern: ^/login$
   security: false

  secured_area:
   pattern: ^/
   anonymous: ~
   form_login:
    login_path: /login
    check_path: /login_check
   logout:
    path: /logout
    target: /

 access_control:
  - { path: ^/admin/user, role: ROLE_ADMIN }
  - { path: ^/admin/role, role: ROLE_ADMIN }
  - { path: ^/page/home, role: ROLE_USER }
  - { path: ^/page/information, role: ROLE_USER }

encodersはパスワード暗号化の為の設定で、上記の様に複数指定出来るようです。
role_hierarchyは権限の階層。
providersは要はユーザー情報の設定の事で、security.ymlに直接書く設定とデータベースから引っ張ってくる方法を設定しています。
firewallsは認証の設定。
firewallsの「anonymous: ~」とすると、認証はするけれどもアクセス制御が空なら全部見れるよっていう設定っぽい。
access_controlはアクセス制御の設定。

3.ルーティングの設定
ログイン、ログアウトのルーティング設定をします。

# app/config/routing.yml
login:
 pattern: /login
 defaults: { _controller: AcmeSecurityBundle:Security:login }

login_check:
 pattern: /login_check

logout:
 pattern: /logout

ログイン、ログインチェック、ログアウトのルーティングを設定します。ログインチェックとログアウトは特にアクション、テンプレートを用意する必要はないそうです。


4.コントローラーを作る

# src/Acme/SecurityBundle/Controller/SecurityController.php
namespace Acme\SecurityBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\Security\Core\SecurityContext;

class SecurityController extends Controller
{
 public function loginAction()
 {
  $request = $this->getRequest();
  $session = $request->getSession();

  // ログインエラーがあれば、ここで取得
  if ($request->attributes->has(SecurityContext::AUTHENTICATION_ERROR)) {
   $error = $request->attributes->get(SecurityContext::AUTHENTICATION_ERROR);
  } else {
   $error = $session->get(SecurityContext::AUTHENTICATION_ERROR);
  }

  return $this->render('AcmeSecurityBundle:Security:login.html.twig', array(
   // ユーザによって前回入力された username
   'last_username' => $session->get(SecurityContext::LAST_USERNAME),
   'error' => $error,
  ));
 }
}


5.ログインフォームのテンプレートを作る

# src/Acme/SecurityBundle/Resources/views/Security/login.html.twig
{% if error %}
 <div>{{ error.message }}</div>
{% endif %}

<form action="{{ path('login_check') }}" method="post">
 <label for="username">Username:</label>
 <input type="text" id="username" name="_username" value="{{ last_username }}" />

 <label for="password">Password:</label>
 <input type="password" id="password" name="_password" />

 {#
  認証成功した際のリダイレクト URL を制御したい場合(詳細は以下に説明する)
  <input type="hidden" name="_target_path" value="/account" />
 #}

 <input type="submit" name="login" />
</form>


6.Userエンティティを作る。
取り敢えず「UserInterface」は無視する。メソッドをオーバーライドしておかないと、コンソールが動かないので。
# src/Acme/SecurityBundle/Entity/User.php
namespace Acme\SecurityBundle\Entity;
use Symfony\Component\Security\Core\User\UserInterface;
use Doctrine\ORM\Mapping as ORM;
/**
* @orm\Entity(repositoryClass="Acme\SecurityBundle\Repository\UserRepository")
* @orm\Table(name="user")
*/
class User //implements UserInterface
{
  /**
  * @orm\Id
  * @orm\Column(type="integer")
  * @orm\GeneratedValue(strategy="AUTO")
  */
 protected $id;
 /**
  * @orm\Column(name="username",unique="true")
  */
 protected $username;
 /**
  * @orm\Column(name="password",length="255",)
  */
 protected $password;
  /**
  * @orm\Column(name="salt", length="16")
  */
 protected $salt;
  /**
  * @orm\Column(name="role_id", type="integer", nullable="true")
  */
 protected $role;

}

$roleはユーザーのロールエンティティが入るイメージだと思います。

ドクトリンにゲッター、セッターを作ってもらいます。
php app/console doctrine:generate:entities Acme/SecurityBundle/Entity/User

「UserInterface」をインプリメントして以下のメソッドを追加。
# src/Acme/SecurityBundle/Entity/User.php
...
 public function getRoles(){
  return array($this->role->getName());
 }
 public function eraseCredentials(){
 }
 public function equals(UserInterface $user){
  return $this->getUsername() == $user->getUsername();
 }

テーブルは後でまとめて作るのでここではスルーする。

7.Roleエンティティを作る

# src/Acme/SecurityBundle/Entity/Role.php
namespace Acme\SecurityBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
/**
* @orm\Entity(repositoryClass="Acme\SecurityBundle\Repository\RoleRepository")
* @orm\Table(name="role")
*/
class Role
{
 /**
  * @orm\Id
  * @orm\Column(type="integer")
  * @orm\GeneratedValue(strategy="AUTO")
  */
 protected $id;
 /**
  * @orm\Column(name="name", unique="true", nullable="true")
  */
 protected $name;
 /**
  * @ORM\Column(name="description", type="text")
  */
 protected $description;

 /**
  * @ORM\OneToMany(targetEntity="User", mappedBy="role")
  */
 protected $users;

 public function __construct()
 {
  $this->users = new ArrayCollection();
 }
}


ドクトリンにゲッター、セッターを作ってもらいます。
php app/console doctrine:generate:entities Acme/SecurityBundle/Entity/Role


8.データベースにテーブルを作る
php app/console doctrine:schema:update --force


9.レポジトリを作る

php app/console doctrine:generate:entities Acme

ユーザーレポジトリは「UserProviderInterface」を「implements」する必要があるので、追加します。
最終的には以下のようになってます。
namespace Acme\SecurityBundle\Repository;

use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use Doctrine\ORM\EntityRepository;
use Acme\SecurityBundle\Entity\User;

/**
* UserRepository
*
* This class was generated by the Doctrine ORM. Add your own custom
* repository methods below.
*/
class UserRepository extends EntityRepository implements UserProviderInterface
{
 public function loadUserByUsername($username)
 {
  return $this->findOneBy(array('username' => $username));
 }
 public function supportsClass($class)
 {
  return true;
 }
 public function refreshUser(UserInterface $user)
 {
  if(!$user->getUsername()){
   throw new Exception(sprintf('Intances of "%s" are not supported.', get_class($user)));
  }
   return $this->loadUserByUsername($user->getUsername());
 }
}


一応これでブラウザでアクセスするとそれっぽい事になっています。
次回はこれに続いてUserの管理する辺りを作っていきます。

0 件のコメント:

コメントを投稿