2013年6月18日火曜日

GAE + Go で Sign in with Twitter (4)

Authentication Header が作れたので、リクエストを送信する部分を作ります。
OAuth 1.0a の通信の流れを簡単に描くと以下の用になります。





まず、リクエストトークンを要求します。事前に Twitter へ登録しているアプリであればリクエストトークンがもらえます。登録されたアプリかどうかの判断は、Authorization ヘッダによって判断されます。登録されたアプリであると判断されたらリクエストトークンが帰ってきます。

次に、データのアクセスについてユーザの同意を得ます。Twitter のログインページヘリダイレクトして、Twitter へのログイン・ユーザデータへのアクセス許可をしてもらいます。リダイレクトする際にリクエストトークン(未認証)を渡すと、アクセス許可された後ににリクエストトークン(認証済)が帰ってきます。

次に、認証済のリクエストトークンとアクセストークンを交換します。アクセストークンとはユーザのデータへアクセスするために使うパスワードのようなものです。Twitter ではアクセストークンと一緒にユーザのID、ユーザ名が返ってくるため、Twitter アカウントを使ってログインするだけならここまでで OK です。

それ以降は、アクセストークンを使ってユーザのデータへアクセスすることができます。今回はアクセスしませんのでこちらはやらないです。


HTTP リクエスト送信処理

まず、HTTP リクエストを送ってレスポンスを得る汎用的なメソッドを書きます。
すべてのリクエストには Authorization Header が含まれるので、先日作った createHeader() メソッドを使います。
/**
 * リクエストを送信してレスポンスを受信する
 * メソッドは POST 固定
 * @method
 * @memberof OAuth
 * @param {string} targetUrl 送信先
 * @param {string} body リクエストボディ
 * @returns {string} レスポンス
 */
func (this *OAuth) request(targetUrl string, body string) string {

    // リクエストごとに変わるパラメータを設定
    this.params["oauth_nonce"] = this.createNonce()
    this.params["oauth_timestamp"] = strconv.Itoa(int(time.Now().Unix()))
    this.params["oauth_signature"] = this.createSignature(targetUrl)
 
    // Authorization Header を作成
    header := this.createHeader()
 
    // リクエストの作成
    var request *http.Request
    var err error
    if body == "" {
        request, err = http.NewRequest("POST", targetUrl, nil)
    } else {
        request, err = http.NewRequest("POST", targetUrl, NewReader(body))
    }
    check(this.context, err)
    request.Header.Add("Authorization", header)
 
    // リクエストの送信とレスポンスの受信
    client := urlfetch.Client(this.context)
    response, err := client.Do(request)
    check(this.context, err)
 
    // レスポンスボディの読み取り
    result := make([]byte, 256)
    response.Body.Read(result)
 
    return string(result)
}

エラーチェックのために OAuth クラスへ context を追加したり細かい修正をしていますが省きます。詳しくは Github のコードをご覧ください。

Go から HTTP リクエストを作成する場合、普通は net/http パッケージを使うのですが、GAE 上で動いている場合には GAE の appengine/urlfetch ライブラリを使うことになっています。

 URL Fetch Go API Overview - Google App Engine
 https://developers.google.com/appengine/docs/go/urlfetch/

また、リクエストボディは io.Reader インタフェースを実装している必要があるため、Read メソッドを実装します。
/**
 * リクエストボディ用のリーダー
 * request() で body を送信するために使う
 * @class
 * @member {[]byte} body 本文
 * @member {int} pointer 何バイト目まで読み込んだか表すポインタ
 */
type Reader struct {
 io.Reader
 body []byte
 pointer int
}

/**
 * Reader のインスタンスを作成する
 * @param {string} body 本文
 * @returns {*Reader} 作成したインスタンス
 */
func NewReader(body string) *Reader {
 reader := new(Reader)
 reader.body = []byte(body)
 reader.pointer = 0
 return reader
}


