You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

如何检测运行中的macOS应用是否来自Mac App Store或自分发版本?

区分Mac App Store与自分发版本的可靠方法

这确实是Mac应用多渠道分发中很常见的需求,我在实际开发中踩过不少坑,给你梳理几个靠谱的检测方案,能避开单纯依赖收据的局限性:

一、最可靠的方式:检查应用签名信息

签名是Mac应用运行的核心凭证,App Store版本和自分发版本的签名有本质区别,非越狱环境下完全无法伪造:

1. 证书类型差异

  • App Store版本的签名证书通用名称(CN)会包含"Mac App Store",证书颁发链最终指向"Apple Worldwide Developer Relations Certification Authority"
  • 自分发(开发者ID签名)版本的证书通用名称是"Developer ID Application: 你的团队名称",颁发链指向"Developer ID Certification Authority"

2. 代码实现示例(Swift)

通过Security框架获取当前应用的签名信息,验证证书或权限字段:

import Security
import StoreKit

func isAppStoreDistributed() -> Bool {
    // 获取当前应用的签名对象
    let flags = SecCSFlags(rawValue: kSecCSDynamicPath)
    var code: SecCode?
    guard SecCodeCopySelf(flags, &code) == errSecSuccess, let appCode = code else {
        return false
    }
    
    // 获取签名详情
    var signingInfo: CFDictionary?
    guard SecCodeCopySigningInformation(appCode, SecCSFlags(rawValue: kSecCSSigningInformation), &signingInfo) == errSecSuccess,
          let infoDict = signingInfo as? [String: Any] else {
        return false
    }
    
    // 检查证书链中的App Store标识
    if let certificates = infoDict[kSecCodeInfoCertificates as String] as? [SecCertificate] {
        for cert in certificates {
            if let certSubject = SecCertificateCopySubjectSummary(cert) as String? {
                if certSubject.contains("Mac App Store") {
                    return true
                }
            }
        }
    }
    
    // 辅助验证:检查权限中的应用标识格式
    if let entitlements = infoDict[kSecCodeInfoEntitlements as String] as? [String: Any],
       let appID = entitlements["com.apple.application-identifier"] as? String {
        // 替换成你的团队ID,App Store版本的appID必然以团队ID开头
        let yourTeamID = "YOUR_TEAM_ID_HERE"
        if appID.hasPrefix("\(yourTeamID).") {
            // 结合收据刷新验证(可选,进一步确认)
            let receiptURL = Bundle.main.appStoreReceiptURL
            if receiptURL != nil, FileManager.default.fileExists(atPath: receiptURL!.path) {
                return true
            } else {
                // 异步尝试刷新收据,后续通过回调确认
                let refreshReq = SKReceiptRefreshRequest(receiptProperties: nil)
                refreshReq.delegate = self // 需实现SKRequestDelegate处理结果
                refreshReq.start()
            }
        }
    }
    
    return false
}

// 实现SKRequestDelegate处理收据刷新结果
extension YourViewController: SKRequestDelegate {
    func requestDidFinish(_ request: SKRequest) {
        if request is SKReceiptRefreshRequest {
            let receiptExists = FileManager.default.fileExists(atPath: Bundle.main.appStoreReceiptURL!.path)
            if receiptExists {
                // 确认是App Store版本
            }
        }
    }
    
    func request(_ request: SKRequest, didFailWithError error: Error) {
        if request is SKReceiptRefreshRequest {
            // 刷新失败,大概率是自分发版本
        }
    }
}

二、收据验证(需规避“缺失即非App Store”的误区)

你提到的那个库的问题在于太绝对——收据缺失不代表不是App Store版本:用户可能从备份恢复应用、离线运行,都会导致收据暂时缺失。正确的做法是:

  • 不要仅判断收据是否存在,而是尝试刷新收据:App Store版本能成功获取到苹果签名的收据,自分发版本会刷新失败
  • 即使收据存在,也要验证其签名合法性(通过SKReceiptValidator或手动验证),避免伪造收据

三、辅助判断:安装路径(仅作补充)

默认情况下App Store应用会安装在/Applications,但用户可以手动移动,所以这个只能作为辅助参考,不能作为核心判断依据。

关键总结

  • 优先用签名检测:这是最可靠的,因为签名是应用运行的必要条件,且App Store签名无法伪造
  • 收据验证作为补充:解决签名检测可能存在的边缘情况,同时要处理收据缺失的场景
  • 绝对不要仅靠“收据是否存在”来判断,这会误判很多正常场景

内容的提问来源于stack exchange,提问作者Duck

火山引擎 最新活动