1. 程式人生 > >Android連線WiFi再探索

Android連線WiFi再探索

應用場景

在安卓app上,使用者輸入WiFi名稱(SSID)和密碼,試圖連線這個WiFi。那麼使用者輸入的WiFi就有各種情況了,這個WiFi可以沒有密碼,也可以通過不同的加密方式加密。而不同的加密方式,需要寫不同的程式碼才能使WiFi連線成功。無論百度還是谷歌,搜出來的程式碼大都是針對WPA/WPA2加密方式的,即使有些考慮到了無密碼和WEP加密方式的WiFi連線,程式碼也都寫得不清不楚,看著實在糟心。於是在查閱了N多資料,看了WifiConfiguration的原始碼後,有了這篇文章。不保證本文的所有內容完全正確(因為我也只是半桶水),但至少可以給大家一條思路,供參考。

錯誤做法

先說說一個基本沒啥卵用的做法吧。程式碼如下:

    private int getSecurityType(WifiConfiguration config) {
        if (config.allowedKeyManagement.get(WifiConfiguration.KeyMgmt.WPA_PSK)) {
            return SECURITY_PSK;
        }
        if (config.allowedKeyManagement.get(WifiConfiguration.KeyMgmt.WPA_EAP) || config.allowedKeyManagement.get
(WifiConfiguration.KeyMgmt.IEEE8021X)) { return SECURITY_EAP; } return (config.wepKeys[0] != null) ? SECURITY_WEP : SECURITY_NONE; }

這段程式碼是我在網上搜到的,很多地方都有引用到,應該是互抄。有什麼問題呢?且不說通過密碼管理方式倒推出加密方式的邏輯是否嚴謹,只說一點,方法的入參WifiConfiguration config從何而來。網上給了答案,通過wifiManager.getConfiguredNetworks()呀,這不就拿到了。請注意,wifiManager.getConfiguredNetworks()是獲取配置好的網路連線,換句話說,你的裝置曾經連上過這些WiFi,裝置才能儲存這些config。而我的訴求是在從沒有連線過這些WiFi的情況下去連線,config並不存在。所以,這條路走不通,歇菜。

正確做法

正確的做法是:通過ScanResult獲取WiFi的資訊。

List results = wifiManager.getScanResults();
for (int i = 0; i < results.size(); i++) {
    ScanResult item = (ScanResult) results.get(i);
}

程式碼很好理解,拿到掃描到的WiFi列表。

if (item.SSID.equals(ssid)) {
    WifiConfiguration config = createWifiInfo(ssid, pwd, item);
    int netID = wifiManager.addNetwork(config);
    boolean bRet = wifiManager.enableNetwork(netID, true);
    Log.d(TAG, (bRet ? "Connect wifi ok" : "Connect wifi failed") + ",ssid=" + item.SSID);
}

