VOYAGE GROUP エンジニアブログ

voyagegroup_techのブログ
VOYAGE GROUPエンジニアブログです。

2011年09月

Symfony2のススメ2 ~認証とともに~

こんにちは、底辺プログラマの高橋です。

今回で最後になりますが、Symfony2でデータベースを利用した場合のユーザ認証の実装について触れていきたいと思います。

(6)認証設定(doctrine利用の認証の場合)
「Symfony2のススメ1」(5)の記述から「encoders」と「providers」の項目を変更します。
「chain_provider」を指定する事で前回の設定を行った「user」アカウントでの設定を有効にしつつ追加設定を行っています。
このように複数のエンコーダー、プロバイダーを定義できる点 もこの認証機能のすぐれた点となります。

$ vi app/config/security.yml
security:
    encoders:
main1:
class: Symfony\Component\Security\Core\User\User
algorithm: plaintext
        main2:
            class:     Acme\SecurityBundle\Entity\User
            algorithm: sha1
    providers:
chain_provider:
providers: [in_memory, main]
in_memory:
users:
user: { password: userpass, roles: [ 'ROLE_USER' ] }
admin: { password: adminpass, roles: [ 'ROLE_ADMIN' ] }
        main:
            entity:
                class: Acme\SecurityBundle\Entity\User
                property: username

(7)エンティティクラスの作成
symfony1系だと「lib/model」の下にモデルクラスが作成されていましたが、
Symfony2系はバンドルディレクトリの「Entity」下に作成する事になると思います。(※1)
設定ファイルからのクラスの自動生成も可能ですが、今回は手動で以下のファイルを作成します。

$ vi src/Acme/SecurityBundle/Entity/User.php
<?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", nullable="true")
     */
    protected $username;
    /**
     * @orm\Column(name="password",length="128")
     */
    protected $password;
     /**
      * @orm\Column(name="salt", length="16")
      */
    protected $salt;
    public function getRoles(){
        return array("ROLE_USER");
    }
    public function getPassword(){
        return $this->password;
    }
    public function getSalt(){
        return $this->salt;
    }
    public function getUsername(){
        return $this->username;
    }
    public function eraseCredentials(){
    }
    public function equals(UserInterface $user){
        return $this->getUsername() == $user->getUsername();
    }
}

コマンドを実行するとコメントの定義を元にセッター/ゲッターメソッドを「User.php」に自動生成してくれます。
コメントの「@orm~」の部分がデータベースの定義の記述となります。 

php app/console doctrine:generate:entities AcmeSecurityBundle:User
下記コマンドで
コメントの定義を元にデータベースにテーブルを作成します。
php app/console doctrine:schema:update --force

ユーザ管理に利用するテーブルのエンティティクラスは「UserInterface」をimplementsする必要があります。
そのため、Interfaceに定義されている下記メソッドを実装しています。

getRoles():      # ユーザの権限を返す
getPassword():   # ユーザのパスワードを返す
getSalt():       # ユーザのサルトを返す
getUsername():   # ユーザを識別するIDを返す
eraseCredentials(): # 重要なデータが含まれている場合の削除処理
equals():        # 同じユーザであるとの判定処理

(8)レポジトリクラスの作成
Symfony1系では「lib/model」の下に自動生成されていた「XXXXTable.class.php」(doctrineの場合)
「XXXXXPeer.php」(Propelの場合)と同じような用途のクラスとなります。(※1)
このクラスも自分で作成する必要があります。

$ vi src/Acme/SecurityBundle/Repository/UserRepository.php
<?php
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;

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('Instances of "%s" are not supported.', get_class($user)));
        }
        return $this->loadUserByUsername($user->getUsername());
    }
}
「UserProviderInterface」をimplementsする必要があるので、その実装をしています。
loadUserByUsername($username)     # usernameが与えられた時のユーザのロード処理
supportsClass($class)             # classの型の妥当性
refreshUser(UserInterface $user)  # ユーザのリフレッシュ処理

以上でユーザ認証する処理の実装は完了となります。
ユーザを作成する処理の実装については割愛します。Userテーブルに以下SQLでデータを入れると「hoge/hoge」でログインできるようになります
(今回の設定だと「sha1→base64」のストレッチングをデフォルトで5000回行なうのでこの値となります)

insert into user(id, username, password, salt) values(1, 'hoge', 'E5GwPYtO3mEBCJ3mTVn2VTu8p40=', '');


