Статьи

Flickr из приложения для Windows Phone

Следующей кабиной от рейтинга аутентификации является Flickr . В предыдущих статьях вы уже видели пару реализаций OAuth 1. К сожалению, хотя Flickr утверждает, что они реализуют OAuth Core 1.0 Revision A, может показаться, что это не так для приложений Windows Phone. Вместо этого вам необходимо использовать их устаревший процесс аутентификации. Это даст вам токен аутентификации, который вы можете использовать для звонков на Flickr API. При каждом вызове, который вы делаете, вам также нужно будет проходить аутентификацию, используя этот токен, снова используя устаревший процесс аутентификации. Это было хорошо задокументировано Денисом Делимарским в его серии о работе с Flickr .

Вместо того, чтобы просто дублировать работу Денниса в этом посте, я хочу показать, как, если у вас есть устаревший токен аутентификации, вы можете обменять его на токен OAuth. Это означает, что вы можете совершать все свои сервисные вызовы против Flickr, используя их новую систему аутентификации. В будущем, когда Flickr разберется со своей реализацией OAuth, все, что вам нужно будет сделать, это поменять первоначальную аутентификацию и заменить ее кодом OAuth.

Шаг 1: Подпишитесь на приложение Flickr

Давайте разберемся — во-первых, вам, конечно, придется зарегистрировать приложение в Flickr. В дополнение к тому, что их реализация OAuth не работает должным образом, их документация — это завтрак, и очень трудно найти то, что вы хотите. Начните с перехода на http://developer.flickr.com . Оттуда щелкните ссылку API под баннером, а затем ссылку «Запросить ключ API», которая должна быть шага 1 в разделе «Начало работы». Мы почти там; нажмите на ссылку Запросить ключ API (да, это еще одна ссылка с тем же именем, на этот раз на странице App Garden). Если вы заблудились, перейдите по адресу http://www.flickr.com/services/apps/create/apply, чтобы зарегистрировать свое приложение.

Flickr предлагает либо некоммерческий, либо коммерческий процесс регистрации. Большая разница в процессе регистрации заключается в том, что коммерческая регистрация может занять некоторое время (также обратите внимание на предупреждения о заполнении всех полей, чтобы убедиться, что они считают вашу заявку действительной). Рисунок 1 иллюстрирует некоммерческую регистрацию для нашего примера приложения.

Flickr WP Figure1

Фигура 1

После того, как вы ввели информацию о своем заявлении, нажмите кнопку «Отправить». В случае некоммерческого приложения вы сразу же получите ключ и секрет, как показано на рисунке 2.

Flickr WP Figure2

Фигура 2

Есть одна вещь, которую вы должны изменить в своем приложении, чтобы сделать его доступным, как приложение Windows Phone. Нажмите на ссылку Изменить поток проверки подлинности для этого приложения. Это перенесет вас на новую страницу, где вы сможете настроить процесс аутентификации для вашего приложения (рисунок 3).

Flickr WP Figure3

Рисунок 3

Под Тип приложения выберите Мобильное приложение. Убедитесь, что вы скопировали URL-адрес аутентификации, указанный в разделе «Разрешения для мобильных устройств», поскольку эта ссылка понадобится для запуска процесса аутентификации в вашем приложении. Нажмите кнопку «Сохранить изменения», чтобы зафиксировать это изменение.

Шаг 2. Создайте приложение

