JavaでZaimのAPI(OAuth1.0)を叩く

なんかもう辛いの一言ですねOAuth1.0…。
Springをあんまり使ったことがないのでよく分からないんですが、API叩く時はRestTemplateを使うといいみたいですね。
でもOAuth1.0だとうまくハマらなくて、結局HttpClientを使う感じになりました。
SpringでOAuth1.0のAPIを叩くときのベストプラクティスってあるんでしょうか。。。
もっといい方法があるよって方はご教授ください。

（2018/08/06追記）
spring-security-oauthというSpring Securityの姉妹プロジェクトを使うとOAuth1.0もうまくいけるみたいです。
https://projects.spring.io/spring-security-oauth/docs/oauth1.html
巷ではOAuth2を実装してみたみたいなのは溢れてますが1.0は本当に情報がすくないですね。

## やりたいこと
ZaimのOAuth1.0のAPIを叩いてデータを取得する。

## 使う
- Java 8
- Spring Boot 2.0.1
- Gradle
- Apache HttpClient 4.5.5
- OAuth Signpost（OAuth1.0ライブラリ）
 - signpost-core 1.2.1.2  
 - signpost-commonshttp4 1.2.1.2


## Gradle
dependencyはこんな感じです。
以下は必須で、ほかは好みで適当に。

- spring-boot-starter-web
- signpost-core:1.2.1.2
- signpost-commonshttp4:1.2.1.2

```groovy:build.gradle
dependencies {
	// spring-boot
	compile('org.springframework.boot:spring-boot-starter-thymeleaf')
	compile('org.springframework.boot:spring-boot-starter-web')
	compile('org.springframework.boot:spring-boot-devtools')
	compileOnly('org.projectlombok:lombok')
	testCompile('org.springframework.boot:spring-boot-starter-test')
	// OAuth1.0a
	compile('oauth.signpost:signpost-core:1.2.1.2')
	compile('oauth.signpost:signpost-commonshttp4:1.2.1.2')
}
```

## Zaimでアプリケーション登録
以下で新しいアプリケーションを追加する。
https://dev.zaim.net

追加すると以下の情報がゲットできるので控えておく。

- コンシューマ ID
- コンシューマシークレット
- リクエストトークン取得 URL
- 認証 URL
- アクセストークン取得 URL

コンシューマ IDとコンシューマシークレットは漏洩しないよう気をつける。

## 処理の流れ
OAuth1.0についてはここが詳しい。
http://yuroyoro.hatenablog.com/entry/20100506/1273137673

1. OAuth認証に使用するインスタンスの生成
2. Zaimの認証ページのURLを生成する
3. 生成したURLを用いてZaimの認証ページを表示する
4. 認証後、認証結果からアクセストークンを生成する
5. アクセストークンにてリクエストに署名実施後、通信する

### 1. OAuth認証に使用するインスタンスの生成
Zaimにて新しいアプリケーションを追加した際に控えておいた情報を使って、インスタンスを生成する。

```java:
private static final String CONSUMER_KEY = "Zaimの登録情報";
private static final String CONSUMER_SECRET = "Zaimの登録情報";
private static final String REQUEST_TOKEN_URL = "Zaimの登録情報";
private static final String AUTHORIZE_URL = "Zaimの登録情報";
private static final String ACCESS_TOKEN_URL = "Zaimの登録情報";

private OAuthConsumer consumer;
private OAuthProvider provider;

consumer = new CommonsHttpOAuthConsumer(CONSUMER_KEY, CONSUMER_SECRET);
provider = new CommonsHttpOAuthProvider(REQUEST_TOKEN_URL, ACCESS_TOKEN_URL, AUTHORIZE_URL);
```

使う分にはこれで十分ですが、この辺の情報はもちろんプロパティファイルとかに外出しすべきです。
プロパティ用のBeanを作っておきます。

```properties:application.properties
zaim.consumerKey=your_consumerKey
zaim.consumerSecret=your_consumerSecret
zaim.requestTokenUrl=https://api.zaim.net/v2/auth/request
zaim.authorizeUrl=https://auth.zaim.net/users/auth
zaim.accessTokenUrl=https://api.zaim.net/v2/auth/access
```

