10

我正在尝试使用 Smack 库在 Android 上进行 Facebook 聊天。我已经阅读了Facebook 的Chat API,但我无法理解如何使用这个库向 Facebook 进行身份验证。

谁能指出我如何做到这一点?

更新:根据 no.good.at.coding 答案,我有这个代码适应了 Asmack 库。一切正常,除了我收到对登录的响应:未授权。这是我使用的代码:

public class SASLXFacebookPlatformMechanism extends SASLMechanism
{

    private static final String NAME              = "X-FACEBOOK-PLATFORM";

    private String              apiKey            = "";
    private String              applicationSecret = "";
    private String              sessionKey        = "";

    /**
     * Constructor.
     */
    public SASLXFacebookPlatformMechanism(SASLAuthentication saslAuthentication)
    {
        super(saslAuthentication);
    }

    @Override
    protected void authenticate() throws IOException, XMPPException
    {

        getSASLAuthentication().send(new AuthMechanism(NAME, ""));
    }

    @Override
    public void authenticate(String apiKeyAndSessionKey, String host,
            String applicationSecret) throws IOException, XMPPException
    {
        if (apiKeyAndSessionKey == null || applicationSecret == null)
        {
            throw new IllegalArgumentException("Invalid parameters");
        }

        String[] keyArray = apiKeyAndSessionKey.split("\\|", 2);
        if (keyArray.length < 2)
        {
            throw new IllegalArgumentException(
                    "API key or session key is not present");
        }

        this.apiKey = keyArray[0];
        Log.d("API_KEY", apiKey);
        this.applicationSecret = applicationSecret;
        Log.d("SECRET_KEY", applicationSecret);
        this.sessionKey = keyArray[1];
        Log.d("SESSION_KEY", sessionKey);

        this.authenticationId = sessionKey;
        this.password = applicationSecret;
        this.hostname = host;

        String[] mechanisms = { "DIGEST-MD5" };
        Map<String, String> props = new HashMap<String, String>();
        this.sc =
                Sasl.createSaslClient(mechanisms, null, "xmpp", host, props,
                        this);
        authenticate();
    }

    @Override
    protected String getName()
    {
        return NAME;
    }

    @Override
    public void challengeReceived(String challenge) throws IOException
    {
        byte[] response = null;

        if (challenge != null)
        {
            String decodedChallenge = new String(Base64.decode(challenge));
            Log.d("DECODED", decodedChallenge);
            Map<String, String> parameters = getQueryMap(decodedChallenge);

            String version = "1.0";
            String nonce = parameters.get("nonce");
            String method = parameters.get("method");

            long callId = new GregorianCalendar().getTimeInMillis() / 1000L;

            String sig =
                    "api_key=" + apiKey + "call_id=" + callId + "method="
                            + method + "nonce=" + nonce + "session_key="
                            + sessionKey + "v=" + version + applicationSecret;

            try
            {
                sig = md5(sig);
                sig = sig.toUpperCase();
            } catch (NoSuchAlgorithmException e)
            {
                throw new IllegalStateException(e);
            }

            String composedResponse =
                    "api_key=" + URLEncoder.encode(apiKey, "utf-8")
                            + "&call_id=" + callId + "&method="
                            + URLEncoder.encode(method, "utf-8") + "&nonce="
                            + URLEncoder.encode(nonce, "utf-8")
                            + "&session_key="
                            + URLEncoder.encode(sessionKey, "utf-8") + "&v="
                            + URLEncoder.encode(version, "utf-8") + "&sig="
                            + URLEncoder.encode(sig, "utf-8");

            Log.d("COMPOSED", composedResponse);

            response = composedResponse.getBytes("utf-8");
        }

        String authenticationText = "";

        if (response != null)
        {
            authenticationText =
                    Base64.encodeBytes(response, Base64.DONT_BREAK_LINES);
        }

        // Send the authentication to the server
        getSASLAuthentication().send(new Response(authenticationText));
    }

    private Map<String, String> getQueryMap(String query)
    {
        Map<String, String> map = new HashMap<String, String>();
        String[] params = query.split("\\&");

        for (String param : params)
        {
            String[] fields = param.split("=", 2);
            map.put(fields[0], (fields.length > 1 ? fields[1] : null));
        }

        return map;
    }