/**
 * 本文を読み出す
 * 2回目以降は前回の続きから読み出せる
 * @method
 * @memberof *Reader
 * @param {[]byte} p 読みだしたデータの保存先
 * @returns {int} 読みだしたバイト数
 * @returns {error} エラー
 */
func (this *Reader) Read(p []byte) (int, error) {
 var l int
 var err error
 if this.pointer + len(p) < len(this.body) {
  l = len(p)
  err = nil
 } else {
  l = len(this.body) - this.pointer
  err = io.EOF
 }
 
 for i := 0; i < l; i++ {
  p[i] = this.body[i + this.pointer]
 }
 
 this.pointer = l + this.pointer
 
 return l, err
}


このリクエスト送信メソッドを使って実際にリクエストを送る部分を書きます。


リクエストトークンの要求

まず最初に、リクエストトークンの要求です。
Twitter の場合、https://api.twitter.com/oauth/request_token にリクエストを送信します。
/**
 * Twitter へリクエストトークンを要求する
 * @method
 * @memberof OAuth
 * @returns {map[string]string} リクエスト結果
 */
func (this *OAuth) requestToken() map[string]string {
    response := this.request("https://api.twitter.com/oauth/request_token", "")
    datas := strings.Split(response, "&")
    result := make(map[string]string, len(datas))
    for i := 0; i < len(datas); i++ {
        data := strings.Split(datas[i], "=")
        result[data[0]] = data[1]
    }
 
    return result
}

レスポンスボディにリクエストトークンが入っています。
パラメータ名=値&パラメータ名=値
という感じになっているのでパラメータ毎に切り離して返します。


ユーザの許可を得る

次に、ユーザ認証画面へリダイレクトします。
Sign in with Twitter ボタンが押された時にリダイレクトするようにしましょう。
リダイレクト先は
https://api.twitter.com/oauth/authenticate?oauth_token=***
です。***にはリクエストトークンが入ります。

ユーザが Twitter へログインして、データへのアクセスを許可します。
許可が完了したら oauth_callback へリダイレクトされます。
このとき、パラメータとして認証済リクエストトークン(oauth_token) が渡されるので取り出します。

/**
 * OAuthで他のサイトでログインしてから戻ってきた時
 * @method
 * @memberof Controller
 * @param {http.ResponseWriter} w 応答先
 * @param {*http.Request} r リクエスト
 */
func (this *Controller) oauthCallback(w http.ResponseWriter, r *http.Request) {
    token := r.FormValue("oauth_token")
    verifier := r.FormValue("oauth_verifier")
 
    c := appengine.NewContext(r)
    oauth := NewOAuth(c)
    result := oauth.exchangeToken(token, verifier, "https://api.twitter.com/oauth/access_token")
    view := new(View)
    view.login(c, w)
    fmt.Fprintf(w, "あなたのidは %s です
あなたのユーザ名は %s です", result["user_id"], result["screen_name"])
 
    u, _ := user.CurrentOAuth(c, "")
    log.Printf("ユーザ: %#v", u)
}

この中で、認証済みリクエストトークンとアクセストークンを交換しています。
/**
 * リクエストトークンをアクセストークンに変換する
 * @memberof OAuth1
 * @method
 * @param {string} token リクエストトークン
 * @param {string} verifier 認証データ
 * @param {string} targetUrl リクエストの送信先
 * @returns {map[string]string} アクセストークンとユーザデータ
 */
func (this *OAuth1) exchangeToken(token string, verifier string, targetUrl string) map[string]string {
    this.params["oauth_token"] = token
    body := fmt.Sprintf("oauth_verifier=%s", verifier)
    response := this.request(targetUrl, body)
 
    datas := strings.Split(response, "&")
    result := make(map[string]string, len(datas))
    for i := 0; i < len(datas); i++ {
        data := strings.Split(datas[i], "=")
        result[data[0]] = data[1]
    }
    return result
}

アクセストークンと一緒に、ユーザ名(screen_name)、ユーザID(user_id)が帰ってくるので、これをログインデータとして使えばOKです。

0 件のコメント:

コメントを投稿