[OAuth Series] 使用 Access Token 存取 Private APIs

在前一回完成了整個 OAuth 驗證與授權的流程後,程式應該可以成功取得 Access Token 以及 Access Token Secret,只要有這兩個資料,應用程式就可以以 Access Token 所代表的使用者來與 OAuth 服務的 Private API 來互動,大多數 OAuth 服務上的 API 都會需要先取得 Access Token 後才可以使用 (雖然還是有少數可以不用啦),所以這篇文章就來說明怎麼使用 Access Token 來存取 Private APIs。

前一回完成了整個 OAuth 驗證與授權的流程後,程式應該可以成功取得 Access Token 以及 Access Token Secret,只要有這兩個資料,應用程式就可以以 Access Token 所代表的使用者來與 OAuth 服務的 Private API 來互動,大多數 OAuth 服務上的 API 都會需要先取得 Access Token 後才可以使用 (雖然還是有少數可以不用啦),所以這篇文章就來說明怎麼使用 Access Token 來存取 Private APIs。

延續前一回文章所用的資料結構以及函數,前面已經做了 RenderOAuthAuthorizationHeaderForRequestToken() 以及 RenderOAuthAuthorizationHeaderForAccessToken(),現在我在程式中加入一個新的函數,以支援 Private API 的呼叫:

public virtual string RenderOAuthAuthorizationHeaderForPerformRequest()
{
    StringBuilder headerBuilder = new StringBuilder();
    Dictionary<string, string> oauthDictionary =
        this.GetDictionaryFromOAuthData(OAuthTagRenderPhraseEnum.PerformRequest, true);

    oauthDictionary["oauth_signature"] = OAuthUtility.UrlEncode(oauthDictionary["oauth_signature"]);

    foreach (KeyValuePair<string, string> oauthParamItem in oauthDictionary)
    {
        if (headerBuilder.Length == 0)
            headerBuilder.Append(oauthParamItem.Key + "=\"" + oauthParamItem.Value + "\"");
        else
            headerBuilder.Append("," + oauthParamItem.Key + "=\"" + oauthParamItem.Value + "\"");
    }

    return "OAuth " + headerBuilder.ToString();
}

在存取 Private API 時,需要的 OAuth 參數有 oauth_nonce, oauth_consumer_key, oauth_timestamp, oauth_signature_method, oauth_version, oauth_signature, oauth_token 等,RenderOAuthAuthorizationHeaderForPerformRequest() 所做的事,就是將這些參數組成一個 base string,再使用 Consumer Key 和 Access Token Secret 產生簽章後,附加到 OAuth Authorization 標頭中。

準備好 OAuth 參數後,就可以撰寫出 PerformRequest() 來呼叫 OAuth Private APIs:

public override string PerformRequest(string RequestUrl)
{
    HttpWebRequest request = HttpWebRequest.Create(RequestUrl) as HttpWebRequest;
    HttpWebResponse response = null;
    string responseData = null;
    ServicePointManager.Expect100Continue = false;

    request.Method = "GET";

    this._oauthDataRepository.MakeNewRequestParams();
    this._oauthDataRepository.PrepareRequestSignature(
        new Uri(RequestUrl), "GET", OAuthTagRenderPhraseEnum.PerformRequest);

    request.Headers.Add("Authorization",
        this._oauthDataRepository.RenderOAuthAuthorizationHeaderForPerformRequest());

    try
    {
        response = request.GetResponse() as HttpWebResponse;

        StreamReader sr = new StreamReader(response.GetResponseStream());
        responseData = sr.ReadToEnd();
        sr.Close();
    }
    catch (WebException we)
    {
        response = we.Response as HttpWebResponse;

        StreamReader sr = new StreamReader(response.GetResponseStream());
        responseData = sr.ReadToEnd();
        sr.Close();

        if (response.StatusCode == HttpStatusCode.Unauthorized)
            throw new OAuthUnauthorizedException("ERROR_OAUTH_UNAUTHORIZED",
                this._oauthDataRepository.GetOAuthSiguatureBase(
                    new Uri(RequestUrl), "GET", OAuthTagRenderPhraseEnum.PerformRequest),
                this._oauthDataRepository.Signature,
                responseData);
        else
            throw new OAuthNetworkException("ERROR_NETWORK_PROBLEM", response.StatusCode, responseData);

    }
    catch (Exception e)
    {
        throw new OAuthException("ERROR_EXCEPION_OCCURRED", e);
    }
    finally
    {
        response.Close();
    }

    response = null;
    request = null;

    return responseData;
}