    private String md5(String text) throws NoSuchAlgorithmException,
            UnsupportedEncodingException
    {
        MessageDigest md = MessageDigest.getInstance("MD5");
        md.update(text.getBytes("utf-8"), 0, text.length());
        return convertToHex(md.digest());
    }

    private String convertToHex(byte[] data)
    {
        StringBuilder buf = new StringBuilder();
        int len = data.length;

        for (int i = 0; i < len; i++)
        {
            int halfByte = (data[i] >>> 4) & 0xF;
            int twoHalfs = 0;

            do
            {
                if (0 <= halfByte && halfByte <= 9)
                {
                    buf.append((char) ('0' + halfByte));
                }
                else
                {
                    buf.append((char) ('a' + halfByte - 10));
                }
                halfByte = data[i] & 0xF;
            } while (twoHalfs++ < 1);
        }

        return buf.toString();
    }
}

这是通过发送和接收消息与服务器的通信:

PM SENT (1132418216): <stream:stream to="chat.facebook.com" xmlns="jabber:client" xmlns:stream="http://etherx.jabber.org/streams" version="1.0">


PM RCV  (1132418216): <?xml version="1.0"?><stream:stream id="C62D0F43" from="chat.facebook.com" xmlns="jabber:client" xmlns:stream="http://etherx.jabber.org/streams" version="1.0" xml:lang="en"><stream:features><mechanisms xmlns="urn:ietf:params:xml:ns:xmpp-sasl"><mechanism>X-FACEBOOK-PLATFORM</mechanism><mechanism>DIGEST-MD5</mechanism></mechanisms></stream:features>


PM SENT (1132418216): <auth mechanism="X-FACEBOOK-PLATFORM" xmlns="urn:ietf:params:xml:ns:xmpp-sasl"></auth>


PM RCV  (1132418216): <challenge xmlns="urn:ietf:params:xml:ns:xmpp-sasl">dmVyc2lvbj0xJm1ldGhvZD1hdXRoLnhtcHBfbG9naW4mbm9uY2U9NzFGNkQ3Rjc5QkIyREJCQ0YxQTkwMzA0QTg3OTlBMzM=</challenge>


PM SENT (1132418216): <response xmlns="urn:ietf:params:xml:ns:xmpp-sasl">YXBpX2tleT0zMWYzYjg1ZjBjODYwNjQ3NThiZTZhOTQyNjVjZmNjMCZjYWxsX2lkPTEzMDA0NTYxMzUmbWV0aG9kPWF1dGgueG1wcF9sb2dpbiZub25jZT03MUY2RDdGNzlCQjJEQkJDRjFBOTAzMDRBODc5OUEzMyZzZXNzaW9uX2tleT0yNjUzMTg4ODNkYWJhOGRlOTRiYTk4ZDYtMTAwMDAwNTAyNjc2Nzc4JnY9MS4wJnNpZz04RkRDRjRGRTgzMENGOEQ3QjgwNjdERUQyOEE2RERFQw==</response>


PM RCV  (1132418216): <failure xmlns="urn:ietf:params:xml:ns:xmpp-sasl"><not-authorized/></failure>

正如在开发人员 Facebook 论坛中所读,需要从您的应用程序的 Facebook 设置页面禁用“禁用已弃用的身份验证方法”设置。但是,即使这样做,我也无法登录。会话密钥是 OAuth 令牌的第二部分,格式为 AAA|BBB|CCC,我的意思是 BBB。

4

5 回答 5

14

最后,感谢 no.good.at.coding 代码和 harism 的建议,我已经能够连接到 Facebook 聊天。此代码是 Asmack 库(Android 的 Smack 端口)的机制。对于 Smack 库,必须使用 no.good.at.coding 机制。

SASLXFacebookPlatformMechanism.java:

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.Map;

import org.apache.harmony.javax.security.auth.callback.CallbackHandler;
import org.apache.harmony.javax.security.sasl.Sasl;
import org.jivesoftware.smack.SASLAuthentication;
import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smack.sasl.SASLMechanism;
import org.jivesoftware.smack.util.Base64;

public class SASLXFacebookPlatformMechanism extends SASLMechanism
{

    private static final String NAME              = "X-FACEBOOK-PLATFORM";

    private String              apiKey            = "";
    private String              applicationSecret = "";
    private String              sessionKey        = "";