通過SSID鎖定我們的目標WiFi,並生成連線需要的WifiConfiguration

    private WifiConfiguration createWifiInfo(String SSID, String Password, ScanResult scanResult) {
        WifiConfiguration config = new WifiConfiguration();
        config.allowedAuthAlgorithms.clear();
        config.allowedGroupCiphers.clear();
        config.allowedKeyManagement.clear();
        config.allowedPairwiseCiphers.clear();
        config.allowedProtocols.clear();
        config.SSID = "\"" + SSID + "\"";
        config.status = WifiConfiguration.Status.ENABLED;

        String firstCapabilities = scanResult.capabilities.substring(1, scanResult.capabilities.indexOf("]"));
        String[] capabilities = firstCapabilities.split("-");
        String auth = capabilities[0];
        String keyMgmt = "";
        String pairwiseCipher = "";

        if (capabilities.length > 1) {
            keyMgmt = capabilities[1];
        }
        if (capabilities.length > 2) {
            pairwiseCipher = capabilities[2];
        }

        /**
         *  設定認證方式和密碼(如果需要的話)
         *
         *  Open System authentication (required for WPA/WPA2)
         *  public static final int OPEN = 0;
         *  Shared Key authentication (requires static WEP keys)
         *  public static final int SHARED = 1;
         *  LEAP/Network EAP (only used with LEAP)
         *  public static final int LEAP = 2;
         */
        if (auth.contains("EAP")) {
            //EAP
            config.preSharedKey = "\"" + Password + "\"";
            config.allowedAuthAlgorithms.set(WifiConfiguration.AuthAlgorithm.LEAP);
        } else if (auth.contains("WPA")) {
            //WPA/WPA2
            config.preSharedKey = "\"" + Password + "\"";
            config.allowedAuthAlgorithms.set(WifiConfiguration.AuthAlgorithm.OPEN);
        } else if (auth.contains("WEP")) {
            //WEP
            config.wepKeys[0] = "\"" + Password + "\"";
            config.wepTxKeyIndex = 0;
            config.allowedAuthAlgorithms.set(WifiConfiguration.AuthAlgorithm.SHARED);
            config.allowedGroupCiphers.set(WifiConfiguration.GroupCipher.WEP104);
            config.allowedGroupCiphers.set(WifiConfiguration.GroupCipher.WEP40);
        } else {
            //NONE
        }

        /**
         *  設定加密協議
         *  WPA/IEEE 802.11i/D3.0
         *  public static final int WPA = 0;
         *  WPA2/IEEE 802.11i
         *  public static final int RSN = 1;
         */
        if (auth.contains("WPA2")) {
            config.allowedProtocols.set(WifiConfiguration.Protocol.RSN);
        } else if (auth.contains("WPA")) {
            config.allowedProtocols.set(WifiConfiguration.Protocol.WPA);
        }

        /**
         *  設定密碼管理方式
         *
         *  WPA is not used; plaintext or static WEP could be used.
         *  public static final int NONE = 0;
         *  WPA pre-shared key (requires {@code preSharedKey} to be specified).
         *  public static final int WPA_PSK = 1;
         *  WPA using EAP authentication. Generally used with an external authentication server.
         *  public static final int WPA_EAP = 2;
         *  IEEE 802.1X using EAP authentication and (optionally) dynamically generated WEP keys.
         *  public static final int IEEE8021X = 3;
         */
        if (!keyMgmt.equals("")) {
            if (keyMgmt.contains("IEEE802.1X")) {
                config.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.IEEE8021X);
            } else if (auth.contains("WPA") && keyMgmt.contains("EAP")) {
                config.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.WPA_EAP);
            } else if (auth.contains("WPA") && keyMgmt.contains("PSK")) {
                config.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.WPA_PSK);
            }
        } else {
            config.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.NONE);
        }

        /**
         *  設定PairwiseCipher和GroupCipher
         */
        if (!pairwiseCipher.equals("")) {
            if (pairwiseCipher.contains("CCMP")) {
                config.allowedGroupCiphers.set(WifiConfiguration.GroupCipher.CCMP);
                config.allowedPairwiseCiphers.set(WifiConfiguration.PairwiseCipher.CCMP);
            }
            if (pairwiseCipher.contains("TKIP")) {
                config.allowedGroupCiphers.set(WifiConfiguration.GroupCipher.TKIP);
                config.allowedPairwiseCiphers.set(WifiConfiguration.PairwiseCipher.TKIP);
            }
        }

        return config;
    }

關鍵邏輯講解

所謂的關鍵邏輯就是createWifiInfo這個方法了。方法很長,一點點看吧。

理論儲備

ScanResult的capabilities屬性很重要,它描述了認證、金鑰管理、接入點所支援的加密方案。列一下常見的capabilities:
1.[WPA2-PSK-CCMP][ESS]
2.[WPA2-PSK-CCMP+TKIP][ESS]
3.[WPA-PSK-CCMP+TKIP][WPA2-PSK-CCMP+TKIP][WPS][ESS]
4.[WPS][ESS]
5.[ESS]
6.……
如果你用iPhone開個手機熱點的話,capabilities就是第一個。如果你家裡用的是小米的路由器,沒有做過特殊設定,capabilities就是第三個。第四第五個是無密碼的情況,根據路由器的不同,可能是四也可能是五。

對於前三種情況,我們看到第一個[]裡面都有2個-,也就是說第一個[]裡用-分隔後可以得到三個資訊[Authentication Algorithm - Key Management Algorithm - Pairwise Cipher],中文[認證演算法-祕鑰管理演算法-成對加密]。

程式碼解析

後面要怎麼做就很清楚啦,拿到這三個資訊,挨個解析就可以了。

        /**
         *  設定認證演算法和密碼(如果需要的話)
         *
         *  Open System authentication (required for WPA/WPA2)
         *  public static final int OPEN = 0;
         *  Shared Key authentication (requires static WEP keys)
         *  public static final int SHARED = 1;
         *  LEAP/Network EAP (only used with LEAP)
         *  public static final int LEAP = 2;
         */
        if (auth.contains("EAP")) {
            //EAP
            config.preSharedKey = "\"" + Password + "\"";
            config.allowedAuthAlgorithms.set(WifiConfiguration.AuthAlgorithm.LEAP);
        } else if (auth.contains("WPA")) {
            //WPA/WPA2
            config.preSharedKey = "\"" + Password + "\"";
            config.allowedAuthAlgorithms.set(WifiConfiguration.AuthAlgorithm.OPEN);
        } else if (auth.contains("WEP")) {
            //WEP
            config.wepKeys[0] = "\"" + Password + "\"";
            config.wepTxKeyIndex = 0;
            config.allowedAuthAlgorithms.set(WifiConfiguration.AuthAlgorithm.SHARED);
            config.allowedGroupCiphers.set(WifiConfiguration.GroupCipher.WEP104);
            config.allowedGroupCiphers.set(WifiConfiguration.GroupCipher.WEP40);
        } else {
            //NONE
        }