```java:OAuthProperty
/**
 * OAuth認証に必要な情報をプロパティファイルより取得し保持
 */
public class OAuthProperty {

	@Getter
	@Value("${zaim.consumerKey}")
	private String consumerKey;

	@Getter
	@Value("${zaim.consumerSecret}")
	private String consumerSecret;

	@Getter
	@Value("${zaim.requestTokenUrl}")
	private String requestTokenUrl;

	@Getter
	@Value("${zaim.authorizeUrl}")
	private String authorizeUrl;

	@Getter
	@Value("${zaim.accessTokenUrl}")
	private String accessTokenUrl;

}
```

```java:BeanConfig.java
@Configuration
public class BeanConfig {

	@Bean
	OAuthProperty oAuthProperty() {
		return new OAuthProperty();
	}

	// accessTokenがユーザ毎に違うため、せめてsession毎に持つ必要あり
	@Bean
	@Scope(value = "session", proxyMode = ScopedProxyMode.TARGET_CLASS)
	OAuthConsumer oAuthConsumer(OAuthProperty oAuthProperty) {
		return new CommonsHttpOAuthConsumer(oAuthProperty.getConsumerKey(),
				oAuthProperty.getConsumerSecret());
	}

	// accessTokenがユーザ毎に違うため、せめてsession毎に持つ必要あり
	@Bean
	@Scope(value = "session", proxyMode = ScopedProxyMode.TARGET_CLASS)
	OAuthProvider oAuthProvider(OAuthProperty oAuthProperty) {
		return new CommonsHttpOAuthProvider(oAuthProperty.getRequestTokenUrl(),
				oAuthProperty.getAccessTokenUrl(), oAuthProperty.getAuthorizeUrl());
	}

}
```


### 2. Zaimの認証ページのURLを生成する

作成したインスタンスを使用して、認証ページのURLを生成します。
Zaimのページで認証後、返ってくるURLをコールバックURLに設定します。
（例として、この後作成するコントローラのURLとする）

```java:
provider.retrieveRequestToken(consumer, "コールバックURL");
```

```java:例
provider.retrieveRequestToken(consumer, "http://localhost:8080/authenticated");
```

### 3. 生成したURLを用いてZaimの認証ページを表示する
上で作成したURLに遷移させます。
今回は適当なコントローラを作って、そこにアクセスするとリダイレクトする形とします。
これでhttp://localhost:8080/ にアクセスするとZaimの認証ページに飛ばされるようになります。

```java:indexController
@Controller
public class IndexController {

	@Autowired
	OAuthConsumer consumer;

	@Autowired
	OAuthProvider provider;

	@RequestMapping("/")
	public String index() throws Exception {
		// 認証用URL生成
		String URL = provider.retrieveRequestToken(consumer, "http://localhost:8080/authenticated");
		return "redirect:" + URL;
	}
}
```

### 4. 認証後、認証結果からアクセストークンを生成する
Zaimでログインパスワードを入力して認証が完了すると、”Zaimの認証ページのURLを生成する”で設定していたコールバックURLに返ってきます。
この時、以下2つのパラメータが付随して返却されるので取得します。

- oauth_token
- oauth_verifier

このoauth_verifierを使って、アクセストークンを生成します。

```java:
	/**
	 * zaimで認証後のコールバックでのアクセス
	 * @param oauthToken zaimより返却
	 * @param oauthVerifier zaimより返却
	 * @return
	 * @throws Exception
	 */
	@RequestMapping("/authenticated")
	public String authenticated(@RequestParam(value = "oauth_token") String oauthToken,
			@RequestParam(value = "oauth_verifier") String oauthVerifier, Model model)
			throws Exception {

		// accessTokenとaccessTokenSecretを生成する
		provider.retrieveAccessToken(consumer, oauthVerifier);


		return "index";
	}
```

このように生成したアクセストークンは取得することができます。
この2つを保存しておくことで、zaimで再び認証することなく、アクセスができるようになります。

```java:
		// accessTokenとaccessTokenSecretを生成する
		provider.retrieveAccessToken(consumer, oauthVerifier);

		// トークンを取得（これを保存しておくと再認証しなくていい
		String accessToken = consumer.getToken();
		String accessTokenSecret = consumer.getTokenSecret();
```

