なんかもう辛いの一言ですね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
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
- OAuth認証に使用するインスタンスの生成
- Zaimの認証ページのURLを生成する
- 生成したURLを用いてZaimの認証ページを表示する
- 認証後、認証結果からアクセストークンを生成する
- アクセストークンにてリクエストに署名実施後、通信する
1. OAuth認証に使用するインスタンスの生成
Zaimにて新しいアプリケーションを追加した際に控えておいた情報を使って、インスタンスを生成する。
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を作っておきます。
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
/**
* 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;
}
@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とする)
provider.retrieveRequestToken(consumer, "コールバックURL");
provider.retrieveRequestToken(consumer, "http://localhost:8080/authenticated");
3. 生成したURLを用いてZaimの認証ページを表示する
上で作成したURLに遷移させます。
今回は適当なコントローラを作って、そこにアクセスするとリダイレクトする形とします。
これでhttp://localhost:8080/ にアクセスするとZaimの認証ページに飛ばされるようになります。
@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を使って、アクセストークンを生成します。
/**
* 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で再び認証することなく、アクセスができるようになります。
// accessTokenとaccessTokenSecretを生成する
provider.retrieveAccessToken(consumer, oauthVerifier);
// トークンを取得(これを保存しておくと再認証しなくていい
String accessToken = consumer.getToken();
String accessTokenSecret = consumer.getTokenSecret();
こんな感じでトークンを使用すると再認証する必要がなくなります。
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が吐かれるようになります。
/**
* 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を叩きにいきます。
今回は認証後、そのままの流れでユーザ情報を取得してみます。
/**
* 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にセットするとそのまま叩くことができます。
// 保存していたトークンをセット
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