    /**
     * Constructor.
     */
    public SASLXFacebookPlatformMechanism(SASLAuthentication saslAuthentication)
    {
        super(saslAuthentication);
    }

    @Override
    protected void authenticate() throws IOException, XMPPException
    {

        getSASLAuthentication().send(new AuthMechanism(NAME, ""));
    }

    @Override
    public void authenticate(String apiKeyAndSessionKey, String host,
            String applicationSecret) throws IOException, XMPPException
    {
        if (apiKeyAndSessionKey == null || applicationSecret == null)
        {
            throw new IllegalArgumentException("Invalid parameters");
        }

        String[] keyArray = apiKeyAndSessionKey.split("\\|", 2);
        if (keyArray.length < 2)
        {
            throw new IllegalArgumentException(
                    "API key or session key is not present");
        }

        this.apiKey = keyArray[0];
        this.applicationSecret = applicationSecret;
        this.sessionKey = keyArray[1];

        this.authenticationId = sessionKey;
        this.password = applicationSecret;
        this.hostname = host;

        String[] mechanisms = { "DIGEST-MD5" };
        Map<String, String> props = new HashMap<String, String>();
        this.sc =
                Sasl.createSaslClient(mechanisms, null, "xmpp", host, props,
                        this);
        authenticate();
    }

    @Override
    public void authenticate(String username, String host, CallbackHandler cbh)
            throws IOException, XMPPException
    {
        String[] mechanisms = { "DIGEST-MD5" };
        Map<String, String> props = new HashMap<String, String>();
        this.sc =
                Sasl.createSaslClient(mechanisms, null, "xmpp", host, props,
                        cbh);
        authenticate();
    }

    @Override
    protected String getName()
    {
        return NAME;
    }

    @Override
    public void challengeReceived(String challenge) throws IOException
    {
        byte[] response = null;

        if (challenge != null)
        {
            String decodedChallenge = new String(Base64.decode(challenge));
            Map<String, String> parameters = getQueryMap(decodedChallenge);

            String version = "1.0";
            String nonce = parameters.get("nonce");
            String method = parameters.get("method");

            long callId = new GregorianCalendar().getTimeInMillis();

            String sig =
                    "api_key=" + apiKey + "call_id=" + callId + "method="
                            + method + "nonce=" + nonce + "session_key="
                            + sessionKey + "v=" + version + applicationSecret;

            try
            {
                sig = md5(sig);
            } catch (NoSuchAlgorithmException e)
            {
                throw new IllegalStateException(e);
            }

            String composedResponse =
                    "api_key=" + URLEncoder.encode(apiKey, "utf-8")
                            + "&call_id=" + callId + "&method="
                            + URLEncoder.encode(method, "utf-8") + "&nonce="
                            + URLEncoder.encode(nonce, "utf-8")
                            + "&session_key="
                            + URLEncoder.encode(sessionKey, "utf-8") + "&v="
                            + URLEncoder.encode(version, "utf-8") + "&sig="
                            + URLEncoder.encode(sig, "utf-8");

            response = composedResponse.getBytes("utf-8");
        }

        String authenticationText = "";

        if (response != null)
        {
            authenticationText =
                    Base64.encodeBytes(response, Base64.DONT_BREAK_LINES);
        }

        // Send the authentication to the server
        getSASLAuthentication().send(new Response(authenticationText));
    }

    private Map<String, String> getQueryMap(String query)
    {
        Map<String, String> map = new HashMap<String, String>();
        String[] params = query.split("\\&");

        for (String param : params)
        {
            String[] fields = param.split("=", 2);
            map.put(fields[0], (fields.length > 1 ? fields[1] : null));
        }

        return map;
    }

    private String md5(String text) throws NoSuchAlgorithmException,
            UnsupportedEncodingException
    {
        MessageDigest md = MessageDigest.getInstance("MD5");
        md.update(text.getBytes("utf-8"), 0, text.length());
        return convertToHex(md.digest());
    }

    private String convertToHex(byte[] data)
    {
        StringBuilder buf = new StringBuilder();
        int len = data.length;

        for (int i = 0; i < len; i++)
        {
            int halfByte = (data[i] >>> 4) & 0xF;
            int twoHalfs = 0;

            do
            {
                if (0 <= halfByte && halfByte <= 9)
                {
                    buf.append((char) ('0' + halfByte));
                }
                else
                {
                    buf.append((char) ('a' + halfByte - 10));
                }
                halfByte = data[i] & 0xF;
            } while (twoHalfs++ < 1);
        }

        return buf.toString();
    }
}

