開発

2016.03.29

SymfonyのSecurityを使った課金モデルを実装

こんにちは。シモダです。

Symfony標準のアクセスコントロール機能はなかなか便利で拡張もし易いのですが、
多くのドキュメントだと「ログイン」や「管理画面へのアクセス」等の実装例がほとんどです。
参考:http://symfony.com/doc/current/book/security.html

もう少し他の例にも上手く使えないかということで、
よくある例えば「月額課金」のような、有料/無料ユーザのモデルへ応用してみました。

Symfony標準のdemoアプリケーションをベースに、
「ある期間のみ(例:1ヶ月など)記事を読める」機能を実装します。

 

$ symfony demo
$ cd symfony_demo
$ php app/console server:run

まずはいきなりですが AppBundle\Controller\BlogController の postShowAction へ、
「有料のユーザのみ」アクセスできるよう、`@Security` にてアクセス制限を追加します。
詳しくはこちら:https://symfony.com/doc/current/bundles/SensioFrameworkExtraBundle/index.html

 

--- a/src/AppBundle/Controller/BlogController.php
+++ b/src/AppBundle/Controller/BlogController.php
@@ -50,6 +50,7 @@ class BlogController extends Controller
/**
 * @Route("/posts/{slug}", name="blog_post")
+ * @Security("has_role('ROLE_PAID_USER')")
 *
 * NOTE: The $post controller argument is automatically injected by Symfony
 * after performing a database query looking for a Post with the 'slug'
 * value given in the route.
 * See http://symfony.com/doc/current/bundles/SensioFrameworkExtraBundle/annotations/converters.html
 */
 public function postShowAction(Post $post)
 {

有料ユーザが持つ権限=Roleを、 `ROLE_PAID_USER` とします。
また有料ユーザは無料ユーザの権限を包括するので、 app/config/security.yml にて role_hierarchyを定義します。

 

--- a/app/config/security.yml
+++ b/app/config/security.yml
@@ -43,3 +43,5 @@ security:
 # this is a catch-all for the admin area
 # additional security lives in the controllers
 - { path: ^/admin, roles: ROLE_ADMIN }
+ role_hierarchy:
+ ROLE_PAID_USER: ROLE_USER

この時点で、 ROLE_PAID_USER権限を持たないユーザは各記事へアクセスできなくなります。
同時に、非ログインユーザもアクセス時にログインを求められるようになります。

 

403

ユーザがアクセス権限を持つかどうかは、Userクラスの getRoles() メソッドに記述します。
アクセス権限が必要なアクションにリクエストがあった場合に、Symfonyによって内部的にこのメソッドが呼び出され、
ユーザの持つ権限をチェックする仕組みです。

一旦、全てのユーザが有料ユーザの権限をもつように実装します。

 

--- a/src/AppBundle/Entity/User.php
+++ b/src/AppBundle/Entity/User.php
@@ -107,6 +107,10 @@ class User implements UserInterface
/**
 * Returns the roles or permissions granted to the user for security.
 */
 public function getRoles()
 {
 $roles = $this->roles;
// guarantees that a user always has at least one role for security
 if (empty($roles)) {
 $roles[] = 'ROLE_USER';
 }
+ // ここのif条件で、権限を持つかどうかをチェック
+ // 後ほど実装。
+ if (true) {
+ $roles[] = 'ROLE_PAID_USER';
+ }
+
 return array_unique($roles);
 }

さて、次に「有料ユーザの期間を管理する」仕組みを作ります。
Licenseというエンティティを作り、有効期限 expired_at を記録することで、「有料ユーザでいられる期間」を管理しようと思います。
いわゆる”会員証”のような概念となります。
ユーザとの関係はOne_To_Manyにしました。

雑なクラス図はこちら

class

 

コマンドを使ってエンティティを作成

 

$ php app/console doctrine:generate:entity
The Entity shortcut name: AppBundle:License
...(中略)...
New field name (press <return> to stop adding fields): expired_at
Field type [datetime]:
...(中略)...
Do you confirm generation [yes]? yes

さらに、Userとのリレーションを定義します。

 

--- a/src/AppBundle/Entity/License.php
+++ b/src/AppBundle/Entity/License.php
@@ -28,6 +28,11 @@ class License
 */
 private $expiredAt;
+ /**
+ * @ORM\ManyToOne(targetEntity="User", inversedBy="licenses")
+ * @ORM\JoinColumn(name="user_id", referencedColumnName="id")
+ */
+ private $user;
/**
 * Get id
--- a/src/AppBundle/Entity/User.php
+++ b/src/AppBundle/Entity/User.php
@@ -3,6 +3,7 @@
 namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
+use Doctrine\Common\Collections\ArrayCollection;
 use Symfony\Component\Security\Core\User\UserInterface;
/**
@@ -46,6 +47,16 @@ class User implements UserInterface
 */
 private $roles = array();
+ /**
+ * @ORM\OneToMany(targetEntity="License", mappedBy="user")
+ * @ORM\OrderBy({"expiredAt" = "DESC"})
+ */
+ private $licenses;
+
+ public function __construct() {
+ $this->licenses = new ArrayCollection();
+ }

 

次は「有効期限前のLicenseを持っているかどうかで、有料ユーザとみなす」部分の実装です。
Userに「有効期限前のLicenseを持っているかどうか」を判定するメソッド hasAvailableLicenses() を追加します。

 

--- a/src/AppBundle/Entity/User.php
+++ b/src/AppBundle/Entity/User.php
@@ -135,4 +139,19 @@ class User implements UserInterface
+
+ /**
+ * 有効期限内のLicenseがあるかどうか返す
+ *
+ * @return boolean
+ */
+ private function hasAvailableLicenses(\Datetime $date = null)
+ {
+ if (is_null($date)) {
+ $date = new \Datetime;
+ }
+ return $this->licenses->exists(function ($key, $license) use ($date) {
+ return $license->getExpiredAt() > $date;
+ });
+ }

 

 

後はこれを用いて、先ほどの `getRoles()` 内部で「有料ユーザとみなす」判定に用いればOKです。

 

 

--- a/src/AppBundle/Entity/User.php
+++ b/src/AppBundle/Entity/User.php
@@ -107,6 +107,10 @@ class User implements UserInterface
/**
 * Returns the roles or permissions granted to the user for security.
 */
 public function getRoles()
 {
 $roles = $this->roles;
// guarantees that a user always has at least one role for security
 if (empty($roles)) {
 $roles[] = 'ROLE_USER';
 }
- // ここのif条件で、権限を持つかどうかをチェック
- // 後ほど実装。
- if (true) {
+ if ($this->hasAvailableLicenses()) {
 $roles[] = 'ROLE_PAID_USER';
 }
return array_unique($roles);
 }

DBを直接操作して動作確認してみます。
demoアプリケーションに元々登録されているユーザがいるので、Licenseを持たせてみます。

 

 

$ sqlite3 app/data/blog.sqlite
# ユーザの確認
sqlite> select * from user; 
1|john_user|john_user@symfony.com|$2y$13$nXcXgsQSnNuj1Z4KibTSKuaq3V0flRNM5ezRBJn16iaX36YzJLeI.|[]
2|anna_admin|anna_admin@symfony.com|$2y$13$FgCRpImk9IsUVsqvctCtAOwcmS2PLQInKJMDbihjYAaa0X5Y9qN9u|["ROLE_ADMIN"]

# 有効なlicenseを一件登録
sqlite> insert into license(user_id, expired_at) values(1, '2016-12-31 00:00:00');

 

ブログ記事にアクセスしてみます。
非ログイン状態の場合はログインフォームに飛ばされると思います。

Licenseの有効期限前なので、問題なくdemo記事を読むことができます。

スクリーンショット 2016-03-28 19.58.32

有効期限をいじって失効させてみましょう。

 

sqlite> update license set expired_at = '2000-01-01 00:00:00' where id = 1;

どうでしょうか?403になれば成功です。

後は実際の課金に合わせて、Licenseを登録する処理を実装すればOKです。

Share on FacebookTweet about this on TwitterShare on Google+Share on Tumblr

この記事を書いた人

staff エンジニア:下田敬祐
一覧に戻る

[ WANTED! ]

共に地方を創る仲間を、随時募集中です