これまでのTwitterAPI有料化の騒動について,多数の制限が設定されたことも問題ですが,開発者の視点では,「いつ何が起こるかが分からない」と常に心配しなければいけないことが,もっとも厳しいことではないかと思います。突然,サービスが停止する,APIが使えなくなる,有料になる,心配ごとは絶えないですが,数多くのユーザーとツールとして影響力を維持している限り,うまく付き合っていかなければならないことは変わりないところ。
そこで,今回はTwitterAPI2のoauth2.0 に対応しました。TwitterIDとの連携ログインは,ナラベルにとって重要な機能なので,現状のoauth1.0aが非推奨になっても運用上の支障が発生しないようにしておく必要があります。ナラベルはgrailsフレームワークで構築されているので,言語はGroovyです。Javaでの実装も同じステップだと思いますので、参考になればなによりです。
まずは,JavaのSDKの利用環境を設定します。TwitterからAPIv2用のSDKがリリースsれましますが,ステイタスはまだβ版です。
build.gradle
implementation group: 'com.github.scribejava', name: 'scribejava-core', version: '8.3.3'
implementation group: 'com.squareup.okhttp3', name: 'okhttp', version: '4.11.0'
implementation group: 'org.jetbrains.kotlin', name: 'kotlin-stdlib', version: '1.8.21'
4つのライブラリを読み込みます。
実際の連携ログインの手順としては,
-
- TwitterIDでログインボタンをユーザーがクリック
- Controllerの所定のActionへJump
- Action内でoauth2に必要なオブジェクトの生成と遷移先URLの生成
- Actionから遷移先URLへリダイレクト
- ユーザーが承認
- 前述のActionで設定されたCallBackURLへとリダイレクト
- callbackで指定されたActionで改めてTwitterAPIにアクセスしてユーザー情報取得
- OKであればログイン
といた手順を見ます。ここではoauth2の詳細については,専門ではないので割愛。
で,実装は以下のとおり。
最初に必要なものをimportします。
import com.github.scribejava.core.model.OAuth2AccessToken;
import com.github.scribejava.core.pkce.PKCE;
import com.github.scribejava.core.pkce.PKCECodeChallengeMethod;
import com.twitter.clientlib.auth.TwitterOAuth20Service;
import com.twitter.clientlib.TwitterCredentialsOAuth2;
import com.twitter.clientlib.ApiException;
import com.twitter.clientlib.model.ResourceUnauthorizedProblem;
import com.twitter.clientlib.model.Get2UsersMeResponse;
controllerのActionは以下のとおり
//*** twitterLogin :oauth2 ****************************************
def twitterLogin2(){
//*** start;
log.info("*** twitterLogin2 : LoginController *** ");
//*** build callback url;
StringBuffer callbackURL = request.getRequestURL();
int index = callbackURL.lastIndexOf("/");
callbackURL.replace(index, callbackURL.length(), "").append("/twitterCallback2");
log.info(" CallBack URL : " + callbackURL.toString());
//*** build oauth2 object
try{
TwitterOAuth20Service service = new TwitterOAuth20Service(
'twitter.ClientID', // your client id
'twitter.ClientSecrets', // your client secrets
callbackURL.toString(), // callback url
String secretState = "state";
PKCE pkce = new PKCE();
pkce.setCodeChallenge("challenge");
pkce.setCodeChallengeMethod(PKCECodeChallengeMethod.PLAIN);
pkce.setCodeVerifier("challenge");
String authorizationUrl = service.getAuthorizationUrl(pkce, secretState);
log.info(" authorization URL : " + authorizationUrl);
//*** save object; callbackで必要なobjectをsessionへ保存
def twitteroauth2=[:];
twitteroauth2.put("service",service);
twitteroauth2.put("pkce",pkce);
session.setAttribute("twitteroauth2",twitteroauth2);
//*** redirect
response.sendRedirect(authorizationUrl);
}
catch (TwitterException e) {
log.error(" *** Twitter ERR : twitterLogin2 : " + e);
return render(view:'index');
}
//*** return
return render(view:'index');
}
このActionでは、事前にTwitterDevから作成したキーなどを使って、認証先にアクセスするためのリダイレクトURLを生成して、実際にリダイレクトさせています。
次は、認証後のcallback。
//**** twitter callback ******************************************
def twitterCallback2(){
//*** start
log.info("*** twitterCallBack2 : LoginController *** ");
//*** get object from session cookie
def twitteroauth2 = session.getAttribute("twitteroauth2");
TwitterOAuth20Service service = (TwitterOAuth20Service)twitteroauth2.get("service");
PKCE pkce = twitteroauth2.get("pkce");
session.removeAttribute("twitteroauth2")
//*** null checking
if(!service || !pkce){
log.error(" *** Twitter object NULL ERR : twitterCallBack2 :");
flash.errors.add(this.message(code:"mes.err.login.object",default:"Oauth object ERRPR"));
return forward(controller:"login",action:"index");
}
if(!params.code){
log.error(" *** can not get code : callback object : twitterCallBack2 :");
return forward(controller:"login",action:"index");
}
OAuth2AccessToken accessToken = service.getAccessToken(pkce, params.code);
if(!accessToken){
log.error(" *** can not get AccessToken : callback object : twitterCallBack2 :");
return forward(controller:"login",action:"index");
}
//*** setup apiInstance;
TwitterApi apiInstance = null;
try{
apiInstance = new TwitterApi(new TwitterCredentialsOAuth2(
'twitter.ClientID', // your clientID
'twitter.ConsumerSecret', // your clientsecrets
accessToken.getAccessToken(),accessToken.getRefreshToken()));
}
catch(ApiException e){
log.error(" *** can not build apiInstance : twitterCallBack2 :");
return forward(controller:"login",action:"index");
}
//*** get user info
Set<String> userFields = new HashSet<>(Arrays.asList());
Set<String> expansions = new HashSet<>(Arrays.asList());
Set<String> tweetFields = new HashSet<>(Arrays.asList());
try {
Get2UsersMeResponse result = apiInstance.users().findMyUser()
.userFields(userFields)
.expansions(expansions)
.tweetFields(tweetFields)
.execute();
//** set user Object;
preUser.put("authid",result.data.id);
preUser.put("authnm",result.data.name);
}
catch (ApiException e) {
log.error(e.printStackTrace());
return forward(controller:"login",action:"index");
}
//*** return
return forward(controller:'login',action:'preUser');
流れとしては、oauth1.0aとほとんど変わらないので、差異、使っているAPIの関数やライブラリ。これでAPI1系が非推奨になっても、慌てずに済みそう。当面は、oauth1.0aを利用する予定。
細かい話は、本家のDocを参照してください。
developer.twitter.com
動いてしまえば、そんなに難しくないのですが、いろいろと調べるのに時間がかかってしまったのと、やはりTwitterがどのような対応をしてくるのかの様子見していた関係で、計画から実装までは結構時間がかかってしまいました。コーディング&テストは、実質1日使っていない。
万が一、Twitter連携ログインが完全に停止(もしくは有料化)となった場合には、ユーザーデータの移行も考える必要があるが、そちらの方が悩ましい。
いずれにせよ、Twitter騒動が収まりを見せていることが何よりだ。