macOS - SMJobBless 獲取管理員權(quán)限

*如果應(yīng)用程序偶爾需要管理員權(quán)限執(zhí)行命令,可使用AppleScript接口
*但是如果應(yīng)用程序需要頻繁使用管理員權(quán)限執(zhí)行命令,這時(shí)候就需要用到SMJobBless獲取長(zhǎng)時(shí)間可用的權(quán)限了
*由于此方法不能使用App Sandbox,可能會(huì)影響上架App Store

1.添加Target,作為后臺(tái)程序(Helper)

File -> New -> Target... -> macOS -> Command Line Tool

SMJobBless - New Target.png

命名格式一般為 com.yourcompany.mainapp.helper

SMJobBless - New Target Name.png

設(shè)置Bundle Identifier和Targer名一致,并且建議重命名一下Helper目錄(例子里把"com.ljq.SMJobBlessApp.CommandHelper"目錄命名為"CommandHelper"了)

SMJobBless - Helper Bundle ID.png

2.創(chuàng)建plist文件

選中Helper的目錄,F(xiàn)ile -> New -> File...(或Commmand + N),創(chuàng)建Property List文件,命名為"CommandHelper-Info"

SMJobBless - New Property List File.png

選中CommandHelper-Info.plist右鍵 -> Open As -> Soure Code,復(fù)制粘貼以下內(nèi)容并修改:

CFBundleIdentifier:Helper程序的Bundle Identifier

CFBundleName:Helper程序的名稱

SMAuthorizedClients里的"com.ljq.SMJobBlessApp"為主Target的Bundle Identifier

SMAuthorizedClients里的"GN79NMG846"為開發(fā)者賬號(hào)的Team Identifier

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>CFBundleIdentifier</key>
    <string>com.ljq.SMJobBlessApp.CommandHelper</string>
    <key>CFBundleInfoDictionaryVersion</key>
    <string>6.0</string>
    <key>CFBundleName</key>
    <string>CommandHelper</string>
    <key>CFBundleVersion</key>
    <string>1.0</string>
    <key>SMAuthorizedClients</key>
    <array>
        <string>anchor apple generic and identifier &quot;com.ljq.SMJobBlessApp&quot; and (certificate leaf[field.1.2.840.113635.100.6.1.9] /* exists */ or certificate 1[field.1.2.840.113635.100.6.2.6] /* exists */ and certificate leaf[field.1.2.840.113635.100.6.1.13] /* exists */ and certificate leaf[subject.OU] = &quot;GN79NMG846&quot;)</string>
    </array>
</dict>
</plist>

如圖所示:

SMJobBless - Helper Info Plist.png

再次選中Helper的目錄,創(chuàng)建Property List文件,此次命名為"CommandHelper-Launchd",并且和上面一樣復(fù)制粘貼以下內(nèi)容并修改:

Label:Helper程序的Bundle Identifier

MachServices:用于XPC通信服務(wù)

(其他為可選參數(shù))

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>com.ljq.SMJobBlessApp.CommandHelper</string>
    <key>RunAtLoad</key>
    <true/>
    <key>MachServices</key>
    <dict>
        <key>com.ljq.SMJobBlessApp.CommandHelper</key>
        <true/>
    </dict>
    <key>KeepAlive</key>
    <dict>
        <key>SuccessfulExit</key>
        <false/>
    </dict>
</dict>
</plist>

如圖所示:

SMJobBless - Helper Launchd Plist.png

3.配置主Target

主Target -> General -> Frameworks... ,添加SystemConfiguration.framework與Security.framework

SMJobBless - General Frameworks.png

主Target -> Signing & Capabilities ,移除App Sandbox(假如有)

SMJobBless - App Sandbox.png

主Target -> Build Phases -> Dependencies,添加Helper

SMJobBless - Dependencies.png

主Target -> Build Phases -> Copy Bundle Resources添加"CommandHelper-Info.plist"和"CommandHelper-Launchd.plist"

SMJobBless - Copy Bundle Resources.png

主Target -> Build Phases 左上角+號(hào) New Copy Files Phase

