macOS - SMJobBless 获取管理员权限

*如果应用程序偶尔需要管理员权限执行命令,可使用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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,108评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,699评论 1 296
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,812评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,236评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,583评论 3 288
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,739评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,957评论 2 315
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,704评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,447评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,643评论 2 249
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,133评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,486评论 3 256
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,151评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,108评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,889评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,782评论 2 277
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,681评论 2 272

推荐阅读更多精彩内容