CNCopyCurrentNetworkInfo详解

2019-10-24  本文已影响0人  starmier

最近收到了苹果的邮件,说获取WiFi SSID的接口[CNCopyCurrentNetworkInfo](https://developer.apple.com/documentation/systemconfiguration/1614126-cncopycurrentnetworkinfo) 不再返回SSID的值。不仔细看还真会被吓一跳,对物联网的相关APP简直是炸弹。仔细看邮件还好说明了可以先获取用户位置权限才能返回SSID。

```

Dear Developer,

As we announced at WWDC19, we're making changes to further protect user privacy and prevent unauthorized location tracking. Starting with iOS 13, the CNCopyCurrentNetworkInfo API will no longer return valid Wi-Fi SSID and BSSID information. Instead, the information returned by default will be:

SSID: “Wi-Fi” or “WLAN” (“WLAN" will be returned for the China SKU)

BSSID: "00:00:00:00:00:00"

If your app is using this API, we encourage you to adopt alternative approaches that don’t require Wi-Fi or network information. Valid SSID and BSSID information from CNCopyCurrentNetworkInfo will still be provided to VPN apps, apps that have used NEHotspotConfiguration to configure the current Wi-Fi network, and apps that have obtained permission to access user location through Location Services.

Test your app on the latest iOS 13 beta to make sure it works properly. If your app requires valid Wi-Fi SSID and BSSID information to function, you can do the following:

For accessory setup apps, use the NEHotSpotConfiguration API, which now has the option to pass a prefix of the SSID hotspot your app expects to connect to.

For other types of apps, use the CoreLocation API to request the user’s consent to access location information.

Learn more by reading the updated documentation or viewing the the Advances in Networking session video from WWDC19\. You can also submit a TSI for code-level support.

Best regards,

Apple Developer Relations

```

### 简述CNCopyCurrentNetworkInfo

从 iOS 4.1 开始,Apple 提供了「[CNCopyCurrentNetworkInfo](https://link.zhihu.com/?target=https%3A//developer.apple.com/documentation/systemconfiguration/1614126-cncopycurrentnetworkinfo)」这项函数,调用时将会得到 SSID 与 BSSID(本质上是** Wi-Fi 对应的 MAC 地址**)信息:

```

/*!

@function CNCopyCurrentNetworkInfo

@discussion Returns the Network Info for the specified interface.

For example, Network Info dictionary will contain the following

keys, and values:

<pre>

@textblock

Keys                      : Values

=======================================

kCNNetworkInfoKeySSIDData : CFDataRef

kCNNetworkInfoKeySSID    : CFStringRef

kCNNetworkInfoKeyBSSID    : CFStringRef

@/textblock

</pre>

@param interfaceName Name of the interface you are interested in

@result Network Info dictionary associated with the interface.

Returns NULL if an error was encountered.

You MUST release the returned value.

*/

```

`CNCopyCurrentNetworkInfo` 函数需要通过名为 [Access WiFi Information Entitlement](https://link.zhihu.com/?target=https%3A//developer.apple.com/documentation/bundleresources/entitlements/com_apple_developer_networking_wifi-info%3Flanguage%3Dobjc) 的 key 判断后才能调用。从 iOS 12 开始,调用该函数将默认返回 `nil`,需要在 Xcode 项目中开启「Access WiFi Information」后才会返回正确的值。这个功能需要在开发者页面的 App IDs 中激活才能使用。

> *To use this function in iOS 12 and later, enable the Access WiFi Information capability for your app in Xcode. When you enable this capability, Xcode automatically adds the Access WiFi Information entitlement to your entitlements file and App ID.*

而在 iOS 13 中,使用这项函数的条件将变得更为严格。根据 [WWDC19 Session 713](https://link.zhihu.com/?target=https%3A//developer.apple.com/videos/play/wwdc2019/713/)(Advances in Networking, Part 2,0:53:45)的说明,在 iOS 13 中除了开启「Access WiFi Information」以外,App 还需要符合下列三项条件中的至少一项才会返回正确的 `CNCopyCurrentNetworkInfo` 函数值,否则仍然会返回 `nil` :

![image](https://img.haomeiwen.com/i1996279/e9d6b93ee0b3997a.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

*  获得了定位服务权限的应用;

*  目前正处于启用状态的 VPN 应用;

*  使用 [NEHotspotConfiguration](https://link.zhihu.com/?target=https%3A//developer.apple.com/documentation/networkextension/nehotspotconfiguration)(仅支援通过应用配置的 Wi-Fi 网路)。

这里的 `NEHotspotConfiguration` 是在 iOS 11 中新加入的一个 class,它的特性简单来说是某个 App 将已知 Wi-Fi 的 SSID、密码等信息加入到 App 内的配置文件后,这个 App 可以直接在应用内完成连接至该 Wi-Fi 网路的操作,不需要再跳转至系统偏好设置。对于拥有 `NEHotspotConfiguration` 但未获得定位服务权限或 VPN 的应用来说,只有加入至 App 内配置文件的 Wi-Fi 才能调用对应的 `CNCopyCurrentNetworkInfo`。

负责网路技术的 Apple 工程师 Stuart Cheshire 在 WWDC19 上对这一改变的解释是:

> *Now you all know the importance of privacy to Apple. And one of the things we realized is that accessing Wi-Fi information can be used to infer location. So starting now, to access that Wi-Fi information, you will need the same kind of privileges that you need to get other location information.*

可以看到,Apple 还是由于 BSSID 信息,也就是 MAC 地址容易被推算出用户当前所处的地理位置而做出的这项改变。由于大部分像路由器这样的 AP 设备都被固定在了一个地理位置,而 BSSID 又能作为 Wi-Fi 设备的唯一标识,iOS 在定位时也会使用这种方法搭配 AP 设备的 RSSI 信号强度进行三角定位。即使对于无法获取 RSSI 的第三方 App 而言,只要拥有了相应的数据,仅依靠 BSSID 也能获取到用户的大致地理位置。

由于蓝牙设备同样具备 MAC 地址,这也是为什么 iOS 13 中新加入了「蓝牙」权限的原因之一。

那么,依赖 `CNCopyCurrentNetworkInfo` 的第三方 App 该如何解决这一问题呢?我按照使用该函数的一些常见 App 类型进行了分类:

#### 1\. Wi-Fi 连接类 App

这一类别主要包括了「Wi-Fi XX 钥匙」「XX 地铁 Wi-Fi」以及一些连接企业 AP(这些通常也属于 VPN 应用)的 App。其实这类 App 里有一些已经从 iOS 11 开始用上了刚刚提到的 `NEHotspotConfiguration`,因为它们都拥有要连接 Wi-Fi 的 SSID 与密码数据,满足使用要求。对于「Wi-Fi XX 钥匙」这类需要支撑千万级别 Wi-Fi 数据的 App 来说,可以向 Apple 申请使用 iOS 9 开始提供的 [NEHotspotHelper](https://link.zhihu.com/?target=https%3A//developer.apple.com/documentation/networkextension/nehotspothelper) class,它的作用是获取设备当前 Wi-Fi 列表中的所有信息,并支援在系统设置的 Wi-Fi 列表中加入自定义的注释。

```

//连接WiFi,设置名字和密码

NEHotspotConfiguration *c = [[NEHotspotConfiguration alloc] initWithSSID:@"Asus_c039" passphrase:@"leedarson@638" isWEP:NO];

NEHotspotConfigurationManager *m = [NEHotspotConfigurationManager sharedManager];

[m applyConfiguration:c completionHandler:^(NSError * _Nullable error) {

    NSLog(@"error :%@",error);

}];

//获取ssid

- (NSString*) getWifiSsid {

    NSString *wifiName = nil;

   

    CFArrayRef wifiInterfaces = CNCopySupportedInterfaces();

   

    if (!wifiInterfaces) {

        return nil;

    }

   

    NSArray *interfaces = (__bridge NSArray *)wifiInterfaces;

   

    for (NSString *interfaceName in interfaces) {

        CFDictionaryRef dictRef = CNCopyCurrentNetworkInfo((__bridge CFStringRef)(interfaceName));

       

        if (dictRef) {

            NSDictionary *networkInfo = (__bridge NSDictionary *)dictRef;

            NSLog(@"network info -> %@", networkInfo);

            wifiName = [networkInfo objectForKey:(__bridge NSString *)kCNNetworkInfoKeySSID];

           

            CFRelease(dictRef);

        }

    }

   

    CFRelease(wifiInterfaces);

    return wifiName;

}

```

#### 2\. 定位类 App

虽然大部分 App 都通过 [Core Location](https://link.zhihu.com/?target=https%3A//developer.apple.com/documentation/corelocation) 来获取与地理位置有关的信息,但仍然有一些 App 会在没有定位服务权限的情况下使用 `CNCopyCurrentNetworkInfo` 来获取地理位置。除去一些拥有专门数据来进行室内定位等情况的 App 而言,一般情况下直接改用 `Core Location` 会更好一些,因为在 iOS 13 上无论是通过 `CNCopyCurrentNetworkInfo` 还是 `Core Location` 来获取定位现在都会有这样的提示了:

![image](https://img.haomeiwen.com/i1996279/11e67b5ea9452c14.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

```

- (NSString*) getWifiSsid {

    if (@available(iOS 13.0, *)) {

        //用户明确拒绝,可以弹窗提示用户到设置中手动打开权限

        if ([CLLocationManager authorizationStatus] == kCLAuthorizationStatusDenied) {

            NSLog(@"User has explicitly denied authorization for this application, or location services are disabled in Settings.");

            //使用下面接口可以打开当前应用的设置页面

            //[[UIApplication sharedApplication] openURL:[NSURL URLWithString:UIApplicationOpenSettingsURLString]];

            return nil;

        }

        CLLocationManager* cllocation = [[CLLocationManager alloc] init];

        if(![CLLocationManager locationServicesEnabled] || [CLLocationManager authorizationStatus] == kCLAuthorizationStatusNotDetermined){

            //弹框提示用户是否开启位置权限

            [cllocation requestWhenInUseAuthorization];

            usleep(500);

            //递归等待用户选选择

            return [self getWifiSsid];

        }

    }

    NSString *wifiName = nil;

    CFArrayRef wifiInterfaces = CNCopySupportedInterfaces();

    if (!wifiInterfaces) {

        return nil;

    }

    NSArray *interfaces = (__bridge NSArray *)wifiInterfaces;

    for (NSString *interfaceName in interfaces) {

        CFDictionaryRef dictRef = CNCopyCurrentNetworkInfo((__bridge CFStringRef)(interfaceName));

       

        if (dictRef) {

            NSDictionary *networkInfo = (__bridge NSDictionary *)dictRef;

            NSLog(@"network info -> %@", networkInfo);

            wifiName = [networkInfo objectForKey:(__bridge NSString *)kCNNetworkInfoKeySSID];

            CFRelease(dictRef);

        }

    }

   

    CFRelease(wifiInterfaces);

    return wifiName;

}

```

我们会得到如下结果

```

//同意的打印结果

2019/08/09 14:23:47:121 network info -> {

    BSSID = "4c:ed:fb:a0:91:e4";

    SSID = "Asus_c039";

    SSIDDATA = <41737573 5f633033 39>;

}

//如果用户不获取位置权限的结果

2019/08/09 14:34:01:586 network info -> {

    BSSID = "00:00:00:00:00:00";

    SSID = WLAN;

    SSIDDATA = <574c414e>;

}

```

#### 3\. VPN 类 App

包括刚才说到的连接企业 AP 的应用,以及像 Surge 这样会通过不同 SSID 来切换配置文件的 VPN App,只要在获得了 [NEVPNManager](https://link.zhihu.com/?target=https%3A//developer.apple.com/documentation/networkextension/nevpnmanager) 并且开发者账户登记为企业的情况下可以直接使用 `CNCopyCurrentNetworkInfo` 函数,不需要考虑定位服务权限的情况(目前的 iOS 13 beta 这项特性存在着一些问题,VPN App 可能依然无法获取相关数据)。

#### ## 4\. Wi-Fi 绑定类 App

这类 App 是所有 `CNCopyCurrentNetworkInfo` 函数中使用情况最复杂,也最难解决这一问题的 App,所以特别把它放在最后来讲。这一类 App 在调用函数后会将 Wi-Fi 的 SSID 与 BSSID 信息保存到应用内进行绑定,以供 App 与该 Wi-Fi 设备的通信使用。这一类 App 可以说是千奇百怪什么样的都有,这里举一个比较典型的「米家」App 好了。

米家 App 的主要功能是通过蓝牙或 Wi-Fi 连接智能家居设备进行通信控制,Wi-Fi 连接设备的方式一般是这样的:

1)智能家居内的 Wi-Fi 模块先进行 SSID 广播,iOS 设备的米家 App 需要先输入让智能家居接入的设备工作 Wi-Fi SSID(这里通常是路由器)与密码;

2)在输入完毕后,米家 App 将引导用户至 iOS 的系统设置连接智能家居的 Wi-Fi(通常是 xiaomi-XXXX 这样的 SSID),连接成功后再次打开米家 App,让 App 获取到这一设备的 SSID 与 BSSID 信息,同时将需要接入的 Wi-Fi 数据以及账户信息通过连接着的 Wi-Fi 网路传输给智能家居进行绑定;

3)完成绑定后智能家居将直接通过接入的设备工作 Wi-Fi 与内网或小米路由器通信,不需要再通过 iOS 设备直接连接。

![image](https://img.haomeiwen.com/i1996279/e45bae24c34a7766.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

简单来说就是在绑定设备时,`CNCopyCurrentNetworkInfo` 成为了一个米家 App 必须要获取的数据:如果说设备工作 Wi-Fi 的 SSID 可以让用户手动输入,那么如何获取智能家居设备的 BSSID 就成了一个绕不开的问题(目前米家 App 在 iOS 13 上就因为这个问题而导致无法正常连接 Wi-Fi 设备)。我觉得对于这种情况而言,有以下几种解决办法:

**1)直接获取定位服务权限:**这种方法最简单粗暴,只需要写好获取定位服务权限的代码就可以解决 `CNCopyCurrentNetworkInfo` 无法调用的问题。但对于用户来说,这一点并不是最好的办法,因为当用户允许了米家 App 的定位服务权限时,就意味着米家 App 同样可以通过 `Core Location`等方式获得原来拿不到的精确定位,至少对于我来说是不希望米家仅仅用这种方式简单地来处理了。

**2)通过 Wi-Fi 通信方式直接获取 SSID 与 BSSID 数据:**虽然现在 `CNCopyCurrentNetworkInfo` 中的 SSID 与 BSSID 无法直接调用了,但这并不意味着这些数据没有办法从其他渠道拿到,因为即使无法调用 iOS 的 SDK,在连接至智能家居的 Wi-Fi 后它们依然可以与 App 传输各种包括 Wi-Fi 信息在内的数据。如果米家的 App 已经提前考虑到了这一点并在智能家居的 Wi-Fi 传输协议中写好了相关信息,那么它们也可以绕开系统 SDK 直接通过 Wi-Fi 传输至 App 中。

**3)通过其它手段获取 SSID 与 BSSID 数据:**如果米家 App 没有提前写好传输协议,还可以通过一些其它手段来获取相应的数据。比如米家摄像机一类的设备,是通过 App 生成一个包含设备工作 Wi-Fi 的 SSID、密码等信息的 QR 码供设备扫描连接,同样绕开了对 `CNCopyCurrentNetworkInfo` 的获取;下下之策则是让用户主动寻找再手动输入智能家居的 Wi-Fi 信息,并在之后销售的智能家居产品包装里再贴上相应的 QR 码,但我觉得以现有的技术来说完全没有必要做到这个地步。

网上也有人考虑过像米家这样的 App 是否可以通过 `NEHotspotConfiguration` class 来获取 Wi-Fi 数据,但至少就米家 App 而言很难做到。`NEHotspotConfiguration` 需要 App 在描述文件中提供 SSID 信息,而米家的大部分智能家居设备为了避免设备冲突,每一台设备都使用了不同的后缀来进行区分;把它们都加进描述文件虽然理论上没有问题,但实际可能会让情况变得非常复杂,`NEHotspotConfiguration` 更加适合像「ChinaNet」「花生地铁WiFi」这样固定的 SSID 使用。

还有一类 App,它们读取 SSID 和 BSSID 纯粹就是想偷偷统计用户在哪里使用自己家的应用,这种就无需处理了。。。。

上一篇下一篇

猜你喜欢

热点阅读