Documentation

You are viewing the documentation for the 2.1.5 release in the 2.1.x series of releases. The latest stable release series is 2.4.x.

§認証機能の追加

ここまででダッシュボードを表示することができるようになりましたが、ユーザ達にタスクを作り、アサインし、取り組むことを許可する前に、ユーザ達が自身を認証する方法が必要です。

§ログインスクリーンの実装

始めるに当たって、ユーザがログインできる画面を実装しましょう。conf/routes にログイン画面用の新しいルートを作成してください:

GET     /login                  controllers.Application.login()

そしてここで app/controllers/Application.java に login アクションを追加してください:

public static Result login() {
    return ok(
        login.render()
    );
}

このアクションで新しい login テンプレートを参照しているので、app/views/login.scala.html にテンプレートのスケルトンを書いていきましょう:

<html>
    <head>
        <title>Zentasks</title>
        <link rel="shortcut icon" type="image/png" href="@routes.Assets.at("images/favicon.png")">
        <link rel="stylesheet" type="text/css" media="screen" href="@routes.Assets.at("stylesheets/login.css")">
    </head>
    <body>
        <header>
            <a href="@routes.Application.index" id="logo"><span>Zen</span>tasks</a>
        </header>
    
    </body>
</html>

ここでブラウザから <http://localhost:9000/login> を開いて、ルートが動作していることを確認してください。タイトルは別として、空白のページが表示されるはずです。

§フォームの追加

もちろん、ログインページには email アドレス (ユーザー名) とパスワードを保持するフォームが必要です。

Play はフォームをレンダリングし、その内容を復号化し、そしてその内容に対してバリデーションを行うフォーム API を提供しています。Java オブジェクトとしてフォームを実装するところから始めましょう。app/controllers/Application.java クラスを開き、末尾に Login という名前の static なインナークラスを宣言してください。

public static class Login {

    public String email;
    public String password;

}

このフォームをレンダリングするためにテンプレートに引き渡す必要があります。このフォームをテンプレートに引き渡すために、app/controllers/Application.javalogin を変更します:

    public static Result login() {
        return ok(
            login.render(form(Login.class))
        );
    }

そして、ログインテンプレートが受け取れるよう、このフォームを app/views/login.scala.html の引数として宣言します:

