На днях (сегодня у нас 21 апреля 2015) создал новое приложение в аккаунте Instagram. И обнаружил, что опция «Signed API headers» больше недоступна.
Теперь, согласно топику, используется опция «Signed API requests». Пока что старые приложения смогут работать без изменений, а вот после 1 сентября 2015 «Signed API headers» будет убрано.
В старых приложениях настройки приватности выглядят следующим образом:
А в новом:
Если вы попытаетесь работать в новом приложении по старому принципу, то выскочит, что-то вроде:
1 2 3 4 5 |
Exception in thread "main" org.jinstagram.exceptions.InstagramException: Unknown error response code: 403 {"code": 403, "error_type": OAuthForbiddenException", "error_message": "Invalid signed-request: Missing required parameter 'sig'"} at org.jinstagram.Instagram.handleInstagramError(Instagram.java:916) at org.jinstagram.Instagram.createInstagramObject(Instagram.java:893) at org.jinstagram.Instagram.getCurrentUserInfo(Instagram.java:142) at org.jinstagram.test.Runner.main(Runner.java:30) |
Это вывод библиотеки jInstagram, которая на сегодняшний день не имеет изменений для работы по новым правилам Instagram.
Поскольку изменений скорее всего ждать придется долго, а мой проект использует библиотеку jInstagram, то придется внести изменения самостоятельно.
Приступим…
Согласно новым правилам, каждый запрос должен иметь параметр sig. Лаконичная и четкая фраза на сайте разработчиков Instagram описывает как получается параметр sig. Для этого нужно:
- Отсортировать все параметры вызываемого API метода Instagram в алфавитном порядке и представить в виде строк «параметр=значение» разделенных символом «|»
- Взять имя метода, добавить символ «|» и добавить строку полученную в предыдущем пункте, теперь должно получиться что-то вроде: «вызываемый_метод|параметр1=значенией1|параметр2=значение2|…|параметрN=значениеN«
- С помощью HMAC (Hash Message Authentication Code) получить из строки код.
На деле для правки библиотеки jInstagram был добавлен класс EnforceSignedUtils со следующим содержанием:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 |
package org.jinstagram.utils; import java.nio.charset.Charset; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.util.Map; import java.util.Map.Entry; import java.util.TreeMap; import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; import org.apache.commons.codec.binary.Hex; import org.jinstagram.auth.model.OAuthConstants; import org.jinstagram.exceptions.InstagramException; public class EnforceSignedRequestUtils { private static final String HMAC_SHA256 = "HmacSHA256"; private static final String keyValuePairSpearator = "|"; private static final String keyValueSpearator = "="; public static final String ENFORCE_SIGNED_REQUEST = "sig"; private Mac mac; public EnforceSignedRequestUtils(String clientSecret) throws InstagramException { SecretKeySpec keySpec = new SecretKeySpec(clientSecret.getBytes(Charset .forName("UTF-8")), HMAC_SHA256); try { mac = Mac.getInstance(HMAC_SHA256); mac.init(keySpec); } catch (NoSuchAlgorithmException e) { throw new InstagramException("Invalid algorithm name!", e); } catch (InvalidKeyException e) { throw new InstagramException("Invalid key: " + clientSecret, e); } } public String signature(String methodName, Map<String, String> params, String accessToken) throws InstagramException { String toSignMsg = methodName; Map<String, String> sortedMap = new TreeMap<String, String>(); sortedMap.put(OAuthConstants.ACCESS_TOKEN, accessToken); if (params != null && params.size() > 0) sortedMap.putAll(params); for (Entry<String, String> paramEntry : sortedMap.entrySet()) { toSignMsg += keyValuePairSpearator + paramEntry.getKey() + keyValueSpearator + paramEntry.getValue(); } byte[] result = mac .doFinal(toSignMsg.getBytes(Charset.forName("UTF-8"))); String encodedResult = Hex.encodeHexString(result); return encodedResult; } } |
Тут метод sinature получает имя метода, параметры метода и их значения и accessToken. Создается TreeMap для сортировки параметров по алфавиту, туда копируются все параметры. А дальше применяется алгоритм HmacSHA256 для получения кода .
В классе Instagram для хранения нашей утилиты для подписи добавлено поле:
1 |
private EnforceSignedRequestUtils enforceRequest; |
Конструктор был заменен на:
1 2 3 4 5 6 7 |
public Instagram(String token, String secret, String ips) throws InstagramException { Token accessToken = new Token(token, secret); this.accessToken = accessToken; clientId = null; config = new InstagramConfig(); enforceRequest = new EnforceSignedRequestUtils(secret); } |
И один метод был изменен следующим образом:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
/** * Get response from Instagram. * * @param verb HTTP Verb * @param methodName Instagram API Method * @param params parameters which would be sent with the request. * @return Response object. */ protected Response getApiResponse(Verbs verb, String methodName, Map<String, String> params) throws IOException { Response response; String apiResourceUrl = config.getApiURL() + methodName; OAuthRequest request = new OAuthRequest(verb, apiResourceUrl); request.setConnectTimeout(config.getConnectionTimeoutMills(), TimeUnit.MILLISECONDS); request.setReadTimeout(config.getReadTimeoutMills(), TimeUnit.MILLISECONDS); // Additional parameters in url if (params != null) { for (Map.Entry<String, String> entry : params.entrySet()) { if (verb == Verbs.GET) { request.addQuerystringParameter(entry.getKey(), entry.getValue()); } else { request.addBodyParameter(entry.getKey(), entry.getValue()); } } } // Add the AccessToken to the Request Url if ((verb == Verbs.GET) || (verb == Verbs.DELETE)) { if (accessToken == null) { request.addQuerystringParameter(OAuthConstants.CLIENT_ID, clientId); } else { request.addQuerystringParameter(OAuthConstants.ACCESS_TOKEN, accessToken.getToken()); } } else { if (accessToken == null) { request.addBodyParameter(OAuthConstants.CLIENT_ID, clientId); } else { request.addBodyParameter(OAuthConstants.ACCESS_TOKEN, accessToken.getToken()); } } if(enforceRequest != null && accessToken != null) { // Add signature to request if ((verb == Verbs.GET) || (verb == Verbs.DELETE)) { request.addQuerystringParameter(EnforceSignedRequestUtils.ENFORCE_SIGNED_REQUEST, enforceRequest.signature(methodName, params, accessToken.getToken())); } else { request.addBodyParameter(EnforceSignedRequestUtils.ENFORCE_SIGNED_REQUEST, enforceRequest.signature(methodName, params, accessToken.getToken())); } } response = request.send(); return response; } |
Все, что связано с «Signed Header» было удалено из библиотеки. Теперь метод EnforceSignedRequestUtils.signature будет формировать подпись sig необходимым нам образом и добавлять ее к параметрам в методе getApiResponse.
Нельзя сказать, что решение самое лучшее, но по мне так оно самое быстрое, а что самое главное, оно работает!
Все изменения вместе с необходимыми библиотеками лежат в этом репозитории: git@gitlab.com:Strakh/org.jinstagram.mod.git.