美文网首页
macOS - SMJobBless 获取管理员权限

macOS - SMJobBless 获取管理员权限

作者: LeungKinKeung | 来源:发表于2021-08-30 17:16 被阅读0次

    *如果应用程序偶尔需要管理员权限执行命令,可使用AppleScript接口
    *但是如果应用程序需要频繁使用管理员权限执行命令,这时候就需要用到SMJobBless获取长时间可用的权限了
    *由于此方法不能使用App Sandbox,可能会影响上架App Store

    1.添加Target,作为后台程序(Helper)

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

    SMJobBless - New Target.png

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

    SMJobBless - New Target Name.png

    设置Bundle Identifier和Targer名一致,并且建议重命名一下Helper目录(例子里把"com.ljq.SMJobBlessApp.CommandHelper"目录命名为"CommandHelper"了)

    SMJobBless - Helper Bundle ID.png

    2.创建plist文件

    选中Helper的目录,File -> New -> File...(或Commmand + N),创建Property List文件,命名为"CommandHelper-Info"

    SMJobBless - New Property List File.png

    选中CommandHelper-Info.plist右键 -> Open As -> Soure Code,复制粘贴以下内容并修改:

    CFBundleIdentifier:Helper程序的Bundle Identifier

    CFBundleName:Helper程序的名称

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

    SMAuthorizedClients里的"GN79NMG846"为开发者账号的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的目录,创建Property List文件,此次命名为"CommandHelper-Launchd",并且和上面一样复制粘贴以下内容并修改:

    Label:Helper程序的Bundle Identifier

    MachServices:用于XPC通信服务

    (其他为可选参数)

    <?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 左上角+号 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"并添加以下内容:

    <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

    注:如果出问题,可以尝试移除.entitlements文件试试,非必须的

    SMJobBless - Remove Entitlements File.png

    假如要移除entitlements文件,还需要在主Target -> Build Settings -> Signing -> Code Signing Entitlements 删除其内容

    SMJobBless - Remove Entitlements Setting.png

    4.配置Helper Target

    Helper Target -> Build Settings -> Linking -> Other Linker Flags,修改以下内容复制进去(点击+直接粘贴长文本,回车后会自动换行),下面两个"xxx.plist"路径要修改为项目实际的路径,其他不用改

    -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,修改以下路径复制进去

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

    如图所示:

    SMJobBless - Packaging.png

    5.1.编写安装代码(以下内容建议直接查看demo源码)

    修改ViewController.m,运行时如果弹窗内容为"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程序并不算得上是一个完整的代码,此时需要使用到XPC接口来调用Helper程序

    选中Helper的目录添加头文件(File -> New -> File... -> macOS -> Header File),命名为CommandHelperProtocol.h,并添加以下内容

    #import <Foundation/Foundation.h>
    
    @protocol CommandHelperProtocol
    
    - (void)executeCommand:(NSString *)command reply:(void(^)(int result))reply;
    
    @end
    

    再在此目录添加一个类(File -> New -> File... -> macOS -> Cocoa Class),命名为CommandHelper,并添加以下内容

    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中添加以下内容

    #import <Foundation/Foundation.h>
    #import "CommandHelper.h"
    
    int main(int argc, const char * argv[]) {
        [[CommandHelper new] run];
        return 0;
    }
    

    测试是否可用(更新了Helper代码后,需要在/Library/PrivilegedHelperTools/目录下删除此项目的Helper程序)

    往ViewController.m添加以下代码,测试设置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) {
            // 无法连接XPC服务、Helper进程已退出或已崩溃
            NSLog(@"Get remote object proxy error: %@",error);
            reply((int)error.code);
        }] executeCommand:command reply:reply];
        [xpcConnection resume];
    }
    
    ...
    
    @end
    
    

    假如程序顺利执行,就会把Wi-Fi下的DNS设置为8.8.8.8

    SMJobBless - Test.png

    6.卸载

    新建一个'Uninstall.sh'脚本到主Target的目录下,添加以下内容并修改"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
    

    执行'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

    相关文章

      网友评论

          本文标题:macOS - SMJobBless 获取管理员权限

          本文链接:https://www.haomeiwen.com/subject/xhxqgktx.html