不過 OAuth 的參數在每個 OAuth 服務間有一些不同的設定,例如 Yahoo 要在 OAuth 參數中加一個 realm 的設定,遇到這種情況時就只能依服務來修改程式碼,不能全部沿用。

另外,Access Token 和 Access Token Secret 預設是永不過期的,除非使用者撤銷對應用程式的授權,因此應用程式可以將取得的 Access Token 和 Access Token Secret 存起來,日後直接使用 Access Token 即可存取 OAuth 服務的 Private APIs,儲存的方法可以是 Database, XML, Text Files 或其他任何可保存的方法,例如下列程式:

public class PersistXmlProvider : IPersistProvider
{
    private static string AccessTokensFileName = "OAuthAccessTokens.dat";

    public void PersistAccessToken(string ProviderType, string AccessToken, string TokenSecret, DateTime ExpireDate)
    {
        XmlDocument accessTokenStorage = new XmlDocument();

        if (File.Exists(Environment.CurrentDirectory + @"\" + PersistXmlProvider.AccessTokensFileName))
            accessTokenStorage.Load(Environment.CurrentDirectory + @"\" + PersistXmlProvider.AccessTokensFileName);
        else
            accessTokenStorage.LoadXml("<tokens></tokens>");

        XmlNode nodeAccessToken = accessTokenStorage.DocumentElement.SelectSingleNode("//tokens/token[@provider='" + ProviderType + "']");
        XmlNode nodeProviderType = null, nodeAccessTokenValue = null, nodeAccessTokenSecret = null, nodeExpiresDateTime = null;

        if (nodeAccessToken == null)
        {
            // insert token
            nodeAccessToken = accessTokenStorage.CreateNode(XmlNodeType.Element, "token", null);
            nodeProviderType = accessTokenStorage.CreateNode(XmlNodeType.Attribute, "provider", null);
            nodeAccessTokenValue = accessTokenStorage.CreateNode(XmlNodeType.Attribute, "tokenValue", null);
            nodeAccessTokenSecret = accessTokenStorage.CreateNode(XmlNodeType.Attribute, "tokenSecret", null);
            nodeExpiresDateTime = accessTokenStorage.CreateNode(XmlNodeType.Attribute, "expireDate", null);

            nodeProviderType.Value = ProviderType;
            nodeAccessTokenValue.Value = AccessToken;
            nodeAccessTokenSecret.Value = TokenSecret;
            nodeExpiresDateTime.Value = ExpireDate.ToString("yyyy/MM/dd HH:mm:ss"); ;

            nodeAccessToken.Attributes.SetNamedItem(nodeProviderType);
            nodeAccessToken.Attributes.SetNamedItem(nodeAccessTokenValue);
            nodeAccessToken.Attributes.SetNamedItem(nodeAccessTokenSecret);
            nodeAccessToken.Attributes.SetNamedItem(nodeExpiresDateTime);
            accessTokenStorage.DocumentElement.AppendChild(nodeAccessToken);
        }
        else
        {
            // update token
            nodeProviderType = nodeAccessToken.Attributes.GetNamedItem("provider");
            nodeAccessTokenValue = nodeAccessToken.Attributes.GetNamedItem("tokenValue");
            nodeAccessTokenSecret = nodeAccessToken.Attributes.GetNamedItem("tokenSecret");
            nodeExpiresDateTime = nodeAccessToken.Attributes.GetNamedItem("expireDate");

            nodeProviderType.Value = ProviderType;
            nodeAccessTokenValue.Value = AccessToken;
            nodeAccessTokenSecret.Value = TokenSecret;
            nodeExpiresDateTime.Value = ExpireDate.ToString("yyyy/MM/dd HH:mm:ss"); ;
        }

        accessTokenStorage.Save(Environment.CurrentDirectory + @"\" + PersistXmlProvider.AccessTokensFileName);
        accessTokenStorage = null;
    }

    public void RetriveAccessToken(string ProviderType, out string AccessToken, out string TokenSecret, out DateTime ExpireDate)
    {
        XmlDocument accessTokenStorage = new XmlDocument();

        if (!File.Exists(Environment.CurrentDirectory + @"\" + PersistXmlProvider.AccessTokensFileName))
        {
            accessTokenStorage = null;
            throw new OAuthException("ERROR_PERSISTED_ACCESS_TOKEN_IS_NOT_EXIST");
        }
        else
            accessTokenStorage.Load(Environment.CurrentDirectory + @"\" + PersistXmlProvider.AccessTokensFileName);

        XmlNode nodeAccessToken = accessTokenStorage.DocumentElement.SelectSingleNode("//tokens/token[@provider='" + ProviderType + "']");

        if (nodeAccessToken == null)
        {
            accessTokenStorage = null;
            throw new OAuthException("ERROR_PERSISTED_ACCESS_TOKEN_IS_NOT_EXIST");
        }
        else
        {
            XmlNode nodeAccessTokenValue = nodeAccessToken.Attributes.GetNamedItem("tokenValue");
            XmlNode nodeAccessTokenSecret = nodeAccessToken.Attributes.GetNamedItem("tokenSecret");
            XmlNode nodeExpiresDateTime = nodeAccessToken.Attributes.GetNamedItem("expireDate");
            DateTime expireDate = DateTime.ParseExact(nodeExpiresDateTime.Value, "yyyy/MM/dd HH:mm:ss", null);

            if (expireDate < DateTime.Now)
            {
                accessTokenStorage = null;
                throw new OAuthException("ERROR_PERSISTED_ACCESS_TOKEN_IS_EXPIRED");
            }
            else
            {
                AccessToken = nodeAccessTokenValue.Value;
                TokenSecret = nodeAccessTokenSecret.Value;
                ExpireDate = expireDate;
            }              
        }

        accessTokenStorage = null;
    }

    public void RemoveAccessToken(string ProviderType)
    {
        XmlDocument accessTokenStorage = new XmlDocument();

        if (!File.Exists(Environment.CurrentDirectory + @"\" + PersistXmlProvider.AccessTokensFileName))
        {
            accessTokenStorage = null;
            return;
        }
        else
            accessTokenStorage.Load(Environment.CurrentDirectory + @"\" + PersistXmlProvider.AccessTokensFileName);

        XmlNode nodeAccessToken = accessTokenStorage.DocumentElement.SelectSingleNode("//tokens/token[@provider='" + ProviderType + "']");

        if (nodeAccessToken == null)
        {
            accessTokenStorage = null;
            return;
        }
        else
            accessTokenStorage.DocumentElement.RemoveChild(nodeAccessToken);

        accessTokenStorage.Save(Environment.CurrentDirectory + @"\" + PersistXmlProvider.AccessTokensFileName);
        accessTokenStorage = null;
    }

    public bool IsAccessTokenValid(string ProviderType)
    {
        XmlDocument accessTokenStorage = new XmlDocument();

        if (!File.Exists(Environment.CurrentDirectory + @"\" + PersistXmlProvider.AccessTokensFileName))
        {
            accessTokenStorage = null;
            return false;
        }
        else
            accessTokenStorage.Load(Environment.CurrentDirectory + @"\" + PersistXmlProvider.AccessTokensFileName);

        XmlNode nodeAccessToken = accessTokenStorage.DocumentElement.SelectSingleNode("//tokens/token[@provider='" + ProviderType + "']");
        bool result = false;

        if (nodeAccessToken == null)
        {
            result = false;
        }
        else
        {
            XmlNode nodeExpiresDateTime = nodeAccessToken.Attributes.GetNamedItem("expireDate");
            DateTime expireDate = DateTime.ParseExact(nodeExpiresDateTime.Value, "yyyy/MM/dd HH:mm:ss", null);

            if (expireDate < DateTime.Now)
                result = false;
            else
                result = true;
        }

        accessTokenStorage = null;
        return result;
    }
}

只是就目前我個人所知的,Facebook 和 Yahoo 的 Access Token 是有到期時間的,Facebook 在不指定參數的情況下,預設是有到期時間的;Yahoo 則是預設 3600 秒 (一小時),若超過時只能再用 Refresh Access Token (http://developer.yahoo.com/oauth/guide/oauth-refreshaccesstoken.html) 的方式來刷新存取時間。