spring-boot, security, socialでSign in with Twitterを実装する

# この記事で説明すること
spring-boot, spring-security, spring-socialを使ってソーシャルログインを実装したので、そのサンプルを利用してどのように実装したかを解説します。長くなると思うので自分が興味のあるChapterだけ選んで読んでみてください。

今回のサンプルでは、ソーシャルログインだけでなく、userid/passwordを入力してログインすることもできます。

# 経緯
spring-bootやspring-securityは[はじめてのSpring Boot](https://www.amazon.co.jp/dp/4777519694)で勉強した。
この本では、spring-boot, securityを使って、ログイン/ログアウトができる名簿アプリを作るのだが、そこに 「Sign in with Twitter」の機能も付けたくなり、つけようとしたが思いの外苦労してしまった…
私はこの苦労をこの世から根絶すべく、当記事の執筆に踏み切ったのである。

# サンプルについて
## ソースコード
こちらです。　https://github.com/PonzyPon/spring-boot-social-demo

## サンプル君のできること
* 独自のID/Passwordでログインできます
* Twitterでログインできます
* Twitterでログインした際にそれがそのユーザーにとって初めてのログインだった場合、裏で暗黙的にユーザー登録をします
* ログインしたら、画面左上にそのユーザーの名前を表示できます
* サーバー側ロジックで現在ログイン中のユーザーのUserIdを取得できます `UserContext#getUserId`

## サンプル君のできないこと
* ユーザー登録機能は実装していません。あらかじめ`V1__create-tables.sql`にてuser1というサンプルユーザーを作成しています

## サンプル君のDB設計
このサンプルでは、独自ID/Passwordでのログインと、ソーシャルログインのどちらもを実現したかった。そのため２つのテーブルを作成した。ユーザーのマスタである`User`テーブルと、ソーシャルログイン情報を保持する`UserConnection`テーブルである。`User#user_id`と`UserConnection#userId`は同じ物。この値を使って連結する。
### User テーブル
|カラム|型|補足|
|---|---|---|
|user_id|VARCHAR|primary key|
|username|VARCHAR|これもUNIQUE。<br>ログイン時にユーザーが入力する|
|display_name|VARCHAR||
|encoded_password|VARCHAR||
### UserConnection テーブル
|カラム|型|
|---|---|
|userId|VARCHAR|
|providerId|VARCHAR|
|providerUserId|VARCHAR|
|rank|INT|
|displayName|VARCHAR|
|profileUrl|VARCHAR|
|imageUrl|VARCHAR|
|accessToken|VARCHAR|
|secret|VARCHAR|
|refreshToken|VARCHAR|
|expireTime|BIGINT|

このUserConnectionテーブルだが、実はもうこの構成はほぼ固定。このテーブルを操作するソースコードは自分では全く書いておらず、全てspring任せ。spring-frameworkに[こんな感じ](https://github.com/spring-projects/spring-social/blob/v1.1.6.RELEASE/spring-social-core/src/main/resources/org/springframework/social/connect/jdbc/JdbcUsersConnectionRepository.sql)でsqlのサンプルが提供されている。コピペして使おう。

## ソースコード解説
spring-securityとspring-socialを使っている中でよく分からないのが**俺の責務はどこだ？**ということ。つまり、**springがやってくれる部分と自分がやるべき部分との境界が分かりにくい。**なのでその辺を含めて解説していく。

### 処理フローとその責任者
#### ユーザーログイン
UserをDBからロードする部分はspringはやってくれ**ない**。だからそこは`UserDetailService`というInterfaceを実装する必要がある。そうしておけば、その実装クラスが勝手に呼ばれてspringがUserのロード処理を行ってくれる。
`UserDetailsServiceImpl`のソースは[こちら](https://github.com/PonzyPon/spring-boot-social-demo/blob/master/src/main/java/com/sample/service/impl/UserDetailsServiceImpl.java)

![ユーザーログイン.png](https://qiita-image-store.s3.amazonaws.com/0/187359/90c843c7-35c0-1e1f-e9c7-6eea295139b7.png)

#### ソーシャルログイン
ソーシャルログインの場合は、SNSアカウント情報を保持している`UserConnection`テーブルに対しての存在確認や、データの登録が必要となる。ただし、これは全てspringがやってくれる。我々開発者は、Userの作成処理と、そのUserテーブルからのロード処理をやってやればよい。
`ConnectionSignUpImpl`のソースは[こちら](https://github.com/PonzyPon/spring-boot-social-demo/blob/master/src/main/java/com/sample/service/impl/ConnectionSignUpImpl.java)
`SocialUserDetailsServiceImpl`のソースは[こちら](https://github.com/PonzyPon/spring-boot-social-demo/blob/master/src/main/java/com/sample/service/impl/SocialUsersDetailServiceImpl.java)

![ソーシャルログイン.png](https://qiita-image-store.s3.amazonaws.com/0/187359/6037d748-c3b4-7af4-b060-f1e684d1e12d.png)

### その他のクラス
#### `SignupService`と`UserContext`は必須ではない
基本的には最小構成でソーシャルログインのサンプルを作成しようとしているので、不要なクラスは無いが、例えば`SignupService`は作成しなくてもよい。このInterfaceは自分で作成したものだし、`SignupServiceImpl#createUser`の処理は`ConnectionSignUpImpl#execute`に書いてしまってもよい。今回は、今後ユーザーの登録機能が付けば別クラスに分けることになるだろうと思ってクラスを分けただけ。
また、`UserContext`はログインユーザーのuserIdを簡単に取得できるよう作った便利クラス。無くてもソーシャルログインは動作する。

#### springの設定はSecurityConfigで
上でusernameとpasswordを受け取って認証する処理はspring任せという話をしたが、その設定は`SecurityConfig`で行っている。


解説はここまで。ここから下はサンプルの動かし方やいじくり方を説明します。

## サンプル君の動かし方
（自分のtwitterアプリの`Consumer Key`と`Consumer Secret`がある方は3から）

1. twitter でアプリを登録する　(https://apps.twitter.com/)
こんな感じで入力してください。
![twitter-app-create.png](https://qiita-image-store.s3.amazonaws.com/0/187359/f54de7d4-3cca-e14f-c3b2-879a5b35b53e.png)

2. アプリをCreateしたら、twitter アプリの`Consumer Key`と`Consumer Secret`を( ..)φﾒﾓﾒﾓしておく
![2018-05-19_22h12_16.png](https://qiita-image-store.s3.amazonaws.com/0/187359/5ee50acf-1db2-ab49-cf35-8442c5dac019.png)

3. git clone https://github.com/PonzyPon/spring-boot-social-demo.git
4. src/main/resources以下のapplication.properties.sampleをコピーしてapplication.propertiesを作る
5. application.properties内の`spring.social.twitter.app-id`と`spring.social.twitter.app-secret`にそれぞれ上で( ..)φﾒﾓﾒﾓした`Consumer Key`と`Consumer Secret`を書く
6. プロジェクトのルート(pom.xmlがある場所)で`mvnw spring-boot:run`を実行する（mavenインストール済の人はmvn spring-boot:runでOK）
7. localhost:7000にアクセスする
8. (\*^-^\*)

（※もし、Twitterログイン時にエラーが出たらまずはTwitterアプリの`Callback URL Locked`をNoにしてみてください。それでダメならググって解決するしかないのです(^○^)）

## サンプル君のいじり方
### Twitterでログインした後ツイートしたい
`HomeController#writeTweetIdIfLoggedInWithTwitter`というメソッドをデモ用に作成してあり、もしTwitterでログインしている場合は、そのアカウントのタイムラインの最新のつぶやきのtweetIdを取得しコンソールに書き出している。ここで使われている`twitter`変数を使えばつぶやきも可能なのだ。サンプルとしてソースに書いてあるのでコメントアウトして試してみてほしい

```java:HomeController#writeTweetIdIfLoggedInWithTwitter
// Hello!! とツイートする
twitter.timelineOperations().updateStatus("Hello!!");
```

### 現在ログインしているユーザーのIDを取得したい

`UserContext#getUserId`を呼び出してくれ。誰もログインしていない場合もあるから、戻り値は`Optional<String>`だ。使うときはこんなイメージ。

```java
public class Sample {
  @Autowired
  private final UserContext userContext;

  public Data getData() {
    dao.getData(userContext.getUserId().get());
  }
}
```

# おわりに
このサンプルを作る前の自分が見たら嬉しくなるような記事をイメージして書きました。もし間違っているところや意見・質問あればよろしくお願いします。以上です。