SMJobBless - New Copy Files Phase.png

Destination 改為 Wrapper,Subpath 改為 "Contents/Library/LaunchServices" ,并在下面添加Helper程序

SMJobBless - Copy Files Wrapper.png

選中主Target的Info.plist文件,修改"com.ljq.SMJobBlessApp.CommandHelper"和"GN79NMG846"并添加以下內(nèi)容:

<key>SMPrivilegedExecutables</key>
<dict>
    <key>com.ljq.SMJobBlessApp.CommandHelper</key>
    <string>anchor apple generic and identifier &quot;com.ljq.SMJobBlessApp.CommandHelper&quot; and (certificate leaf[field.1.2.840.113635.100.6.1.9] /* exists */ or certificate 1[field.1.2.840.113635.100.6.2.6] /* exists */ and certificate leaf[field.1.2.840.113635.100.6.1.13] /* exists */ and certificate leaf[subject.OU] = &quot;GN79NMG846&quot;)</string>
</dict>

如圖所示:

SMJobBless - Main Info Plist.png

注:如果出問(wèn)題,可以嘗試移除.entitlements文件試試,非必須的

SMJobBless - Remove Entitlements File.png

假如要移除entitlements文件,還需要在主Target -> Build Settings -> Signing -> Code Signing Entitlements 刪除其內(nèi)容

SMJobBless - Remove Entitlements Setting.png

4.配置Helper Target

Helper Target -> Build Settings -> Linking -> Other Linker Flags,修改以下內(nèi)容復(fù)制進(jìn)去(點(diǎn)擊+直接粘貼長(zhǎng)文本,回車后會(huì)自動(dòng)換行),下面兩個(gè)"xxx.plist"路徑要修改為項(xiàng)目實(shí)際的路徑,其他不用改

-sectcreate __TEXT __info_plist "$(SRCROOT)/CommandHelper/CommandHelper-Info.plist" -sectcreate __TEXT __launchd_plist "$(SRCROOT)/CommandHelper/CommandHelper-Launchd.plist"

如圖所示:

SMJobBless - Other Linker Flags.png

Helper Target -> Build Settings -> Packaging -> Info.plist File,修改以下路徑復(fù)制進(jìn)去

$(SRCROOT)/CommandHelper/CommandHelper-Info.plist

如圖所示:

SMJobBless - Packaging.png

5.1.編寫安裝代碼(以下內(nèi)容建議直接查看demo源碼)

修改ViewController.m,運(yùn)行時(shí)如果彈窗內(nèi)容為"The Helper Tool is available!",表示Helper程序已成功安裝,如果為其他則表示安裝失敗,建議檢查以上流程

#import "ViewController.h"
#import <ServiceManagement/ServiceManagement.h>
#import <Security/Authorization.h>

@interface ViewController (){
    AuthorizationRef _authRef;
}
@end

@implementation ViewController

- (void)viewDidLoad
{
    [super viewDidLoad];
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        [self initHelper];
    });
}

- (void)initHelper
{
    BOOL isAvailable        = YES;
    NSError *error          = nil;
    NSString *installedPath = [NSString stringWithFormat:@"/Library/PrivilegedHelperTools/%@", @"com.ljq.SMJobBlessApp.CommandHelper"];
    if ([[NSFileManager defaultManager] fileExistsAtPath:installedPath] == NO) {
        OSStatus status     = AuthorizationCreate(NULL, kAuthorizationEmptyEnvironment, kAuthorizationFlagDefaults, &self->_authRef);
        if (status == errAuthorizationSuccess) {
            isAvailable     = [self blessHelperWithLabel:@"com.ljq.SMJobBlessApp.CommandHelper" error:&error];
        } else {
            self->_authRef  = NULL;
            isAvailable     = NO;
            assert(NO);/* AuthorizationCreate really shouldn't fail. */
        }
    }
    NSAlert *alert          = [NSAlert new];
    alert.informativeText   =
    isAvailable ?
    @"The Helper Tool is available!" :
    [NSString stringWithFormat:@"Something went wrong! code:%ld %@",error.code,error.localizedDescription];
    [alert beginSheetModalForWindow:self.view.window completionHandler:nil];
}

