PlayFramework + pac4jでOAuthを使う　②DB連携

# Play + pac4jエントリー一覧
[PlayFramework + pac4jでOAuthを使う　①Twitter認可](http://qiita.com/skliber/items/ca640e7eaa737ef9246b)
[PlayFramework + pac4jでOAuthを使う　②DB連携](http://qiita.com/skliber/items/7e02b7f287ac5a4fc1f9)

# 本記事のゴール
取得したプロファイル情報をDBに格納し、突合せによるログインログアウトを実装

# 全コード
https://github.com/skliber/pac4j-test/tree/db_link


# 実装

## 方針

- ユーザーはSNSIDでDB内の一意性を確保

- セッションにはクレデンシャル情報を入れない

## Ebeanの準備
### app/build.sbt

```
lazy val root = (project in file(".")).enablePlugins(PlayJava, PlayEbean)
```

### app/project/plugins.sbt
アンコメント

```
// Play Ebean support, to enable, uncomment this line, and enable in your build.sbt using
// enablePlugins(PlayEbean).
addSbtPlugin("com.typesafe.sbt" % "sbt-play-ebean" % "3.0.2")
```

#### app/conf/application.conf
default.driverとdefault.urlをアンコメント
ebean.default = ["models.entity.*"]を追記

```
...
db {
  # You can declare as many datasources as you want.
  # By convention, the default datasource is named `default`

  # https://www.playframework.com/documentation/latest/Developing-with-the-H2-Database
  default.driver = org.h2.Driver
  default.url = "jdbc:h2:mem:play"
  #default.username = sa
  #default.password = ""

  # You can turn on SQL logging for any datasource
  # https://www.playframework.com/documentation/latest/Highlights25#Logging-SQL-statements
  #default.logSql=true
}
ebean.default = ["models.entity.*"]
...
```

## DBエンティティ

### app/models/entity/BaseEntity.java
全てのエンティティの親クラス
IDによるプライマリーキーと作成日時と更新日時を設定

```java:BaseEntity.java
package models.entity;

import java.time.LocalDateTime;

import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.MappedSuperclass;

import com.avaje.ebean.Model;
import com.avaje.ebean.annotation.CreatedTimestamp;
import com.avaje.ebean.annotation.UpdatedTimestamp;

@MappedSuperclass
public class BaseEntity extends Model {

	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	public Long id;

	@CreatedTimestamp
	public LocalDateTime create_date;

	@UpdatedTimestamp
	public LocalDateTime update_date;

}
```

### app/models/entity/User.java
SNSのユーザーを管理するエンティティ
SNSIDは@Column(unique = true)で一意性を保証する

```java:User.java
package models.entity;

import javax.persistence.Column;
import javax.persistence.Entity;

@Entity
public class User extends BaseEntity {

	/* SNSが提供する一意なID */
	@Column(unique = true)
	public String sns_id;

	/* ユーザーネーム */
	public String name;

	/* メールアドレス */
	public String mail;

	/* パスワード */
	public String password;

	public static Finder<Long, User> finder = new Finder<Long, User>(User.class);

}
```

## サービスクラス
### app/models/service/user/UserService.java
CommonProfile#getId()で認可したユーザーのSNSIDが取得できる

```java:UserService.java
...

	/**
	 * SNS経由の登録orログイン処理
	 *
	 * @param profile
	 * @return
	 */
	public Optional<User> createOrLogin(CommonProfile profile) {

		// DBに同じSNSIDのユーザーが存在したら登録せずログイン
		Optional<User> userOps = userModelService.findBySnsId(profile.getId());
		if(userOps.isPresent()){
			return userOps;
		}

		// 新規作成処理
		User user = new User();
		user.sns_id = profile.getId();
		user.name = profile.getDisplayName();

		return userModelService.save(user);
	}

...
```

### app/models/service/user/UserModelService.java

SNSIDでDBを検索
レコードが存在すればそのユーザーのエンティティを返し、存在しなければemptyを返す

```java:UserModelService.java
...

	public Optional<User> findBySnsId(String snsId) {
		return Optional.ofNullable(snsId).map(sns_id -> User.finder.where().eq("sns_id", sns_id).findUnique());
	}

...

```

## コントローラー

### app/controller/LoginController.java

```java:LoginController.java
public class LoginController extends Controller {

	@Inject
	UserService userService;

	@Inject
	protected PlayCacheStore playCacheStore;

	@Inject
	protected PlaySessionStore playSessionStore;

	private List<CommonProfile> getProfiles() {
		final PlayWebContext context = new PlayWebContext(ctx(), playSessionStore);
		final ProfileManager<CommonProfile> profileManager = new ProfileManager<>(context);
		return profileManager.getAll(true);
	}

	private void storeUserId(String sessionId, String userId) {
		final PlayWebContext context = new PlayWebContext(ctx(), playSessionStore);
		playCacheStore.set(context, sessionId, userId);
	}

	@Secure(clients = "TwitterClient")
	public Result twitterLogin() {
		Optional<User> userOps = userService.createOrLogin(getProfiles().get(0));
		if (!userOps.isPresent()) {
			return badRequest("FAILURE");
		}
		storeUserId(session(org.pac4j.core.context.Pac4jConstants.SESSION_ID), String.valueOf(userOps.get().id));
		return ok(protectedIndex.render(getProfiles()));
	}

}
```
####  @Inject protected PlayCacheStore playCacheStore;
Cacheを使用するためのフィールド

#### private void storeUserId(String sessionId, String userId) 
CacheにUserのIDを保存するためのメソッド

#### storeUserId(session(org.pac4j.core.context.Pac4jConstants.SESSION_ID), String.valueOf(userOps.get().id));

セッションIDの値をキーにしてUserのIDをCacheに保存
これで特定のセッションIDを持つユーザーがDB内のどのUserかわかるようになる

## セッションとキャッシュの図解

![playsessioncache.png](https://qiita-image-store.s3.amazonaws.com/0/77685/d2bd0265-a41e-762f-e7cc-026d1f1c0dbf.png)

### ブラウザ
認可したユーザーのブラウザには、pac4jSessionIdをキーとしてランダムなUUIDが紐付くクッキーが作られる

### サーバー
サーバー内のCacheには、UUIDをキーとしてそのユーザーのIDが紐付く

## 動作確認
Skip...

## Next
- pac4jを経由したフォームからの登録
or
- ロールによる権限の管理
or
- authorizersの応用