Как и в предыдущих статьях, мы создадим простое приложение для демонстрации интеграции Flickr. В следующем XAML отображается Button для запуска процесса аутентификации и два элемента TextBlock для отображения имени пользователя и идентификатора после завершения аутентификации. Об элементе управления WebBrowser следует обратить внимание на то, что атрибут IsScriptEnabled отсутствует — похоже, что установка этого атрибута в значение True приводит к сбою процесса аутентификации Flickr.

 <phone:PhoneApplicationPage x:Class="FlickrTestApp.MainPage" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:phone="clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone"> <Grid> <StackPanel x:Name="LayoutRoot" Background="Transparent"> <Button Content="Authenticate" Click="AuthenticateClick" /> <TextBlock x:Name="UserNameText" /> <TextBlock x:Name="UserIdText" /> </StackPanel> <phone:WebBrowser x:Name="AuthenticationBrowser" Visibility="Collapsed" Navigated="BrowserNavigated" /> </Grid> </phone:PhoneApplicationPage> , <phone:PhoneApplicationPage x:Class="FlickrTestApp.MainPage" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:phone="clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone"> <Grid> <StackPanel x:Name="LayoutRoot" Background="Transparent"> <Button Content="Authenticate" Click="AuthenticateClick" /> <TextBlock x:Name="UserNameText" /> <TextBlock x:Name="UserIdText" /> </StackPanel> <phone:WebBrowser x:Name="AuthenticationBrowser" Visibility="Collapsed" Navigated="BrowserNavigated" /> </Grid> </phone:PhoneApplicationPage> 

Шаг 3: начните аутентификацию Flickr

Теперь вы можете догадаться, что именно здесь мы отправимся на Flickr и получим токен запроса, который мы будем использовать для создания URL-адреса входа. К сожалению, если вы сделаете это, будет отображаться URL-адрес входа в систему, пользователь введет свои учетные данные, а затем WebBrowser отправится в la-la-land и никогда не перейдет на следующую страницу, где пользователь должен авторизовать приложение. Вместо этого здесь вы используете URL-адрес аутентификации, который вы записали на шаге 1.

 private const string AuthenticationUrl = "http://m.flickr.com/auth-7xxxxxxxxxxxxxxxxxxx1"; private void AuthenticateClick(object sender, RoutedEventArgs e) { AuthenticationBrowser.Navigate(new Uri(AuthenticationUrl)); AuthenticationBrowser.Visibility = Visibility.Visible; } 

Здесь следует отметить, что определенная здесь константа AuthenticationUrl использует m.flickr.com вместо www.flickr.com (т. Е. Измените www на m в своем URL-адресе аутентификации). Это гарантирует, что Flickr отображает мобильную страницу для вашего пользователя, чтобы войти в.

Шаг 4. Пользователь входит в систему и авторизует ваше приложение

Когда пользователь нажимает кнопку «Аутентификация» (рисунок 4, первое изображение), ему будет представлена ​​страница входа Yahoo / Flickr (второе изображение), за которым следует экран авторизации (третье изображение). После нажатия кнопки «ОК, я авторизую» они увидят страницу с PIN-кодом. Это похоже на процесс OAuth 1, который заканчивается PIN-кодом Verifier, так что вы можете угадать, что будет дальше …

Flickr WP Figure4a

Figure4a

Flickr WP Figure4b

Figure4b

Шаг 5: Извлеките мини-токен (PIN)