こんな感じでトークンを使用すると再認証する必要がなくなります。

```java:こんなかんじで使う
		consumer = new CommonsHttpOAuthConsumer(CONSUMER_KEY, CONSUMER_SECRET);
		provider = new CommonsHttpOAuthProvider(REQUEST_TOKEN_URL, ACCESS_TOKEN_URL, AUTHORIZE_URL);

		// 保存していたトークンをセット
		consumer.setTokenWithSecret(accessToken, accessTokenSecret);
```

### アクセストークンにてリクエストに署名実施後、通信する
アクセストークンが取得できたので、ようやくZaimにAPIを叩きにいけるようになりました。
まずはHttpClientの準備をします。

HttpClient公式ページにいい感じのエラーハンドラーがあったのでそれをそのまま使います。
API叩いた時のレスポンスコードが200系でなければExceptionが吐かれるようになります。

```Java:APIResponseHandler
/**
 * API叩くときのレスポンスコードチェック
 * 問題なければBodyを返却
 * 正常じゃないときはExceptionとする
 * https://hc.apache.org/httpcomponents-client-ga/httpclient/examples/org/apache/http/examples/client/ClientWithResponseHandler.java
 */
public class APIResponseHandler implements ResponseHandler<String> {

	@Override
	public String handleResponse(
			final HttpResponse response) throws ClientProtocolException, IOException {
		int status = response.getStatusLine().getStatusCode();
		if (status >= 200 && status < 300) {
			HttpEntity entity = response.getEntity();
			return entity != null ? EntityUtils.toString(entity) : null;
		} else {
			throw new ClientProtocolException("Unexpected response status: " + status);
		}
	}

}
```
準備ができたので、APIを叩きにいきます。
今回は認証後、そのままの流れでユーザ情報を取得してみます。

```java:
	/**
	 * zaimで認証後のコールバックでのアクセス、その後ユーザ情報を取得
	 * @param oauthToken zaimより返却
	 * @param oauthVerifier zaimより返却
	 * @return
	 * @throws Exception
	 */
	@RequestMapping("/authenticated")
	public String authenticated(@RequestParam(value = "oauth_token") String oauthToken,
			@RequestParam(value = "oauth_verifier") String oauthVerifier, Model model)
			throws Exception {

        // accessTokenとaccessTokenSecretを生成する
        provider.retrieveAccessToken(consumer, oauthVerifier);

        // HttpClient準備
        CloseableHttpClient httpclient = HttpClients.createDefault();
        ResponseHandler<String> responseHandler = new APIResponseHandler();

        // リクエストに署名する
        HttpGet httpget = new HttpGet("https://api.zaim.net/v2/home/user/verify");
        consumer.sign(httpget);

        // 取得
        String responseBody = httpclient.execute(httpget, responseHandler);
        model.addAttribute("zaimUserVerify", responseBody);

		return "index";
	}
```
これでログイン情報のJSONが返却されたかと思います。

## 再認証せずにAPIを叩く

認証後、再認証せずにAPIを叩く場合は、保存していたaccessTokenとaccessTokenSecretをconsumerにセットするとそのまま叩くことができます。

```java:accessTokenとaccessTokenSecretを使用する場合

        // 保存していたトークンをセット
        consumer.setTokenWithSecret(accessToken, accessTokenSecret);

        // HttpClient準備
        CloseableHttpClient httpclient = HttpClients.createDefault();
        ResponseHandler<String> responseHandler = new APIResponseHandler();

        // リクエストに署名する
        HttpGet httpget = new HttpGet("https://api.zaim.net/v2/home/user/verify");
        consumer.sign(httpget);

        // 取得
        String responseBody = httpclient.execute(httpget, responseHandler);
        model.addAttribute("zaimUserVerify", responseBody);
```

## さいごに
こんな感じでJavaでZaimのAPI(OAuth1.0)を叩くことができました。

SpringのRestTemplateを使うと返ってきたJSONをオブジェクトにマッピングして返してくれたり、めちゃくちゃ便利みたいですが、どうにも使えず。。
もう少しいい感じにOAuth1.0使えないですかね。。

ひとまず自分なりに実装してみたものを置いておきます。
参考になれば幸いです。
https://github.com/kxn4t/saifu