- (BOOL)blessHelperWithLabel:(NSString *)label error:(NSError **)errorPtr
{
    BOOL result     = NO;
    NSError * error = nil;
    
    AuthorizationItem authItem      = { kSMRightBlessPrivilegedHelper, 0, NULL, 0 };
    AuthorizationRights authRights  = { 1, &authItem };
    AuthorizationFlags flags        =   kAuthorizationFlagDefaults              |
                                        kAuthorizationFlagInteractionAllowed    |
                                        kAuthorizationFlagPreAuthorize          |
                                        kAuthorizationFlagExtendRights;
                                               
    /* Obtain the right to install our privileged helper tool (kSMRightBlessPrivilegedHelper). */
    OSStatus status = AuthorizationCopyRights(self->_authRef, &authRights, kAuthorizationEmptyEnvironment, flags, NULL);
    if (status != errAuthorizationSuccess) {
        NSString *errMsg = (__bridge_transfer NSString *)SecCopyErrorMessageString(status, NULL);
        error = [NSError errorWithDomain:NSOSStatusErrorDomain code:status userInfo:@{NSLocalizedDescriptionKey:errMsg}];
    } else {
        CFErrorRef  cfError;
        /* This does all the work of verifying the helper tool against the application
         * and vice-versa. Once verification has passed, the embedded launchd.plist
         * is extracted and placed in /Library/LaunchDaemons and then loaded. The
         * executable is placed in /Library/PrivilegedHelperTools.
         */
        result = (BOOL)SMJobBless(kSMDomainSystemLaunchd, (__bridge CFStringRef)label, self->_authRef, &cfError);
        if (!result) {
            error = CFBridgingRelease(cfError);
        }
    }
    if (!result && (errorPtr != NULL) ) {
        assert(error != nil);
        *errorPtr = error;
    }
    return result;
}

@end

5.2.編寫XPC通信代碼

光是安裝Helper程序并不算得上是一個(gè)完整的代碼,此時(shí)需要使用到XPC接口來(lái)調(diào)用Helper程序

選中Helper的目錄添加頭文件(File -> New -> File... -> macOS -> Header File),命名為CommandHelperProtocol.h,并添加以下內(nèi)容

#import <Foundation/Foundation.h>

@protocol CommandHelperProtocol

- (void)executeCommand:(NSString *)command reply:(void(^)(int result))reply;

@end

再在此目錄添加一個(gè)類(File -> New -> File... -> macOS -> Cocoa Class),命名為CommandHelper,并添加以下內(nèi)容

CommandHelper.h

#import <Foundation/Foundation.h>
#import "CommandHelperProtocol.h"

@interface CommandHelper : NSObject

- (void)run;

@end

CommandHelper.m

#import "CommandHelper.h"

@interface CommandHelper () <NSXPCListenerDelegate>

@property (nonatomic, strong) NSXPCListener *listener;

@end

@implementation CommandHelper

- (instancetype)init
{
    self = [super init];
    if (self) {
        self.listener           = [[NSXPCListener alloc] initWithMachServiceName:@"com.ljq.SMJobBlessApp.CommandHelper"];
        self.listener.delegate  = self;
    }
    return self;
}

- (BOOL)listener:(NSXPCListener *)listener shouldAcceptNewConnection:(NSXPCConnection *)newConnection {
    newConnection.exportedInterface = [NSXPCInterface interfaceWithProtocol:@protocol(CommandHelperProtocol)];
    newConnection.exportedObject    = self;
    [newConnection resume];
    return YES;
}

- (void)executeCommand:(NSString *)command reply:(nonnull void (^)(int))reply
{
    reply(system(command.UTF8String));
}