要使用它:

ConnectionConfiguration config = new ConnectionConfiguration("chat.facebook.com", 5222);
config.setSASLAuthenticationEnabled(true);
XMPPConnection xmpp = new XMPPConnection(config);
try
{
    SASLAuthentication.registerSASLMechanism("X-FACEBOOK-PLATFORM", SASLXFacebookPlatformMechanism.class);
    SASLAuthentication.supportSASLMechanism("X-FACEBOOK-PLATFORM", 0);
    xmpp.connect();
    xmpp.login(apiKey + "|" + sessionKey, sessionSecret, "Application");
} catch (XMPPException e)
{
    xmpp.disconnect();
    e.printStackTrace();
}

apiKey 是 Facebook 应用程序设置页面中给出的 API 密钥。sessionKey 是访问令牌的第二部分。如果令牌是这种形式,AAA|BBB|CCC,BBB 就是会话密钥。sessionSecret 是使用带有 auth.promoteSession 方法的旧 REST API 获得的。要使用它,需要通过 Http 访问此 url:

https://api.facebook.com/method/auth.promoteSession?access_token=yourAccessToken

尽管 Facebook Chat 文档说需要使用您的应用程序密钥,但只有当我使用返回该 REST 方法的密钥时,我才能使其工作。要使该方法有效,您必须在应用程序设置的“高级”选项卡中禁用“禁用已弃用的身份验证方法”选项。

于 2011-04-30T18:21:19.573 回答
4

大约 6 个月前,我在 Smack (不是 asmack)上使用过它,所以我不确定它现在会怎样,但就这样吧,希望它有所帮助!

我在Ignite Realtime Smack 论坛上找到了 Facebook 的 X-FACEBOOK-PLATFORM 身份验证机制的实现,有人从fbgc 项目中获得了它。SASLXFacebookPlatformMechanism.java您会在我链接到的答案中找到带有来源的 ZIP 。您可以按如下方式使用它:

public void login() throws XMPPException
{
    SASLAuthentication.registerSASLMechanism(SASLXFacebookPlatformMechanism.NAME,
            SASLXFacebookPlatformMechanism.class);
    SASLAuthentication.supportSASLMechanism(SASLXFacebookPlatformMechanism.NAME, 0);

    ConnectionConfiguration connConfig = new ConnectionConfiguration(host, port);

    XMPPConnection connection = new XMPPConnection(connConfig);
    connection.connect();
    log.info("XMPP client connected");

    connection.login(Utils.FB_APP_ID + "|" + this.user.sessionId, Utils.FB_APP_SECRET, "app_name");
    log.info("XMPP client logged in");
}

我在没有 SDK 的服务器上执行此操作。我不记得细节(而且 Facebook 文档不是很好),但从我的代码中可以看出,在让用户授权应用程序之后,我收到了来自 Facebook 的带有code参数的回调请求。我打开一个URLConnectionhttps://graph.facebook.com/oauth/access_token?client_id=<app_id>&redirect_uri=http://myserver/context/path/&client_secret=<app_secret>&code=<code>. 响应应该是访问令牌,其中会话 ID 是|表单的 -something 之后的部分XXX|<sessionId>

于 2011-03-17T02:03:17.653 回答
3

这是我成功用于身份验证的代码。也许这会有所帮助,即使这与 Smack 没有任何关系。您可以从从 FB 收到的访问令牌中获取 sessionKey,并且为了获取 sessionSecret,我一直在使用旧的 REST API;

http://developers.facebook.com/docs/reference/rest/auth.promoteSession/

