2011年6月4日土曜日

Evernoteにアクセスする その5 OAuth理解編


OAuthサンプルによって解説しようと思っていましたが、
結局暗号化を行って、テスト書いて、色々分かりやすくコードをいじってってこの状態になりました。
以下のコードでアクセスできます。


package jp.co.ziro.evernote.controller;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
import java.util.TreeMap;
import java.util.logging.Logger;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;

import org.apache.commons.codec.binary.Base64;
import org.slim3.util.ApplicationMessage;

/**
*

* EvernoteへのOAuthアクセスクラス
* - 認証URL作成 setCallbackUrl()
* - アクセストークン取得時はsendRequest(tokenSecret)
* で処理を行います。
*
* 認証Urlの作成
* 1.コンストラクタにより、固定値を設定する
* 2.使用側は setCallbackUrl() にコールバックするUrlを設定する
* 3.createAuthorization() の戻り値が認証Urlを取得
* 4.getTokenSecret(),secretを取得(保存)
*
* 認証Urlにアクセスする
* 認可画面が表示され、そこでの結果がコールバックに渡される
*
* コールバックUrlで行う事
* 1.コンストラクタにより固定値を設定する
* 2.oauth_verifier,oauth_tokenがコールバックに呼ばれるのでそれを
* setToken() setVerifier() で設定
* verifierがnullだったら、認証が行われていない。
* 3.sendRequest()を行う。引数は認証Url時に作成したsecret
* 4.oauth_token にリクエストトークン、edam_shard にAPIアクセス用のIdが渡されます。
*
* これらでthriftのAPIを利用して、Evernoteにアクセスできます。
*
*

* @author secondarykey
*/
public class EvernoteRequest {

@SuppressWarnings("unused")
private static Logger logger = Logger.getLogger(EvernoteRequest.class.getName());

/**
* EverNoteのAPI ConsumerKey
*/
private static final String CONSUMERKEY = ApplicationMessage
.get("evernote.api.key");
/**
* EvernoteのAPI ConsumerSecret
*/
private static final String CONSUMERSECRET = ApplicationMessage
.get("evernote.api.secret");

/**
* アクセスするUrl
*/
private static final String BASEURL = ApplicationMessage
.get("evernote.api.baseUrl");

/**
* リクエストToken取得用のUrl
*/
private static final String requestUrl = BASEURL
+ ApplicationMessage.get("evernote.api.requestTokenUrl");

/**
* 認証のUrl
*/
private static final String authorizationUrlBase = BASEURL + ApplicationMessage.get("evernote.api.oauthUrl");

/**
* 文字コード
*/
private static final String CHARSET = "UTF-8";
/**
* キー作成用のGETメソッド名
*/
private static final String REQUEST_METHOD = "GET";
/**
* OAuth nonces用のランダム値生成
*/
private static final Random random = new Random();


/**
* ベースのURL
* @return リクエストするBaseUrl
*/
public static String getBaseUrl() {
return BASEURL;
}

/**
* リクエストするパラメーター
*/
private Map parameters = new TreeMap();

/**
* TokenSecret
* getTokenSecret()時に空の場合、Exceptionを発生
*/
private String tokenSecret = null;
/**
*

* コンストラクタ
* パラメータ固定値を設定する
*

*/
public EvernoteRequest() {
setParameter("oauth_consumer_key", CONSUMERKEY);
setParameter("oauth_timestamp", getTimestamp());
setParameter("oauth_nonce", Long.toHexString(random.nextLong()));
setParameter("oauth_version", "1.0");
//暗号化を行う
setParameter("oauth_signature_method", "HMAC-SHA1");
}
/**
* タイムスタンプを作成
* @return 現在時刻の秒をString化した値
*/
private String getTimestamp() {
return Long.toString(System.currentTimeMillis() / 1000);
}

/**
* コールバックURLを設定する
* @param callbackUrl コールバックUrl
*/
public void setCallbackUrl(String callbackUrl) {
setParameter("oauth_callback", callbackUrl);
}

/**
* リクエストトークンを設定
* @param requestToken コールバックされたリクエストトークン
*/
public void setToken(String requestToken) {
//それぞれをリクエスターに設定
setParameter( "oauth_token", requestToken);
}

/**
* Verifierの設定
* @param verifier コールバックされたverifier値
*/
public void setVerifier(String verifier) {
setParameter( "oauth_verifier", verifier);
}
/**
* リクエスト時の引数設定
* @param name パラメータ名称
* @param value パラメータの値
*/
private void setParameter(String name, String value) {
parameters.put(name, value);
}

/**
* Encodes this request as a single URL that can be opened.
*/
private String getUrl() {
return requestUrl + "?" + join();
}

/**
* HTTP引数の連結
* @return Url引数にして返す
*/
private String join() {
StringBuilder sb = new StringBuilder();
//開始フラグ
boolean firstParam = true;

//引数数繰り返す
for (Map.Entry parameter : parameters.entrySet()) {
//最初の処理
if (firstParam) {
firstParam = false;
} else {
sb.append('&');
}

//キー値を取得
sb.append(parameter.getKey());
//連結子を設定
sb.append('=');
try {
//値をエンコードして設定
sb.append(URLEncoder.encode(parameter.getValue(), CHARSET));
} catch (UnsupportedEncodingException e) {
throw new RuntimeException("エンコード時の例外", e);
}
}
return sb.toString();
}

/**
* リクエストを行う
* @param
*
* @throws IOException
* if a problem occurs making the request or getting the reply.
*/
public Map sendRequest(String tokenSecret) {
//Signatureを設定
setParameter("oauth_signature", createSignature(tokenSecret));
//アクセスする
HttpURLConnection connection;
try {
connection = (HttpURLConnection) (new URL(getUrl())).openConnection();
} catch (MalformedURLException e) {
throw new RuntimeException("アクセスに対する例外",e);
} catch (IOException e) {
throw new RuntimeException("アクセスに対する例外",e);
}

int responseCode;
String responseMessage;
try {
responseCode = connection.getResponseCode();
responseMessage = connection.getResponseMessage();
} catch (IOException e) {
throw new RuntimeException("結果取得を行う",e);
}
// リクエストが正常じゃない場合
if (responseCode != HttpURLConnection.HTTP_OK) {
throw new RuntimeException("Server returned error code: " + responseCode + " " + responseMessage);
}

return createResponseMap(connection);
}

/**
*

* 暗号化を行う
* 1. メソッド名&アクセスUrl&HTTPの引数の文字列を作成
* 2. secretをTokenと連結してsecretをバイト化
* 3. Secret値を元にSignatureStringを暗号化を行う
* 4. その値をBase64で16進数かする
*

* @param tokenSecret リクエストToken取得時に発行されたSecret
* @return 暗号化したSecret値
*/
private String createSignature(String tokenSecret) {

String encodeUrl = encode(requestUrl);
String encodeJoin = encode(join());
String signatureBaseString = REQUEST_METHOD + "&" + encodeUrl + "&" + encodeJoin;

// 署名対象のテキストを作成
String secret = CONSUMERSECRET + "&";
//TokenSecretが存在する場合
if (tokenSecret != null) {
secret += tokenSecret;
}
byte[] secretyKeyBytes;
try {
secretyKeyBytes = secret.getBytes(CHARSET);
} catch (UnsupportedEncodingException e2) {
throw new RuntimeException("エンコード時の失敗",e2);
}

String hmac = "HmacSHA1";
SecretKeySpec secretKeySpec = new SecretKeySpec(secretyKeyBytes,hmac);
Mac mac;
try {
mac = Mac.getInstance(hmac);
mac.init(secretKeySpec);
} catch (NoSuchAlgorithmException e1) {
throw new RuntimeException("暗号化インスタンス取得失敗",e1);
} catch (InvalidKeyException e1) {
throw new RuntimeException("暗号化インスタンス取得失敗",e1);
}

byte[] data;
try {
data = signatureBaseString.getBytes(CHARSET);
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(CHARSET + " is unsupported!", e);
}
byte[] rawHmac = mac.doFinal(data);
Base64 encoder = new Base64();

return new String(encoder.encode(rawHmac));
}

/**
* 値のUrlエンコード
* @param value エンコードする値
* @return エンコードした値
*/
private String encode(String value) {
String rtnData;
try {
rtnData = URLEncoder.encode(value,CHARSET);
} catch (UnsupportedEncodingException e) {
throw new RuntimeException("エンコード時の例外",e);
}
return rtnData;
}
/**
* レスポンスの値を取得
*
* @param connection
* @return
*/
private Map createResponseMap(HttpURLConnection connection) {

BufferedReader bufferedReader;
String result;
try {
bufferedReader = new BufferedReader(new InputStreamReader(
connection.getInputStream()));
result = bufferedReader.readLine();
} catch (IOException e) {
throw new RuntimeException("読み込み時の例外", e);
}

Map responseParameters = new HashMap();
for (String param : result.split("&")) {
int equalsAt = param.indexOf('=');
if (equalsAt > 0) {
String name = param.substring(0, equalsAt);
String value;
try {
value = URLDecoder.decode(param.substring(equalsAt + 1),
CHARSET);
} catch (UnsupportedEncodingException e) {
throw new RuntimeException("エンコード時の例外", e);
}
responseParameters.put(name, value);
}
}
return responseParameters;
}

/**
* 認証Urlの作成
* @return
*/
public String createAuthorizarionUrl() {
//リクエスト
Map reply = sendRequest(null);
String requestToken = reply.get("oauth_token");
//認証Urlの作成
String authorizationUrl = authorizationUrlBase + "?oauth_token=" + requestToken;
//TokenSecretを設定する
tokenSecret = reply.get("oauth_token_secret");
return authorizationUrl;
}

/**
* TokenSecretの取得
* @return
*/
public String getTokenSecret() {
if ( tokenSecret == null ) {
throw new NullPointerException("TokenSecretが設定されていない");
}
return tokenSecret;
}


}