@(form: Form[Application.Login]

<html>
...

フォームをレンダリングする必要があります。ログインテンプレートにフォームを追加しましょう:

@helper.form(routes.Application.authenticate) {
   <h1>Sign in</h1>

   <p>
       <input type="email" name="email" placeholder="Email" value="@form("email").value">
   </p>
   <p>
       <input type="password" name="password" placeholder="Password">
   </p>
   <p>
       <button type="submit">Login</button>
   </p>
}

ここで確認しておくべき重要なことは、@helper.form の呼び出しです。routes.Application.authenticate というルートを引き渡しました。これは、Play にこのフォームがどこに投稿されるべきかを伝えます。ここで URL が一切ハードコーディングされていないことに気付きましたか? これは、アプリケーションが突然の死を迎えること無しに URL の構造を変更できることを意味しています。

実際には Play はもっとリッチなフォームタグのセットを提供していますが、このログインフォームには行き過ぎたものです。これらはこのチュートリアルの後半で目にすることになるでしょう。

もちろん、この authenticate ルートはまだ実装されていないので、ここで実装していきましょう。まず最初に、conf/routes にルートを追加します:

POST    /login                      controllers.Application.authenticate()

そして app/controllers/Application.java にメソッドを実装します:

public static Result authenticate() {
    Form<Login> loginForm = form(Login.class).bindFromRequest();
    return ok();
}

Application.javaplay.data.* のインポート文を追加していることを確認してください

§フォームのバリデーション

今のところ、authenticate アクションはフォームを読み込む以外は何もしていません。次にやりたいことはフォームのバリデーションですが、このバリデーションについて気に掛けることはただひとつ、ユーザ名とパスワードは正しいか? ということだけです。このバリデーションを実装するため、app/controllers/Application.javaLogin クラスに validate メソッドを書いていきましょう。

public String validate() {
    if (User.authenticate(email, password) == null) {
      return "Invalid user or password";
    }
    return null;
}

見ての通り、このメソッドは任意のバリデーションを行うことができますが、ここでは既に実装した User.authenticate メソッドを使っています。バリデーションに失敗した場合、エラーメッセージとして String が返却され、バリデーションに成功した場合は null が返却されます。

これで authenticate アクションの Form オブジェクトで hasErrors() メソッドを使ってバリデーションを行うことができます:

public static Result authenticate() {
    Form<Login> loginForm = form(Login.class).bindFromRequest();
    if (loginForm.hasErrors()) {
        return badRequest(login.render(loginForm));
    } else {
        session().clear();
        session("email", loginForm.get().email);
        return redirect(
            routes.Application.index()
        );
    }
}

このコードはいくつかの新しい概念を紹介しています。まず、バリデーションに失敗した場合は、バリデーションに失敗したフォームと共にログインページをレンダリングしつつ、400 Bad Request ステータスを返却します。フォームを返却する際、フォームからあらゆるバリデーションエラー、そしてユーザが入力した値を抽出し、それらをユーザに書き戻すことができます。

バリデーションに成功した場合は、セッションに属性を追加します。この属性は email と呼ぶことにして、その値は今まさに正常にログインしたユーザの email アドレスです。このセッション属性は、後で現在ログインしているユーザを検出するために使用します。

ユーザをセッションに格納した後、ダッシュボードへの HTTP リダイレクトを発行します。テンプレートにアセットを取り込むときと同じように、ダッシュボードアクションを参照するためにリバースルーターを使っていることが分かるでしょう。

バリデーションについてはほとんど完了です。あとひとつ残っているのは、バリデーション失敗時のエラーメッセージの表示です。先ほど不正なフォームをテンプレートに投げ返しましたが、このフォームを使ってエラーメッセージを取得します。以下のコードを app/views/login.scala.html の見出し Sign in のすぐ下に配置してください:

@if(form.hasGlobalErrors) {
    <p class="error">
        @form.globalError.message
    </p>
}

ここで不正なパスワードでログインしてみましょう。こんな感じに見えるはずです:

今度は正しいパスワード (secret) を入力し直してログインします。ダッシュボードに案内されるはずです。

§アクションのテスト

そろそろアクションのテストを書き始めていい頃です。ログイン機能を提供するアクションを書いたので、これが動作することを確認しましょう。test/controllers/LoginTest.java というテストクラスのスケルトンを作成するところから始めましょう:

package controllers;

import org.junit.*;
import static org.junit.Assert.*;
import java.util.*;

import play.mvc.*;
import play.libs.*;
import play.test.*;
import static play.test.Helpers.*;
import com.avaje.ebean.Ebean;
import com.google.common.collect.ImmutableMap;

public class LoginTest extends WithApplication {
    @Before
    public void setUp() {
        start(fakeApplication(inMemoryDatabase(), fakeGlobal()));
        Ebean.save((List) Yaml.load("test-data.yml"));
    }

}

フェイクアプリケーションをセットアップする際に、今度は fakeGlobal() を渡していることを確認してください。実は、“本物の” Global.java を作ったことで、以前に書いたテスト開始時に初期データを読み込んでしまう ModelsTest は壊れてしまっています。このため、このテストも fakeGlobal() を使うよう更新するべきです。

それでは、正常に認証した場合に何が起こるか確認するテストを書きましょう:

@Test
public void authenticateSuccess() {
    Result result = callAction(
        controllers.routes.ref.Application.authenticate(),
        fakeRequest().withFormUrlEncodedBody(ImmutableMap.of(
            "email", "[email protected]",
            "password", "secret"))
    );
    assertEquals(302, status(result));
    assertEquals("[email protected]", session(result).get("email"));
}

ここにはいくつかの新しい概念が紹介されています。最初のひとつは Play の “ref” リバースルーターを使っていることです。これにより、callAction に引き渡して実行するアクションの参照を取得することができます。ここでは、Application.authenticate アクションへの参照を取得しました。

フェイクリクエストも作成しています。認証に使用する email と password を保持するフォームボディを与えています。

最後に、テスト結果のステータスとセッションを取得するために、statussession というヘルパーメソッドを使っています。Bob の email アドレスを使って正しく認証すると、セッションにこのアドレスが追加されることを確認しています。この他に、ヘッダやボディのような、テスト結果のその他の箇所にアクセスするためのヘルパーメソッドが存在します。なぜテスト結果を直接取得しないのか不思議に思うかもしれません。その理由は、テスト結果は例えば非同期になるかもしれないので、それらにアクセスするために、Play は必要に応じてそれらを展開する必要があるためです。

テストを実行してこれが成功することを確認してください。今度は、不正な email と password が入力された場合にはログインできないことを確認する別のテストを書きましょう。

@Test
public void authenticateFailure() {
    Result result = callAction(
        controllers.routes.ref.Application.authenticate(),
        fakeRequest().withFormUrlEncodedBody(ImmutableMap.of(
            "email", "[email protected]",
            "password", "badpassword"))
    );
    assertEquals(400, status(result));
    assertNull(session(result).get("email"));
}

このテストを実行して、成功することを確認してください。

§認証機能の実装

これでログインできるようになったので、アクションを認証機能で保護していきましょう。Play のアクション合成を使うことでこれを実現することができます。アクション合成は、複数のアクションを連鎖するようにまとめて組み立てる機能です。それぞれのアクションは、次のアクションにリクエストを委譲する前に何らかの処理を行うことができますし、処理結果を変更することもできます。あるアクションは、次のアクションにリクエストを引き渡さずに、自分自身で処理結果を生成することを決断することもできます。

Play には認証用のアクションが既に組み込まれているので、これを継承して機能を追加したいと思います。この認証機能を Secured と呼ぶことにしましょう。app/controllers/Secured.java を開き、このクラスを実装してください。

package controllers;

import play.*;
import play.mvc.*;
import play.mvc.Http.*;

import models.*;

public class Secured extends Security.Authenticator {

    @Override
    public String getUsername(Context ctx) {
        return ctx.session().get("email");
    }

    @Override
    public Result onUnauthorized(Context ctx) {
        return redirect(routes.Application.login());
    }
}

ここでふたつのメソッドを実装しました。getUsername は、現在ログインしているユーザーのユーザー名を取得するために使われます。今回の例の場合は、ユーザーがログインしたときにセッションの email 属性に設定した email アドレスを取得します。このメソッドが値を返すと、認証機能はユーザーがログインしているものと見なし、リクエストを続行します。一方で、このメソッドが null を返すと、認証機能はリクエストを中断し、その代わりにログイン画面にリダイレクトするよう実装した onUnathorized を実行します。

ダッシュボードでこの認証機能を使ってみましょう。app/controllers/Application.javaindex メソッドに、作成した認証機能を含めた @Security.Authenticated アノテーション追加してください:

@Security.Authenticated(Secured.class)
public static Result index() {
    ...

§認証機能のテスト

この認証機能が動作することを確認するために、test/controllers/LoginTest.java にテストを書いていきましょう:

@Test
public void authenticated() {
    Result result = callAction(
        controllers.routes.ref.Application.index(),
        fakeRequest().withSession("email", "[email protected]")
    );
    assertEquals(200, status(result));
}    

もちろん、より重要なテストは認証されなかった場合にリクエストがブロックされることなので、これを確認しましょう:

@Test
public void notAuthenticated() {
    Result result = callAction(
        controllers.routes.ref.Application.index(),
        fakeRequest()
    );
    assertEquals(303, status(result));
    assertEquals("/login", header("Location", result));
}

テストを実行して認証機能が動作していることを確認してください。

§ログアウト

web ブラウザでダッシュボードにアクセスしてみてください。すでに正しくログインしていれば、認証機能はリクエストをブロックしないので、ダッシュボードを見ることができていると思います。ブラウザを閉じてログアウトすることもできますが、今こそログアウトアクションを実装するのにふさわしい時です。いつも通り、ルートから始めます:

GET     /logout                     controllers.Application.logout()

そして、app/controllers/Application.java に実装します:

public static Result logout() {
    session().clear();
    flash("success", "You've been logged out");
    return redirect(
        routes.Application.login()
    );
}

セッションをクリアすることでユーザーをログアウトしているのが分かると思います。ここで新しい概念がひとつ登場します。セッションをクリアしたあと、flash スコープに属性として成功メッセージを追加しました。flash スコープは、次のリクエストがやってくるまでしか存続しないことを除いて、セッションと似ています。これを使うことで、リダイレクトリクエストがやってきたときにログインテンプレートに成功メッセージを表示することができます。エラーメッセージのときと同じように、app/views/login.scala.html にメッセージを追加しましょう:

@if(flash.contains("success")) {
    <p class="success">
        @flash.get("success")
    </p>
}

最後に、メインテンプレート app/views/main.scala.html の見出し段落の中にログアウトリンクを追加しましょう:

<header>
    <a href="@routes.Application.index" id="logo"><span>Zen</span>tasks</a>
    <dl id="user">
        <dt>User</dt>
        <dd>
            <a href="@routes.Application.logout()">Logout</a>
        </dd>
    </dl>
</header>

ブラウザでダッシュボードにアクセスしてログアウトし、再度ダッシュボードにアクセスしてみてください。ダッシュボードを見ることができず、ログイン画面にリダイレクトされるはずです。ログインすれば、再度ダッシュボードを見ることができるでしょう。

§現在のユーザーを使用する

最後にやっておきたいことがもうひとつあります。今ではユーザーがログインしているかどうかによってアクセスをブロックすることができるようになりましたが、どうすれば現在ログインしているユーザーにアクセスするのでしょう? その答えは request.username() メソッドを通じて得られます。このメソッドは現在のユーザーの email アドレスを提供します。

メインテンプレートのログアウトリンクの隣りにユーザー名を配置しましょう。ユーザー名を取得するためには、実際にユーザー情報全体をデータベースから読み込まなければなりません。モデルに実装したメソッドを使って、ユーザーがメンバーに含まれているプロジェクトと、ユーザーがアサインされているタスクも絞り込みましょう:

app/controllers/Application.javaindex メソッドでユーザーを読み込むところから始めます:

@Security.Authenticated(Secured.class)
public static Result index() {
    return ok(index.render(
        Project.findInvolving(request().username()), 
        Task.findTodoInvolving(request().username()),
        User.find.byId(request().username())
    )); 
}

index テンプレートに渡す引数を追加したので、app/views/index.scala.html に引数を宣言して、これをメインテンプレートに引き渡しましょう:

@(projects: List[Project], todoTasks: List[Task], user: User)

@main(projects, user) {
    ...

もちろん app/views/main.scala.html にも引数の宣言を追加しなければなりません:

@(projects: List[Project], user: User)(body: Html)

これで見出しにユーザー情報を使用できるので、追加したばかりのログアウトリンクの前にユーザー情報を追加します:

<dl id="user">
    <dt>@user.name <span>(@user.email)</span></dt>
    <dd>
        <a href="@routes.Application.logout()">Logout</a>
    </dd>
</dl>

ログインしていることを確認したら、再度ダッシュボードにアクセスしてください。

現在ログインしているユーザーと、このユーザーがアクセスできるプロジェクト、そしてこのユーザーがアサインされているタスクだけを見ることができます。

いつも通り、git に作業内容をコミットしてください。

次章 に進みましょう