ウェブアプリを作っているとユーザ情報を登録させて、ページアクセスを制限したいことが良くあると思います。今回はPlayFrameworkを利用して認証機能を実装してみました。ソースはGitHubにあります。
環境
- PlayFramework2.5
- Java8
- H2DB(MODE=MySQL)
実現する機能
画面一覧
- サインアップ画面
- サインイン画面
- ホーム画面
ファイル構成
PlayAuthentication
├── app
│ ├── common
│ │ ├── global
│ │ │ └── AppActionCreator.java
│ │ └── secure
│ │ └── AppAuthenticator.java
│ ├── controllers
│ │ ├── AppController.java
│ │ ├── IndexController.java
│ │ ├── SigninController.java
│ │ └── SignupController.java
│ ├── forms
│ │ ├── AppForm.java
│ │ ├── SigninForm.java
│ │ └── SignupForm.java
│ ├── models
│ │ ├── AppModel.java
│ │ └── User.java
│ └── views
│ ├── index.scala.html
│ ├── main.scala.html
│ ├── signin.scala.html
│ └── signup.scala.html
├── build.sbt
├── conf
│ ├── application.conf
│ ├── evolutions
│ │ └── default
│ │ └── 1.sql
│ └── routes
└── project
└── plugins.sbt
ファイル名でなんとなく役割は伝わるかと思います。
データベースの準備
サインアップ画面から入力させたユーザ情報をDBに登録します。サインイン画面ではメールアドレス、パスワードを入力させDBの情報と照合し認証します。入力されたユーザ情報は、インメモリデータベースのH2DBに格納したいと思います。O/RマッパーにはPlayと相性の良いEbeanを、データベース管理にはEvolutionsを使いたいと思います。H2DB、Ebean、Evolutionsを使うには下記の三点を修正し、sbtのreload、updateをします。
lazy val root = (project in file(".")).enablePlugins(PlayJava, PlayEbean) // PlayEbeanを追加
libraryDependencies += evolutions // 追加
addSbtPlugin("com.typesafe.sbt" % "sbt-play-ebean" % "3.0.0") // 追加
db {
default.driver = org.h2.Driver
default.url="jdbc:h2:mem:play;MODE=MYSQL" # 今回はMySQLモード
default.username = sa
default.password = ""
}
ebean {
default = ["models.*"]
}
その後データベースの情報を保持するJavaオブジェクトのModelクラスをmodelsパッケージと一緒に作成します。Modelクラスにはスーパークラスを作って、全てのテーブルで作成日時と最終更新日時を定義したいと思います。今回はUserテーブルのみ作成するので、あまり意味はありませんが。
package models;
// import文省略
@MappedSuperclass
public abstract class AppModel extends Model {
/** ID */
@Id
public Long id;
/** 作成日時 */
@CreatedTimestamp
public LocalDateTime created;
/** 最終更新日時 */
@UpdatedTimestamp
public LocalDateTime updated;
}
package models;
// import文省略
@Entity
public class User extends AppModel {
/** 名前 */
public String name;
/** メールアドレス */
public String email;
/** パスワード */
public String password;
/** 最終ログイン日時 */
public LocalDateTime logined;
}
ここまでして、サーバを起動しアクセスします。そうするとブラウザに以下の画面が表示されると思います。
そのまま「Apply this script now!」ボタンを押下するとSQLが実行されH2DBにテーブルが作成されます。さらにconf/evolutions/defaultに以下のSQLファイルが生成されます。
# --- Created by Ebean DDL
# To stop Ebean DDL generation, remove this comment and start using Evolutions
# --- !Ups
create table user (
id bigint not null,
name varchar(255),
email varchar(255),
password varchar(255),
logined timestamp,
created timestamp not null,
updated timestamp not null,
constraint pk_user primary key (id)
);
create sequence user_seq;
# --- !Downs
drop table if exists user;
drop sequence if exists user_seq;
これはjavax.persistence.Entityのannotationを付与しているModelクラスを見てスキーマを起こしてくれているようです。
ユーザ情報の登録、照合
データベースができたことを確認できたので、次はサインアップとサインインする画面を作ります。それぞれViewとFormを作り、項目はメールアドレスとパスワードを持たせます。サインアップには名前も持たせてみました。今回はViewとFormのソースは簡易的なものなので割愛します。興味のある人はGitHubを参照してください。また、vlidation機能の実現でViewとFormについて書いた投稿があるので、こちらも参照してみてください。
あとは画面から入力された情報を用いてコントローラーで処理をします。サインアップ画面コントローターでは名前、メールアドレス、パスワードをユーザ情報としてDBに保存します。
package controllers;
// import文省略
@Singleton
public class SignupController extends AppController {
@Inject
public SignupController(CacheApi cache) {
super(cache);
}
@Inject
private FormFactory formFactory;
@Override
public Result get() {
Form<SignupForm> form = formFactory.form(SignupForm.class);
return ok(views.html.signup.render(form));
}
@Override
public Result post() {
Form<SignupForm> form = formFactory.form(SignupForm.class).bindFromRequest();
if(form.hasErrors()){
return badRequest(views.html.signup.render(form));
}
try{
/*
* ユーザ情報を新規登録する。
*/
Ebean.beginTransaction();
User user = new User();
user.name = form.get().name;
user.email = form.get().email;
user.password = BCrypt.hashpw(form.get().password, BCrypt.gensalt());
user.logined = LocalDateTime.now();
Ebean.insert(user);
Ebean.commitTransaction();
new SigninController(cache).setCacheUser(user);
}catch(Exception e){
Ebean.rollbackTransaction();
return badRequest(views.html.signup.render(form));
}finally {
Ebean.endTransaction();
}
return redirect(routes.IndexController.get());
}
}
サインイン画面コントローラーでは入力されたメールアドレスのユーザのログイン日時を更新します。
package controllers;
// import文省略
@Singleton
public class SigninController extends AppController {
@Inject
private FormFactory formFactory;
/** ユーザ情報キー */
public static final String USER_KEY = "USER";
@Inject
public SigninController(CacheApi cache) {
super(cache);
}
@Override
public Result get() {
Form<SigninForm> form = formFactory.form(SigninForm.class);
return ok(views.html.signin.render(form));
}
@Override
public Result post() {
Form<SigninForm> form = formFactory.form(SigninForm.class).bindFromRequest();
if(form.hasErrors()){
return badRequest(views.html.signin.render(form));
}
try{
/*
* ユーザのログイン日時を更新する。
*/
Ebean.beginTransaction();
User user = Ebean.find(User.class).where().eq("email", form.get().email).findUnique();
user.logined = LocalDateTime.now();
Ebean.update(user);
Ebean.commitTransaction();
setCacheUser(user);
}catch(Exception e){
Ebean.rollbackTransaction();
return badRequest(views.html.signin.render(form));
}finally {
Ebean.endTransaction();
}
return redirect(routes.IndexController.get());
}
/*
* 以下メソッド省略
*/
}
サインアップ画面コントローラーでのユーザ情報保存時、パスワードをハッシュ化するためにBCryptを使用しています。
libraryDependencies += "org.mindrot" % "jbcrypt" % "0.4" // 追加
記述後はまたsbtのrelaod、updateを行ってください。
最後に
今回はユーザ情報を登録、照合するために必要な部分について書きました。認証というよりデータベースの管理や操作などで便利だと思う部分が多く感じました。今回書いていないアクセス制限についてはこちらに書いています。
GitHub