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狀態。有空我會再補一篇文章把這裡梳理一下。
本文肯定會有疏漏,請大家多多指正。