auth的可選值有EAP、WPA、WPA2、WEP,如果都匹配不上就是NONE(無加密)。順說一句,WiFi安全程度按NONE->WEP->WPA->WPA2->EAP的順序增強。其中WPA/WPA2在這塊程式碼中要做的事情一樣,所以並起來寫了。

config.allowedGroupCiphers.set(WifiConfiguration.GroupCipher.WEP104);
config.allowedGroupCiphers.set(WifiConfiguration.GroupCipher.WEP40);

WEP分支中的這兩行程式碼和其他的不太一樣,先hold住不講,後面會解釋。

        /**
         *  設定安全協議
         *  WPA/IEEE 802.11i/D3.0
         *  public static final int WPA = 0;
         *  WPA2/IEEE 802.11i
         *  public static final int RSN = 1;
         */
        if (auth.contains("WPA2")) {
            config.allowedProtocols.set(WifiConfiguration.Protocol.RSN);
        } else if (auth.contains("WPA")) {
            config.allowedProtocols.set(WifiConfiguration.Protocol.WPA);
        }

原始碼寫得很清楚,auth為WPA2需要設定Protocol為RSN,auth為WPA則需要設定Protocol為WPA。網上搜到的程式碼有不少都沒有設定allowedProtocols,那麼在連線WPA2-PSK加密方式的WiFi時,就無法連上。

        /**
         *  設定祕鑰管理方式
         *
         *  WPA is not used; plaintext or static WEP could be used.
         *  public static final int NONE = 0;
         *  WPA pre-shared key (requires {@code preSharedKey} to be specified).
         *  public static final int WPA_PSK = 1;
         *  WPA using EAP authentication. Generally used with an external authentication server.
         *  public static final int WPA_EAP = 2;
         *  IEEE 802.1X using EAP authentication and (optionally) dynamically generated WEP keys.
         *  public static final int IEEE8021X = 3;
         */
        if (!keyMgmt.equals("")) {
            if (keyMgmt.contains("IEEE802.1X")) {
                config.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.IEEE8021X);
            } else if (auth.contains("WPA") && keyMgmt.contains("EAP")) {
                config.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.WPA_EAP);
            } else if (auth.contains("WPA") && keyMgmt.contains("PSK")) {
                config.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.WPA_PSK);
            }
        } else {
            config.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.NONE);
        }

祕鑰管理方式被分成了四類:NONE、WPA_PSK、WPA_EAP、IEEE8021X。這裡的WPA可以是WPA也可以是WPA2(即第一步的auth),進一步解釋,[WPA2-PSK-CCMP+TKIP]和[WPA-PSK-CCMP+TKIP]這兩種capabilities對應的祕鑰管理方式都是WPA_PSK。

        /**
         *  設定PairwiseCipher和GroupCipher
         */
        if (!pairwiseCipher.equals("")) {
            if (pairwiseCipher.contains("CCMP")) {
                config.allowedGroupCiphers.set(WifiConfiguration.GroupCipher.CCMP);
                config.allowedPairwiseCiphers.set(WifiConfiguration.PairwiseCipher.CCMP);
            }
            if (pairwiseCipher.contains("TKIP")) {
                config.allowedGroupCiphers.set(WifiConfiguration.GroupCipher.TKIP);
                config.allowedPairwiseCiphers.set(WifiConfiguration.PairwiseCipher.TKIP);
            }
        }

注意,pairwiseCipher只有在WPA/WPA2的加密方式裡才有。所以,回到第一步,WEP分支中比其他分支多了這兩行程式碼

config.allowedGroupCiphers.set(WifiConfiguration.GroupCipher.WEP104);
config.allowedGroupCiphers.set(WifiConfiguration.GroupCipher.WEP40);

是因為WEP加密方式中也需要allowedGroupCiphers,而capabilities裡沒有顯式表達出來,只有在解析auth的時候就設定好。EAP型別的認證不需要設定這個引數。

總結

看完這麼長串的程式碼,真的是頭痛病都要發作了。尤其是那個討厭的EAP,既可以出現在第一位作為認證方式,又能出現在第二位作為祕鑰管理方式(WPA_EAP)。而我這裡並沒有EAP加密方式的WiFi,所以這塊程式碼我不保證可以用。但是WPA/WPA2加密方式的WiFi親測是可以連線成功的。

最後P個S。
注意這行程式碼:

boolean bRet = wifiManager.enableNetwork(netID, true);

如果bRet為true,也請不要高興太早。bRet為true,只能說明系統可以用配置好的資訊去嘗試連線(嘗試、嘗試、嘗試,重要的事情說三遍!)。嘗試的意思就是,如果密碼不對,那麼最終還是無法連上WiFi的!所以要謹記,enableNetwork返回的僅僅是嘗試連線的結果,最終是否連上,此刻是不知道的。如果想知道,可以註冊一個BroadcastReceiver接收全域性的WiFi狀態。有空我會再補一篇文章把這裡梳理一下。

本文肯定會有疏漏,請大家多多指正。