В методе BrowserNavigated (который вызывается при каждом BrowserNavigated браузера на страницу) вам необходимо извлечь мини-токен (т. Е. PIN-код, показанный на рисунке 4). Следующий код снова использует регулярное выражение для извлечения мини-токена.

 private const string RedirectUrl = "http://m.flickr.com/services/auth/"; private string miniToken; private void BrowserNavigated(object sender, NavigationEventArgs e) { if (e.Uri.AbsoluteUri.ToLower() == RedirectUrl) { var htmlString = AuthenticationBrowser.SaveToString(); var pinFinder = new Regex(@"(?<minitoken>[A-Za-z0-9-]+)</span>", RegexOptions.IgnoreCase); var match = pinFinder.Match(htmlString); if (match.Length > 0) { var group = match.Groups["minitoken"]; if (group.Length > 0){ miniToken = group.Captures[0].Value; if (!string.IsNullOrEmpty(miniToken)) { RetrieveAuthenticationToken (); } } } if (string.IsNullOrEmpty(miniToken)) { Dispatcher.BeginInvoke(() => MessageBox.Show("Authorization denied by user")); } // Make sure PIN is reset to null miniToken = null; AuthenticationBrowser.Visibility = Visibility.Collapsed; } } 

Шаг 6: Получить токен аутентификации

Сам по себе мини-токен не может использоваться для аутентификации вызовов, выполняемых API Flickr. Вместо этого вам необходимо преобразовать его в токен аутентификации, вызвав метод flickr.auth.getFullToken . Для этого вам нужно сгенерировать соответствующий веб-запрос, который будет состоять из имени метода, параметров и подписи. Подпись должна быть хешем MD5 параметров запроса.

К сожалению, в Windows Phone нет реализации MD5. К счастью, есть один, который доступен на http://archive.msdn.microsoft.com/SilverlightMD5 . Перейдите на вкладку «Загрузки» и загрузите файл MD5.cs в свое приложение.

Важное замечание : На момент написания статьи в строке 109 этого файла была ошибка. Замените if (cbSize <= 56) на if (cbSize <56), как это предлагается в комментариях на домашней странице этого сайта.

Процесс создания веб-запроса тогда относительно прост, как показывает метод RequestAuthenticationToken . Не забудьте обновить ConsumerKey и ConsumerSecret указав значения Key и Secret, начиная с шага 1.

 private const string ConsumerKey = "<your key for your flickr application>"; private const string ConsumerSecret = "<your secret for your flickr application>"; private const string Flickr_GetFullToken = "flickr.auth.getFullToken"; private const string MethodKey = "method"; private const string ApiKeyKey = "api_key"; private const string MiniTokenKey = "mini_token"; private const string ApiSignatureKey = "api_sig"; private void RetrieveAuthenticationToken() { var parameters = new Dictionary<string, string>(); parameters[MethodKey] = Flickr_GetFullToken; parameters[ApiKeyKey] = ConsumerKey; parameters[MiniTokenKey] = miniToken; parameters[ApiSignatureKey] = GenerateSignature(parameters); var queryString = BuildParameterString(parameters); var reqUrl = "http://api.flickr.com/services/rest/?" + queryString; var request = WebRequest.CreateHttp(reqUrl); request.BeginGetResponse(result => { var req = result.AsyncState as HttpWebRequest; using (var resp = req.EndGetResponse(result)) using (var strm = resp.GetResponseStream()) { var xml = XElement.Load(strm); var authenticationToken = xml.Descendants("token").Select(x => x.Value).FirstOrDefault(); RetrieveAccessToken(authenticationToken); } }, request); } private string GenerateSignature(IDictionary<string,string> queryParameters) { var parameterList = new List<KeyValuePair<string, string>>(queryParameters); parameterList.Sort((kvp1, kvp2) => { if (kvp1.Key == kvp2.Key) { return string.Compare(kvp1.Value, kvp2.Value); } return string.Compare(kvp1.Key, kvp2.Key); }); var signatureBase = new StringBuilder(ConsumerSecret); foreach (var kvp in parameterList) { signatureBase.Append(kvp.Key + kvp.Value); } return MD5Core.GetHashString(signatureBase.ToString()); } private static string BuildParameterString(IEnumerable<KeyValuePair<string, string>> parameters) { var sb = new StringBuilder(); foreach (var parameter in parameters) { if (sb.Length > 0) sb.Append('&amp;'); sb.AppendFormat("{0}={1}", parameter.Key, parameter.Value); } return sb.ToString(); } 

Шаг 7. Преобразование токена аутентификации в токен доступа

На предыдущем шаге вы заметили, что после получения токена аутентификации вызывается RetrieveAccessToken . Это процесс, с помощью которого мы обмениваемся токеном аутентификации (то есть устаревшей системой аутентификации) с токеном доступа OAuth (и соответствующим секретом токена). Это просто вызов метода для метода flickr.auth.oauth.getAccessToken :

 private const string Flickr_GetAccessToken = "flickr.auth.oauth.getAccessToken"; private const string AuthenticationTokenKey = "auth_token"; private string token; private string tokenSecret; private void RetrieveAccessToken(string authenticationToken) { var parameters = new Dictionary<string, string>(); parameters[MethodKey] = Flickr_GetAccessToken; parameters[ApiKeyKey] = ConsumerKey; parameters[AuthenticationTokenKey] = authenticationToken; parameters[ApiSignatureKey] = GenerateSignature(parameters); var queryString = BuildParameterString(parameters); var reqUrl = "http://api.flickr.com/services/rest/?" + queryString; var request = WebRequest.CreateHttp(reqUrl); request.BeginGetResponse(result => { var req =result.AsyncState as HttpWebRequest; using (var resp = req.EndGetResponse(result)) using (var strm = resp.GetResponseStream()) { var xml = XElement.Load(strm); var access = xml.Descendants("access_token").FirstOrDefault(); token = access.Attribute("oauth_token").Value; tokenSecret = access.Attribute("oauth_token_secret").Value; RetrieveProfile(); } }, request); } 

Шаг 8: Получить профиль пользователя

Теперь на самом деле, если вы посмотрите на ответ XML, который возвращается вместе с токеном аутентификации, вы заметите, что идентификатор пользователя включен в этот ответ. Тем не менее, это не относится к реализации OAuth, поэтому для обеспечения будущего подхода мы будем вызывать метод flickr.test.login , который будет возвращать информацию об аутентифицированном пользователе. Теперь, когда у нас есть OAuth Access Token и Secret, мы вернулись на знакомую территорию, нам просто нужно создать соответствующий запрос аналогично тому, что мы делали в предыдущих постах, посвященных OAuth.

 private const string Flickr_TestLogin = "flickr.test.login"; private const string NoJsonCallBackKey = "nojsoncallback"; private const string FormatKey = "format"; private const string NoCallBack = "1"; private const string JsonResponseFormat = "json"; private void RetrieveProfile() { var requestParameters = new Dictionary<string, string>(); requestParameters[NoJsonCallBackKey] = NoCallBack; requestParameters[FormatKey] = JsonResponseFormat; requestParameters[MethodKey] = Flickr_TestLogin; var profileUrl = "http://api.flickr.com/services/rest"; var request = CreateRequest("GET", profileUrl, requestParameters); request.BeginGetResponse(result => { try { var req2 = result.AsyncState as HttpWebRequest; if (req2 == null) throw new ArgumentNullException("result", "Request parameter is null"); using (var resp = req2.EndGetResponse(result)) using (var strm = resp.GetResponseStream()) { var serializer = new DataContractJsonSerializer(typeof(FlickrLoginResponse)); var user = serializer.ReadObject(strm) as FlickrLoginResponse; Dispatcher.BeginInvoke(() => { UserIdText.Text = user.User.Id; UserNameText.Text = user.User.Username.Name; }); } } catch (Exception ex) { Dispatcher.BeginInvoke(() => MessageBox.Show("Unable to access profile")); } }, request); } [DataContract] public class FlickrLoginResponse { [DataMember(Name = "user")] public FlickrUser User { get; set; } [DataContract] public class FlickrUser { [DataMember(Name = "id")] public string Id { get; set; } [DataMember(Name = "username")] public FlickrUsername Username { get; set; } [DataContract] public class FlickrUsername { [DataMember(Name = "_content")] public string Name { get; set; } } } } 

Вот и все. Чтобы убедиться, что вы не запутались, решая, из какого предыдущего поста скопировать реализацию OAuth (не говоря уже о нескольких небольших корректировках, необходимых для поддержки реализации Flickr), в конце я включил оставшуюся часть кода OAuth. этого поста. Мы почти находимся в конце этой серии постов по аутентификации — если есть социальная сеть, о которой мы не рассказывали и которую вы хотели бы видеть в этой серии, обязательно оставьте комментарий.

 private const string OAuthConsumerKeyKey = "oauth_consumer_key"; private const string OAuthVersionKey = "oauth_version"; private const string OAuthSignatureMethodKey = "oauth_signature_method"; private const string OAuthSignatureKey = "oauth_signature"; private const string OAuthTimestampKey = "oauth_timestamp"; private const string OAuthNonceKey = "oauth_nonce"; private const string OAuthTokenKey = "oauth_token"; private const string OAuthTokenSecretKey = "oauth_token_secret"; private const string OAuthVerifierKey = "oauth_verifier"; private const string OAuthPostBodyKey = "post_body"; private const string OAuthVersion = "1.0"; private const string Hmacsha1SignatureType = "HMAC-SHA1"; private string pin; private WebRequest CreateRequest(string httpMethod, string requestUrl, IDictionary<string, string> requestParameters = null) { if (requestParameters == null) { requestParameters = new Dictionary<string, string>(); } var secret = ""; if (!string.IsNullOrEmpty(token)) { requestParameters[OAuthTokenKey] = token; secret = tokenSecret; } if (!string.IsNullOrEmpty(pin)) { requestParameters[OAuthVerifierKey] = pin; } var url = new Uri(requestUrl); var normalizedUrl = requestUrl; if (!string.IsNullOrEmpty(url.Query)) { normalizedUrl = requestUrl.Replace(url.Query, ""); } var signature = GenerateSignature(httpMethod, normalizedUrl, url.Query, requestParameters, secret); requestParameters[OAuthSignatureKey] = UrlEncode(signature); var sb = new StringBuilder(); sb.Append(url.Query); foreach (var param in requestParameters) { if (sb.Length > 0) sb.Append("&amp;"); sb.Append(string.Format("{0}={1}", param.Key, param.Value)); } if (sb[0] != '?') sb.Insert(0, "?"); var request = WebRequest.CreateHttp(normalizedUrl + sb.ToString()); request.Method = httpMethod; request.Headers[HttpRequestHeader.Authorization] = GenerateAuthorizationHeader(requestParameters); return request; } public string GenerateSignature(string httpMethod, string normalizedUrl, string queryString, IDictionary<string, string> requestParameters, string secret = null) { requestParameters["oauth_callback"] = "oob"; requestParameters[OAuthConsumerKeyKey] = ConsumerKey; requestParameters[OAuthVersionKey] = OAuthVersion; requestParameters[OAuthNonceKey] = GenerateNonce(); requestParameters[OAuthTimestampKey] = GenerateTimeStamp(); requestParameters[OAuthSignatureMethodKey] = Hmacsha1SignatureType; string signatureBase = GenerateSignatureBase(httpMethod, normalizedUrl, queryString, requestParameters); var hmacsha1 = new HMACSHA1(); var key = string.Format("{0}&amp;{1}", UrlEncode(ConsumerSecret), string.IsNullOrEmpty(secret) ? "" : UrlEncode(secret)); hmacsha1.Key = Encoding.UTF8.GetBytes(key); var signature = ComputeHash(signatureBase, hmacsha1); return signature; } private static readonly Random Random = new Random(); public static string GenerateNonce() { // Just a simple implementation of a random number between 123400 and 9999999 return Random.Next(123400, 9999999).ToString(); } public static string GenerateTimeStamp() { var now = DateTime.UtcNow; TimeSpan ts = now - new DateTime(1970, 1, 1, 0, 0, 0, 0); return Convert.ToInt64(ts.TotalSeconds).ToString(); } public static string GenerateSignatureBase(string httpMethod, string normalizedUrl, string queryString, IDictionary<string, string> requestParameters) { var parameters = new List<KeyValuePair<string, string>>(GetQueryParameters(queryString)) { new KeyValuePair<string, string>(OAuthVersionKey, requestParameters[OAuthVersionKey]), new KeyValuePair<string, string>(OAuthNonceKey, requestParameters[OAuthNonceKey]), new KeyValuePair<string, string>(OAuthTimestampKey, requestParameters[OAuthTimestampKey]), new KeyValuePair<string, string>(OAuthSignatureMethodKey, requestParameters[OAuthSignatureMethodKey]), new KeyValuePair<string, string>(OAuthConsumerKeyKey, requestParameters[OAuthConsumerKeyKey]), new KeyValuePair<string, string>("oauth_callback", requestParameters["oauth_callback"]) }; if (requestParameters.ContainsKey(OAuthVerifierKey)) { parameters.Add(new KeyValuePair<string, string>(OAuthVerifierKey, requestParameters[OAuthVerifierKey])); } if (requestParameters.ContainsKey(OAuthTokenKey)) { parameters.Add(new KeyValuePair<string, string>(OAuthTokenKey, requestParameters[OAuthTokenKey])); } foreach (var kvp in requestParameters) { if (kvp.Key.StartsWith("oauth_") || kvp.Key == OAuthPostBodyKey) continue; parameters.Add(kvp); } parameters.Sort((kvp1, kvp2) => { if (kvp1.Key == kvp2.Key) { return string.Compare(kvp1.Value, kvp2.Value); } return string.Compare(kvp1.Key, kvp2.Key); }); var parameterString = BuildParameterString(parameters); if (requestParameters.ContainsKey(OAuthPostBodyKey)) { parameterString += "&amp;" + requestParameters[OAuthPostBodyKey]; } var signatureBase = new StringBuilder(); signatureBase.AppendFormat("{0}&amp;", httpMethod); signatureBase.AppendFormat("{0}&amp;", UrlEncode(normalizedUrl)); signatureBase.AppendFormat("{0}", UrlEncode(parameterString)); return signatureBase.ToString(); } private static IEnumerable<KeyValuePair<string, string>> GetQueryParameters(string queryString) { var parameters = new List<KeyValuePair<string, string>>(); if (string.IsNullOrEmpty(queryString)) return parameters; queryString = queryString.Trim('?'); return (from pair in queryString.Split('&amp;') let bits = pair.Split('=') where bits.Length == 2 select new KeyValuePair<string, string>(bits[0], bits[1])).ToArray(); } /// <summary> /// The set of characters that are unreserved in RFC 2396 but are NOT unreserved in RFC 3986. /// </summary> private static readonly string[] UriRfc3986CharsToEscape = new[] { "!", "*", "'", "(", ")" }; private static readonly char[] HexUpperChars = new[] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' }; public static string UrlEncode(string value) { // Start with RFC 2396 escaping by calling the .NET method to do the work. // This MAY sometimes exhibit RFC 3986 behavior (according to the documentation). // If it does, the escaping we do that follows it will be a no-op since the // characters we search for to replace can't possibly exist in the string. var escaped = new StringBuilder(Uri.EscapeDataString(value)); // Upgrade the escaping to RFC 3986, if necessary. foreach (string t in UriRfc3986CharsToEscape) { escaped.Replace(t, HexEscape(t[0])); } // Return the fully-RFC3986-escaped string. return escaped.ToString(); } public static string HexEscape(char character) { var to = new char[3]; int pos = 0; EscapeAsciiChar(character, to, ref pos); return new string(to); } private static void EscapeAsciiChar(char ch, char[] to, ref int pos) { to[pos++] = '%'; to[pos++] = HexUpperChars[(ch &amp; 240) >> 4]; to[pos++] = HexUpperChars[ch &amp; 'x000f']; } private static string ComputeHash(string data, HashAlgorithm hashAlgorithm) { byte[] dataBuffer = Encoding.UTF8.GetBytes(data); byte[] hashBytes = hashAlgorithm.ComputeHash(dataBuffer); return Convert.ToBase64String(hashBytes); } public static string GenerateAuthorizationHeader(IDictionary<string, string> requestParameters) { var paras = new StringBuilder(); foreach (var param in requestParameters) { if (!param.Key.StartsWith("oauth_")) continue; if (paras.Length > 0) paras.Append(","); paras.Append(param.Key + "="" + param.Value + """); } return "OAuth " + paras; } private IEnumerable<KeyValuePair<string, string>> ExtractTokenInfo(string responseText) { if (string.IsNullOrEmpty(responseText)) return null; var responsePairs = (from pair in responseText.Split('&amp;') let bits = pair.Split('=') where bits.Length == 2 select new KeyValuePair<string, string>(bits[0], bits[1])).ToArray(); token = responsePairs.Where(kvp => kvp.Key == OAuthTokenKey).Select(kvp => kvp.Value).FirstOrDefault(); tokenSecret = responsePairs.Where(kvp => kvp.Key == OAuthTokenSecretKey).Select(kvp => kvp.Value).FirstOrDefault(); return responsePairs; }