- (void)run
{
    [self.listener resume];
    [[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
    [[NSRunLoop currentRunLoop] run];
}

@end

最后在Helper目錄下的main.m中添加以下內(nèi)容

#import <Foundation/Foundation.h>
#import "CommandHelper.h"

int main(int argc, const char * argv[]) {
    [[CommandHelper new] run];
    return 0;
}

測(cè)試是否可用(更新了Helper代碼后,需要在/Library/PrivilegedHelperTools/目錄下刪除此項(xiàng)目的Helper程序)

往ViewController.m添加以下代碼,測(cè)試設(shè)置Wi-Fi的DNS命令

...
#import "CommandHelperProtocol.h"

...

@implementation ViewController

....

- (void)initHelper
{
    ...
    
    if (isAvailable) {
        // Reset DNS: "networksetup -setdnsservers Wi-Fi Empty"
        [self executeCommand:@"networksetup -setdnsservers Wi-Fi 8.8.8.8" reply:^(int result) {
            dispatch_async(dispatch_get_main_queue(), ^{
                NSAlert *alert          = [NSAlert new];
                alert.informativeText   =
                result == errSecSuccess ?
                @"Execute succeeded!" :
                [NSString stringWithFormat:@"Execute failed: %d",result];
                [alert beginSheetModalForWindow:self.view.window completionHandler:nil];
            });
        }];
    } else {
        NSAlert *alert          = [NSAlert new];
        alert.informativeText   = [NSString stringWithFormat:@"Something went wrong! code:%ld %@",error.code,error.localizedDescription];
        [alert beginSheetModalForWindow:self.view.window completionHandler:nil];
    }
}

- (void)executeCommand:(NSString *)command reply:(void(^)(int result))reply
{
    NSXPCConnection *xpcConnection      = [[NSXPCConnection alloc] initWithMachServiceName:@"com.ljq.SMJobBlessApp.CommandHelper"
                                                                                   options:NSXPCConnectionPrivileged];
    xpcConnection.remoteObjectInterface = [NSXPCInterface interfaceWithProtocol:@protocol(CommandHelperProtocol)];
    xpcConnection.exportedInterface     = [NSXPCInterface interfaceWithProtocol:@protocol(CommandHelperProtocol)];
    xpcConnection.exportedObject        = self;
    [[xpcConnection remoteObjectProxyWithErrorHandler:^(NSError * _Nonnull error) {
        // 無(wú)法連接XPC服務(wù)、Helper進(jìn)程已退出或已崩潰
        NSLog(@"Get remote object proxy error: %@",error);
        reply((int)error.code);
    }] executeCommand:command reply:reply];
    [xpcConnection resume];
}

...

@end

假如程序順利執(zhí)行,就會(huì)把Wi-Fi下的DNS設(shè)置為8.8.8.8

SMJobBless - Test.png

6.卸載

新建一個(gè)'Uninstall.sh'腳本到主Target的目錄下,添加以下內(nèi)容并修改"com.ljq.SMJobBlessApp.CommandHelper"

#!/bin/bash

sudo launchctl unload /Library/LaunchDaemons/com.ljq.SMJobBlessApp.CommandHelper.plist
sudo rm /Library/LaunchDaemons/com.ljq.SMJobBlessApp.CommandHelper.plist
sudo rm /Library/PrivilegedHelperTools/com.ljq.SMJobBlessApp.CommandHelper

執(zhí)行'Uninstall.sh'腳本

NSString *scriptPath    = [[NSBundle mainBundle] pathForResource:@"Uninstall" ofType:@"sh"];
NSString *shellScript   = [NSString stringWithFormat:@"out=`sh \\\"%@\\\"`",scriptPath];
NSString *script        = [NSString stringWithFormat:@"do shell script \"%@\" with administrator privileges", shellScript];
NSDictionary *errorInfo         = nil;
NSAppleScript *appleScript      = [[NSAppleScript new] initWithSource:script];
NSAppleEventDescriptor *result  = [appleScript executeAndReturnError:&errorInfo];
if (errorInfo == nil || errorInfo.count == 0) {
    NSLog(@"Uninstall succeeded!!!");
} else {
    NSLog(@"Uninstall failed! script result: %@ , error: %@", [result stringValue], errorInfo.description);
}

示例代碼

https://github.com/LeungKinKeung/SMJobBlessDemo

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容