private final void processChallenge(XmlPullParser parser, Writer writer,
        String sessionKey, String sessionSecret) throws IOException,
        NoSuchAlgorithmException, XmlPullParserException {

    parser.require(XmlPullParser.START_TAG, null, "challenge");
    String challenge = new String(Base64.decode(parser.nextText(),
            Base64.DEFAULT));

    String params[] = challenge.split("&");
    HashMap<String, String> paramMap = new HashMap<String, String>();
    for (int i = 0; i < params.length; ++i) {
        String p[] = params[i].split("=");
        p[0] = URLDecoder.decode(p[0]);
        p[1] = URLDecoder.decode(p[1]);
        paramMap.put(p[0], p[1]);
    }

    String api_key = "YOUR_API_KEY";
    String call_id = "" + System.currentTimeMillis();
    String method = paramMap.get("method");
    String nonce = paramMap.get("nonce");
    String v = "1.0";

    StringBuffer sigBuffer = new StringBuffer();
    sigBuffer.append("api_key=" + api_key);
    sigBuffer.append("call_id=" + call_id);
    sigBuffer.append("method=" + method);
    sigBuffer.append("nonce=" + nonce);
    sigBuffer.append("session_key=" + sessionKey);
    sigBuffer.append("v=" + v);
    sigBuffer.append(sessionSecret);

    MessageDigest md = MessageDigest.getInstance("MD5");
    md.update(sigBuffer.toString().getBytes());
    byte[] digest = md.digest();

    StringBuffer sig = new StringBuffer();
    for (int i = 0; i < digest.length; ++i) {
        sig.append(Integer.toHexString(0xFF & digest[i]));
    }

    StringBuffer response = new StringBuffer();
    response.append("api_key=" + URLEncoder.encode(api_key));
    response.append("&call_id=" + URLEncoder.encode(call_id));
    response.append("&method=" + URLEncoder.encode(method));
    response.append("&nonce=" + URLEncoder.encode(nonce));
    response.append("&session_key=" + URLEncoder.encode(sessionKey));
    response.append("&v=" + URLEncoder.encode(v));
    response.append("&sig=" + URLEncoder.encode(sig.toString()));

    StringBuilder out = new StringBuilder();
    out.append("<response xmlns='urn:ietf:params:xml:ns:xmpp-sasl'>");
    out.append(Base64.encodeToString(response.toString().getBytes(),
            Base64.NO_WRAP));
    out.append("</response>");

    writer.write(out.toString());
    writer.flush();
}
于 2011-04-02T22:34:28.617 回答
1

很抱歉做出新的答案,但我必须包含新代码 @YShinkarev 抱歉迟到了
通过修改 @Adrian 答案以使 challengeReceived 我们可以使用 APIKey 和 accessToken 我修改的所有内容都是composedResponse

@Override
public void challengeReceived(String challenge) throws IOException {
    byte[] response = null;

    if (challenge != null) {
        String decodedChallenge = new String(Base64.decode(challenge));
        Map<String, String> parameters = getQueryMap(decodedChallenge);

        String version = "1.0";
        String nonce = parameters.get("nonce");
        String method = parameters.get("method");

        long callId = new GregorianCalendar().getTimeInMillis();

        String composedResponse = "api_key="
                + URLEncoder.encode(apiKey, "utf-8") + "&call_id=" + callId
                + "&method=" + URLEncoder.encode(method, "utf-8")
                + "&nonce=" + URLEncoder.encode(nonce, "utf-8")
                + "&access_token="
                + URLEncoder.encode(access_token, "utf-8") + "&v="
                + URLEncoder.encode(version, "utf-8");

        response = composedResponse.getBytes("utf-8");
    }

    String authenticationText = "";

    if (response != null) {
        authenticationText = Base64.encodeBytes(response,
                Base64.DONT_BREAK_LINES);
    }

    // Send the authentication to the server
    getSASLAuthentication().send(new Response(authenticationText));
}
于 2012-06-19T10:31:38.077 回答
0

你想让我做什么?

如果您只想登录 FB 聊天,您可以像连接到任何其他 XMPP 服务器一样连接到 FB。

我会查看并使用 Chat API 中的“使用用户名/密码进行身份验证”,Smack 支持它。除非我想写一个 Facebook 应用程序。然后我会尝试使用“使用 Facebook 平台进行身份验证”登录。

因此,只需像使用普通 Jabber 客户端一样使用 Smack 连接到 FB 聊天。

  1. 对于用户名,请使用您的 Facebook用户名。(见http://www.facebook.com/username/
  2. 对于域,请使用:chat.facebook.com
  3. 对于密码,请使用您的 Facebook密码
  4. 关闭SSL和 TSL
  5. 将连接端口设置为:5222(这是 XMPP 的默认值)
  6. 将连接服务器设置为chat.facebook.com
于 2011-03-16T00:52:02.580 回答