まずコンストラクタで各種固定値の設定を行います。


  • oauth_consumer_key:KEY値

  • oauth_signature_method:「PLANTEXT」「HMAC-SHA1」のどちらか

  • oauth_timestamp:現在の秒

  • oauth_nonce:ランダムの値(リクエスト毎に一意)

  • oauth_version:「1.0」固定



Evernoteのサンプルではoauth_signatureにSECRET値を指定していますが、
HMAC-SHA1の際は暗号化を行うので、まだ指定していません。
oauth_signature_methodはPLANTEXT、HMAC-SHA1が存在し、サンプルはPLANTEXTが指定されています。
PLANTEXTを推奨(つうか使って)しているのも珍しい気もします。

HTTPにアクセスする時に利用するMapをTreeMapを利用していますが
暗号化時に引数をソートする必要があるのでTreeMapでKey値のソートを行っているわけです。

コンストラクタで設定した後はコールバックUrl(oauth_callback)を指定します。
Evernoteの認可画面により処理が行われるとそのUrlに戻ってきます。

認可画面に行くためにリクエストトークンを取得しに行きます。
createAuthorizarionUrl()ですね。

ここでsendRequest()でEvernoteにアクセスしています。
oauth_signatureをcreateSignatuer()を行ってHMAC-SHA1の暗号化を行って指定しています。