※1 エンティティとリポジトリについて
エンティティ/リポジトリという用語が登場しますが、このフレーズそのものはPofEAAやDDDで
使用されているパターン名になります。
自分の理解だと(怪しいですがw)下のような意味を持つようです。
エンティティは同一性をもったオブジェクトで、同一性とは5歳の自分と20歳の自分は属性情報が異なるが
同一性を持っておりエンティティとなる。逆に同じ属性情報をもっていても同一性がないと値オブジェクトとなる。
リポジトリはオブジェクトにアクセスするため手段で、データへのアクセス手段をカプセル化してドメイン-モデルレイヤ間の明確な分離を行う。
DDDのエッセンス:http://www.ogis-ri.co.jp/otc/hiroba/technical/DDDEssence/chap2.html
Repository(PofEAA):http://capsctrl.que.jp/kdmsnr/wiki/PofEAA/?Repository


今回実装した認証処理は自分が確認した時点では、リリースされたばかりのせいか
本家のドキュメントを眺めても纏まっておらずグーグル先生に尋ねたり、ソースを見るなど少しがんばる必要がありました。
ただユーザ会の方々がドキュメントを翻訳されてますし、今後も利用ケースが増えてくれば情報は充実していくと思います。
php5.3を利用する要件の場合、選択肢の一つになってくると思いますので
機会があったら一度試してみてはいかがでしょうか?
また最後になりますが、下書き段階の文章にご感想いただいた@brtriverさんありがとうございました!

『アプリケーションハンガリアン...改善してみた』のエントリが「わかりやすくなってなさすぎるw」と指摘いただいたので補足します。

前回のエントリ:アプリケーションハンガリアンを用いて徳丸本の「様々な列でのソート」のサンプルを改善してみた。を読んだ方から「わかりやすくなってなさすぎるw」という指摘をいただいたので補足します。

以下、いただいた指摘
  • 意図は伝わったけどs/usというprefixは一般的じゃないからパッと見てsafe/unsafeだと分からなかった。
  • SFromUSがsafe from unsafeの略とか分からなかった。
  • Joelのエントリ読んでやっと理解した。
そうですね。参考としてJoelのエントリ:間違ったコードは間違って見えるようにするへのリンクを記載するのではなく、まずこっち読んでねとしたほうが良かったかもですね。

伝えたかったことは、前回のエントリの繰り返しになりますが、外部から渡ってきた値を文字列連結でSQLに組み込んでいるのは臭うということです。
$sort_columns = array('id', 'author', 'title', 'price');
$sort_key = $_GET['sort'];
if (array_search($sort_key, $sort_columns) !== false) {
    $sql .= ' ORDER BY ' . $sort_key;
}
その臭いを消すために外部から渡ってきた値はunsafeとし、safeに変換してから使うようにしました。これにより安全であることを分かりやすくしたつもりです。
$usSortKey = $_GET['sort_key'];                     // unsafeなprefixを持つ変数に格納
$sSortKey = SFromUS_SortKey($usSortKey);  // unsafeな値をsafeに変換し、safeなprefixを持つ変数に格納
$sSql .= ' order by ' . $sSortKey;                      // safeなprefixを持つ変数を連結
Joelは「どこか1行のコードでその間違いを見つけられる」ことが利点だと言っています。
$us = $_GET['name'];
これは正しい。

$usName = $us;
これは正しい。

$sName = $us;
これは間違っている。

$sName = $_GET['name'];
これは間違っている。

$sName = SFromUS($us);
これは正しい。

$sSql = ' order by ' . $_GET['name'];
これは間違っている。

$sSql = ' order by ' . $usName;
これは間違っている。

$sSql = ' order by ' . $sName;
これは正しい。

また、前回のエントリでは上記3行より関数の定義が先に記載されているのも分かりにくい原因だったかもしれません。

今回の議論で理解が深まった人がいれば幸いです。
今後もどこかの誰かに役に立つかもしれないことや、どこかの誰かが面白いと感じてくれるようなことを発信していきたいと思いますので、どうぞよろしくお願いします。

アプリケーションハンガリアンを用いて徳丸本の「様々な列でのソート」のサンプルを改善してみた。

こんにちはCTOの小賀(@makoga)です。今回はアプリケーションハンガリアンを用いて徳丸本のサンプルコードを改善してみます。

--------------------------------------------------
2011/9/22に補足エントリ書きました。
『アプリケーションハンガリアン...改善してみた』のエントリが「わかりやすくなってなさすぎるw」と指摘いただいたので補足します。
--------------------------------------------------

要約

  • 基本的にSQL文を動的に組み立てない。極力やらない方向で考える。
  • どうしても動的に組み立てたいときは、外から渡ってきた値は安全ではないので直接使わない。
  • 安全な値とそうでない値はアプリケーションハンガリアンを用いて可読性を高める。

詳細

弊社では現在徳丸本こと体系的に学ぶ 安全なWebアプリケーションの作り方 脆弱性が生まれる原理と対策の実践の勉強会が3つ進行中です。その中の1つで「4.4 SQL呼び出しに伴う脆弱性の様々な列でのソート」のサンプルコードをもう少し改善できるよねという話になりました。
$sort_columns = array('id', 'author', 'title', 'price');
$sort_key = $_GET['sort'];
if (array_search($sort_key, $sort_columns) !== false) {
    $sql .= ' ORDER BY ' . $sort_key;
}
上記が136ページに記載されているサンプルコードですが、以下のような改善点があると思います。
  • GETで渡された値をSQL文に連結しているので、パッと見てこのコードが安全か分かりにくい。
そこでアプリケーションハンガリアンを用いて以下のように改善しました。

function SFromUS_SortKey($usKey) {
    $SFromUsSortCol = array(
        'sort_id' => 'id',
        'sort_author' => 'author',
        'sort_title' => 'title',
        'sort_price' => 'price' );
    if(array_key_exists($usKey, $SFromUsSortCol))
    {
        $sSortKey = $SFromUsSortCol[$usKey];
    } else {
        $sSortKey = 'id';
    }
    return $sSortKey;
}
$usSortKey = $_GET['sort_key'];
$sSortKey = SFromUS_SortKey($usSortKey);
$sSql .= ' order by ' . $sSortKey;
これにより安全な値を連結していることが分かりやすくなったと思います。

参考:間違ったコードは間違って見えるようにする


Rを使ってお絵描き(アンパンマン方程式)

こんにちは、ECナビの水越(@Akiyah)です。

Rでお絵描きの完結編です。アンパンマン方程式というものを作ってみました。
anpanman_equation
この方程式を解くと、こうなります。

anpanman_equation

アン、アン、アンパンマーン!
Rのコードはこんな感じです。
library(rgl)                           # パッケージの呼び出し
open3d()                               # デバイスの起動

rgl.bg(color="#808080")
rgl.light(theta = 5, phi = 5)
x <- seq(-110, 110, length = 880)
y <- x
anpanman <- function(x,y) {
    s(((x   )/85)^2+((y   )/85)^2-1) * # face
    s(((x-55)/24)^2+((y +5)/24)^2-1) * # cheek
    s(((x   )/28)^2+((y +5)/23)^2-1) * # nose
    s(((x-18)/ 9)^2+((y-40)/15)^2-1) * # eye
   s((((x-18)/12)^2+((y-45)/20)^2-2) * h(y-45) + 1) * # eyebrow
   s((((x   )/42)^2+((y+35)/18)^2-2) * h(-y-35) + 1)   # mouse
}
s <- function(z) { sin(z*pi/2) * ((z < 1) * (z > -1)) + (z >= 1)*1 + (z <= -1)*(-1) }
h <- function(z) { z>0 } # function(z) { (abs(z)/z + 1)/2 }
anpanman_view <- function(x,y) {
  -abs(anpanman(abs(x),y) * 30)
}
z <- outer(x, y, anpanman_view)

col_g <- gray(0:3/3)
col <- col_g[1 + 1-((z < 3) & (z > -3))]
terrain3d(x, y, z, color=col)
方程式の正体は、楕円の方程式の組み合わせです。
眉毛と口の式を工夫して、うまく楕円の半分になるようにしました。
前の二回のアンパンマンは眉毛がかけなくて悔しかったのですが、やっと完璧なアンパンマンにすることができましたよ。

Treasure2011終了!!

こんにちは。システム本部の浪川です。
8月15日(月)からスタートしたECナビの夏のもの創り実践プログラム【Treasure】が終了しました。
私は前半の講義で講師として参加させていただきました。

思えば

みんな緊張していて変な空気だった初日に始まり

こちらの講義がきちんと伝えられているか内心ドキドキしていた前半

自分のことの様に緊張しながら見守った中間発表
(緊張で発表中トイレに5回以上行っていたのは内緒です。)

始めのころはDBやPHPでひっかかっていた学生たちが後半には
バリバリ開発しているのを見て「どんだけ成長してんだよ!」って
思ったグループワーク

そして、涙あり?笑いありの最終発表

と1ヶ月間があっという間に過ぎてしまいました。


私も大勢の前で講義をするのは初めてだったので
とても良い経験ができ成長できたと思います。

参加したみなさんにも
「大変だったけど良い経験だったし、成長できた。」
と思っていただけていたらなによりです。


最終発表の様子
P1080643

P1080672

P1080700



最後に

講義資料のチェックやインフラ整備をしてくれたシステム本部のみなさん。
さまざまなところでフォローしてくれた人事のみなさん。
サポーターをしていただいたエンジニアのみなさん。
TAをやってくれた来年の内定者。
そして、参加してくれたみなさん。

みなさんのおかげで今年のTreasureも無事終えることができました。
本当にありがとうございました。
記事検索
QRコード
QRコード