返ってきたResponseから値を取得します。
レスポンスにリクエストトークン(oauth_token)とSecret(oauth_token_secret)が指定されています。
リクエストトークンはhttps://sandbox.evernote.com/?oauth_token=xxxxとして利用すると認可画面にアクセスする事ができます。
Secret値は、アクセストークン時にシグネチャ値のキー値とする時にConsumerSecretと合わせて使用します。

ここまでが認可画面のリンクを作成する流れです。

コールバックURLを指定していますが、認可画面にアクセスし、操作を行うとこのUrlに戻ってきます。
そちらの処理が以下になります。

認証に成功するとコールバックURLにoauth_token、oauth_verifierが付与されて来ます。
oauth_verifierが存在しない場合は認証で拒否された等ですので、エラー画面等を出力しましょう。

設定されていたら、アクセストークンを取得する為、
このクラスをnewしてインスタンス化します。※設定する値は一緒。

oauth_tokenをsetToken() ,oauth_verifierをsetVerifier()を利用して、設定します。
設定したらsendRequest()によりアクセストークンを取得するのですが、
この時に最初のアクセスで取得したtokenSecretを渡します。

取得に成功するとoauth_tokenにアクセストークン、edam_shardにEvernoteにアクセスする時に必要な値が入ってきます。
このアクセストークンさえあれば何でもできるわけです。


その値を利用してAPIのアクセスです。
thriftのライブラリを使っています。


String noteStoreUrl = noteStoreUrlBase + shardId;
THttpClient noteStoreTrans = new THttpClient(noteStoreUrl);
TBinaryProtocol noteStoreProt = new TBinaryProtocol(noteStoreTrans);

NoteStore.Client noteStore =
new NoteStore.Client(noteStoreProt, noteStoreProt);
List notebooks = noteStore.listNotebooks(accessToken);
for (Notebook notebook : notebooks) {
logger.info("Notebook: " + notebook.getName());
}


サンドボックス+"/edam/note/"+edam_shardの値を作成して
それでTHttpClientを生成、それを利用してTBinaryProtocolを生成しています。

それを利用して生成しているNoteStore.ClientはEvernoteのライブラリですね。
生成したオブジェクトでAPI呼び出しを行っています。
ここではlistNotebooks()です。そこにアクセストークンを渡してアクセスしています。


・・・さて、再度理解を深める為にOAuthをしっかり説明してみました。
説明がヘタすぎますけど、ご了承ください。

まぁとにかく次回はAPIで色々やってみます。

0 件のコメント: