通过 PowerShell 创建计划任务的入门
我也曾尝试使用 PowerShell 在 Windows Server 2019 上创建计划任务。没有一个答案奏效。似乎所有的答案都确实有一些正确的解决方案,但没有一个有完整的解决方案。虽然有些答案可能对某些人有用,但我敢肯定,这完全取决于他们现有的系统、安全设置和其他因素。在通过 PowerShell 创建一个非常简单的计划任务以收集一些应用程序遥测数据的过程中,我学到了很多东西。
因此,我将完全修改我的原始答案,并逐步引导您(在最热心的情况下)创建计划任务,尤其是在服务器上。如果它像cron
.
为运行无人值守任务的用户分配登录作为批处理作业
当大多数人想要创建计划任务时,特别是为了服务器/应用程序维护,或者只是为了定期运行某些东西,第一站是 Windows 任务计划程序。提供了一个不错的 GUI(好吧,它可能会更好/更现代化,但是嘿,至少它是一个 GUI),您可以在其中指定所有必要的细节来让您继续前进。问题是,您不能自动化 GUI。正如我发现的那样,GUI 正在幕后做一些微软在告诉你它是如何做的(或者它是如何不做的,就此而言)并不完全坦率的事情。这可能会导致很多问题——包括任务没有运行,甚至任务运行的用户帐户被锁定。
在 Microsoft 的文档中,在组策略标题下的“作为批处理作业登录( SeBatchLogonRight
好吧,这种说法并不总是正确的。
当您使用任务计划程序 GUI 创建计划任务时,是的,如果计划任务配置为运行,无论用户是否登录并且用户没有作为批处理作业的登录权限,那么任务计划程序将分配用户的该权利(除非更改默认设置 - 请参阅上面的引用链接)。
但是,当使用 PowerShell ScheduledTask 模块的 cmdlet 创建计划任务时,不会发生这种自动用户权限分配。因此,您必须手动执行此操作。执行此操作的 GUI 方法是使用本地安全策略 MMC(Microsoft 管理控制台)。当然,在自动化场景中,GUI 已经出来了,所以你的朋友会是secedit.exe
. (我自己,我写了 PowerShell 包装器secedit.exe
。)
因此,假设我们有一个计划任务,该任务将为在您的服务器上运行的应用程序收集遥测数据,然后将该遥测数据发送到您的遥测收集服务,例如 New Relic 或 Data Dog。该任务将在用户帐户下运行CONTOSO\AppTelemetry
。鉴于我们将在蓝/绿部署期间以自动方式通过 PowerShell 创建计划任务,我们需要为该用户分配作为批处理作业登录用户权限。
用于secedit
分配用户权限
这里的步骤非常简单:
- 将现有服务器的安全策略导出到安全策略模板(可选,仅包含您关心的部分)
- 创建新的安全策略模板
- 在新的安全策略模板中将运行计划任务的用户的安全标识符(SID)添加到相应的用户权限
- 将包含安全策略更改的安全策略模板导入新数据库
- 配置系统的安全策略以合并在步骤 4 中创建的安全策略数据库中包含的更改。
让我们看一下实现这一点的代码。
将现有服务器的安全策略导出到安全策略模板
您可以从 CMD 或 PowerShell(桌面版或核心版,没关系)执行此操作。除非另有说明,否则所有示例都将在 PowerShell 中。
secedit.exe /export /cfg secpol.inf /areas USER_RIGHTS
上述命令导出系统的安全策略,但仅导出包含用户权限分配信息的部分。如果您需要添加其他设置,例如注册表项,也可以通过安全策略来完成。一般情况下,请阅读 Microsoft 文档secedit.exe
或安全策略。
创建新的安全策略模板并将我们的计划任务用户的 SID 添加到SeBatchLogonRight
值
现在我们需要创建一个新的安全策略模板,其中包含我们想要的新安全策略与当前安全策略的增量。我建议新的安全策略模板包含尽可能少的信息——只包含那些需要更改的设置。
有人会认为您需要做的就是指定SeBatchLogonRight
必须包含用户的 SID,就是这样。但如果你这么想,那你就错了。默认情况下,SeBatchLogonRight
有一些用户分配给它(请参阅上面引用的 Microsoft 文档链接)。如果我们只是将用户的 SID 分配给新策略模板中的此权限,它将有效地替换系统安全策略中的现有值,而不是更新它。因此,由于我们正在进行附加更改,因此我们需要使用我们上面导出的系统安全策略中的现有值将此用户权限添加到我们的模板中,然后将我们的用户 SID 添加到列表中。
默认情况下,SeBatchLogonRight
为它分配了以下 SID:
SeBatchLogonRight = *S-1-5-32-544,*S-1-5-32-551,*S-1-5-32-559,*S-1-5-32-568
这些是一些标准 Windows 安全组的一些“众所周知的”SID,其用户应该拥有此权限。假设 SIDCONTOSO\AppTelemetry
为S-1-5-21-0000000000-1111111111-2222222222-3333
。但是等等——你是怎么知道的?
function ConvertTo-SecurityIdentifier {
Param (
[Parameter(Position = 0, Mandatory, ValueFromPipeline)]
[string[]] $UsernameOrGroup
)
Process {
foreach ($Name in $UsernameOrGroup) {
$Account = New-Object -Type System.Security.Principal.NTAccount `
-Argument $Name
$Account.Translate([System.Security.Principal.SecurityIdentifier]).Value
}
}
}
Set-Alias -Name ConvertTo-SID -Value ConvertTo-SecurityIdentifier
function ConvertFrom-SecurityIdentifier {
Param (
[Parameter(Position = 0, Mandatory, ValueFromPipeline)]
[string[]] $SecurityIdentifier
)
Process {
foreach ($SID in $SecurityIdentifier) {
$Account = New-Object -Type System.Security.Principal.SecurityIdentifier `
-ArgumentList $SID
$Account.Translate([System.Security.Principal.NTAccount]).Value
}
}
}
Set-Alias -Name ConvertFrom-SID -Value ConvertFrom-SecurityIdentifier
(为什么我不简单地使用Get-ADUser
来获取用户帐户的 SID?有两个原因。首先,使用Get-ADUser
. 从 SID 中获取用户帐户名称并不那么简单。可以做到,但上面的代码更清晰。其次,最重要的是, 并非所有用户都会Get-ADUser
在他们的机器上安装。我以前的笔记本电脑安装了 Windows 远程服务器管理工具 (RSAT), 所以我Get-ADUser
可以使用。没有安装 RSAT 或安装 Active Directory PowerShell 模块,Get-ADUser
不可用. 上面的 cmdlet 不依赖于除 Windows 和 .NET 框架之外的任何东西,如果您尝试获取用户帐户 SID 并使用 PowerShell 来执行此操作,根据定义,您很可能具有两个先决条件。)
然后你可以简单地运行:
$SID = 'CONTOSO\AppTelemtry' | ConvertTo-SecurityIdentifier
现在我们有了 SID,我们可以创建一个安全策略模板(注意,有更好的方法可以做到这一点——我创建了 PowerShell cmdlet,让我以编程方式与这些 INF 文件进行交互,但我只是要使用这里的文档) . 请注意,在您导出的安全策略中,所有 SID 都以 为前缀*
,并且多个值以逗号分隔:
$NewPolicy = @'
[Unicode]
Unicode=yes
[Version]
signature="$CHICAGO$"
Revision=1
[Privilege Rights]
SeBatchLogonRight = *S-1-5-32-544,*S-1-5-32-551,*S-1-5-32-559,*S-1-5-32-568,*S-1-5-21-0000000000-1111111111-2222222222-3333
'@
$NewPolicy | Set-Content batchlogon.inf
将新的安全策略模板导入新的安全策略数据库
标题说明了一切:
secedit.exe /import /db batchlogon.sdb /cfg batchlogon.inf
使用新安全策略数据库中包含的更改配置系统安全策略
标题再次说明了一切:
secedit.exe /configure /db batchlogon.sdb
这就是直接从 CLI为用户分配登录作为批处理作业的全部内容。
通过 PowerShell 创建计划任务
现在用户有权运行计划任务,无论他们是否登录,我们需要创建一个计划任务,无论用户是否登录都将运行。您将看到许多 Q&A 答案的链接,上面写着你需要这个或那个 LogonType(特别是,S4U
或ServiceAccount
)或以最高权限运行它,等等。不要听这些。它(大部分)是错误的。本节将概述创建计划任务所需的最少步骤,无论用户是否登录,该任务都会运行。
创建计划任务操作
首先是创建任务动作。计划任务由几个部分组成:
这些名称是不言自明的。我们将创建一个执行程序的计划任务操作。(还有其他操作类型。请参阅有关任务操作的文档。)
关于 Exec 操作,重要的一点是任务计划程序服务将生成一个实例cmd.exe
来运行提供的程序。这意味着如果您需要为程序指定参数,则需要确保遵循 CMD 命令的困难引用规则。(根据您的论点,这些引用规则可能需要进行大量测试以确保命令按预期运行。即使在我的简单情况下,我也弄错了,并且任务似乎正确运行 -0
退出代码 - 但什么也没做完全没有!)查看cmd.exe /?
所有血腥细节。此外,您可以通过网络搜索找到很多信息。
让我们创建任务操作:
$TaskAction = New-ScheduledTaskAction -Execute 'powershell.exe' `
-Argument ('-NoLogo -ExecutionPolicy Bypass -NoProfile -NonInteractive' + `
'-File "C:\Telemetry\Send-ApplicationTelemetry.ps1"')
同样,请注意引用规则(即,如果您的文件路径中有空格,或者,如果您将一些 powershell 脚本传递给-Command
参数,而不是-File
像我在这里所做的那样使用)。这里有几点需要指出:
- 我本来可以用来
pwsh.exe
运行 PowerShell Core
-NoLogo
避免打印启动 PowerShell 时出现的“横幅”。如果您这样做,它可以使重定向输出到日志文件更好。
-ExecutionPolicy Bypass
指定此脚本将绕过系统上的任何当前执行策略。默认执行策略是Restricted
,除非指定完整路径,否则禁止运行任何脚本,并禁止来自远程源的任何脚本。此开关基本上确保脚本将运行。它可能并不总是必要的,但如果您信任您正在安排的脚本,这也不应该受到伤害。
-NoProfile
用于不加载用户的 PowerShell 配置文件(这与bash
Linux 操作系统上的用户配置文件类似)。但是,也可以有全局配置文件脚本。除非您知道在 PowerShell 启动时需要运行配置文件脚本,否则添加此开关并没有什么坏处,而且很可能会防止错误。
-NonInteractive
是一个非常重要的开关。基本上,这可以防止 powershell 提示用户输入(例如,对于需要确认或用户输入的 cmdlet)。这也意味着如果您的脚本确实需要确认/用户输入,它不会以非交互方式工作(即当用户未登录时)。
-File
告诉 PowerShell 执行指定的文件。您也可以-Command
改用并传入一些“字符串化”的 PowerShell 代码。
创建计划任务主体
这是正确创建计划任务的最重要步骤之一,无论用户是否登录,该任务都将运行。实际上,这一步是配置一个定时任务运行无论用户是否登录都是必需的。
$TaskPrincipal = New-ScheduledTaskPrincipal -Id 'Author' `
-UserId 'CONTOSO\AppTelemetry' `
-LogonType Password `
-RunLevel Limited
让我们更详细地了解这些设置。
-Id
据我所知,这是一个自由格式的文本字段。任务计划程序 GUI 始终使用术语“作者”,但通常,您可以在此处使用任何您想要的东西。需要注意的是,计划任务可以包含许多不同的操作(最多 32 个)。并且 Actions 也有一个与之关联的“上下文”。任务调度程序自动将上下文分配给“作者”(对于简单的单动作任务)。该文档似乎表明您可以为计划任务提供多个主体,并且主体的 Id 与操作的上下文相关,将确定操作将在哪个主体下执行。对于我们的单动作任务,我们将只使用“作者”。
-UserId
或者-GroupId
任务可以在用户帐户或属于指定组的任何用户下运行。请注意,使用组时,只有当组的用户登录时,计划任务才会运行。这是因为使用组 ID,您无法为任务指定密码。
在 99% 的情况下,你会想要-UserId
像我上面那样使用。它不需要是完全合格的用户 ID。请注意,我们不会将密码与主体一起存储。那是后来的。
-LogonType
这可能是这个 cmdlet 最容易被误解的参数之一。LogonType的文档非常好。太糟糕了,PowerShell 帮助New-ScheduledTaskPrincipal
没有链接到它(我对此提出了反馈)。
基本上,您可能永远不需要关心S4U
登录类型。它在某些情况下可能是相关的(这就是它存在的原因),但对于您想要做的大多数事情,它可能不会。
ServiceAccount
例如,如果计划任务将在用户帐户下运行NT AUTHORITY\LOCALSYSTEM
,您将只使用登录类型。NT AUTHORITY\SYSTEM
(还有第三个,但这个名字我现在没有想到。)由于我们的任务不是在系统帐户下运行的,所以这不是我们想要的值。
正如您可能猜到的,Interactive
这意味着任务将在登录用户的上下文中运行。当任务需要启动 GUI 应用程序或任务操作是消息框以在系统上显示对话框时,这可能很有用。显然,这在我们的案例中没有用。还有InteractiveOrPassword
一种将这种登录类型与我们想要的登录类型相结合的类型Password
。所以我不会进一步讨论它。
现在,当然Password
是我们想要的登录类型。这表示密码将与计划任务一起存储,该密码将用于登录用户(作为批处理作业),因此无论用户是否登录,任务都可以运行。是的,这是在任务计划程序 UI中检查用户是否登录时运行复选框的值。
-RunLevel
将此选项视为指定任务是否需要以提升的权限运行。当您运行命令(例如安装某些软件)时,用户访问控制 (UAC) 会启动并显示一个对话框,询问您是否要允许程序对您的计算机进行更改。您可能会点击Yes
一半时间甚至没有阅读它。将此参数设置Highest
为就像单击Yes
UAC 对话框(或只是以提升的权限启动进程,即以管理员身份运行)。这是一件坏事,除非你知道你需要它。你应该总是从Limited
,如果您发现计划任务不会在这些权限下运行,请提升。这就是我所做的。我从最小特权原则和使用Limited
运行级别开始。
创建计划任务触发器
您可以有一个或多个触发器来触发计划任务。有不同的触发器类型。请参阅这些不同类型的文档。我只做一个非常简单的:
$TaskTrigger = New-ScheduledTaskTrigger -Once -At ([DateTime]::Now.AddMinutes(5)) `
-RepetitionInterval ( `
[TimeSpan]::FromMinutes(5) `
)
我只想要一个运行一次的任务,从现在开始 5 分钟,然后每 5 分钟运行一次。
任务设置集
如果我需要自定义其他一些任务设置,我可以使用New-ScheduledTaskSettingsSet
. 我对默认值很好,所以我会跳过这个。但是任务计划程序文档详细说明了这一切,实际上,这些设置在大多数情况下都是不言自明的(除了RunOnlyIfNetworkIsAvailable,但它并不太难理解)。
创建计划任务
现在您可能会认为这是我们创建任务的地方,我们将在任务计划程序中看到它。错误的。但我们将创建一个计划任务对象。您必须执行此步骤,否则您的任务将无法正确注册 - 最重要的是,无论用户是否登录,它都不会设置为Run 。
$Task = New-ScheduledTask -Description 'Collects application telemetry' `
-Action $TaskAction `
-Principal $TaskPrincipal `
-Trigger $TaskTrigger
这里发生的只是我们正在创建一个计划任务实例。但它并没有被添加到任务调度器的任务列表中。这称为注册,我们接下来会这样做。如果你只是$Task
在执行上述命令后输入你的 shell,你会注意到任务名称和路径将是空白的。您甚至不能将其指定为 call 的一部分New-ScheduledTask
。同样,奇怪的是,这发生在注册期间。那么让我们谈谈最重要的部分,注册。
注册计划任务
确实,我可以简单地:
$Task = Register-ScheduledTask -TaskName 'Foo' -TaskPath `\` `
-Action $TaskAction -Trigger $TaskTrigger `
-User 'SomeUser' -Password '$uper53cr37'
但这会导致任务以交互方式运行(仅在用户登录时运行)。这不是我们想要的。这就是我们在上面创建任务主体以及包含任务主体的任务实例的原因。
所以让我们注册我们的任务:
$Task = $Task | Register-ScheduledTask -TaskName 'Collect Telemetry' `
-TaskPath '\Awesome App' `
-User 'CONTOSO\AppTelemetry' `
-Password 'ShhD0ntT3ll4ny0n3!'
如您所见,是的,您必须提供密码。有多种方法可以以自动化的方式安全可靠地执行此操作 - 但可惜,对于演示目的来说并不那么容易。这里重要的部分是提供给Register-ScheduledTask
. 指定的用户必须与我们创建的主体中指定的用户相同。显然,该用户的密码必须正确。如果您使用其他用户注册计划任务,则任务主体将更新为使用注册任务时指定的用户帐户运行任务。(这在此处记录。)
这就是调度任务以运行无论用户是否使用 PowerShell 登录的全部内容。但在我结束之前还有一个话题要讨论:更新计划任务。
更新计划任务
信不信由你,这并不像它应该的那样直截了当。的语法Set-ScheduledTask
没有正确记录。我忘记为上面的任务操作设置工作目录。(假设我需要在某处写入文件,但写入文件的代码不使用绝对路径。)让我们修复操作并更新任务。
这是正确执行此操作的方法。您可以使用不同的方法Set-ScheduledTask
,但我发现其他方法可以锁定用户帐户或根本不起作用(即给您一个错误)。这并不是说那些其他方式是错误的,而只是对于这种特殊的变化,这就是每次都对我有用的方法:
$Task = Get-ScheduledTask -TaskName 'Collect Telemetry' -TaskPath '\Awesome App'
$Task.Actions[0].WorkingDirectory = 'C:\AwesomeApp'
$Task | Set-ScheduledTask -User 'CONTOSO\AppTelemetry' -Password 'ShhD0ntT3ll4ny0n3!'
同样,重要的是使用注册任务时使用的相同用户和密码。如果您使用不同的用户名/密码,那么任务主体也将被更新。这可能不是你想要的。
这就是在 Powershell 中使用计划任务的全部内容。
用于schtasks
调试帮助
当您在通过 PowerShell 创建计划任务时遇到问题时,一种帮助自己的方法是通过 GUI 在本地计算机上创建类似的计划任务。然后,您可以使用该schtasks
命令查询您通过 GUI 创建的计划任务,并将其与您通过 PowerShell 创建的计划任务进行比较。要使用schtasks
,例如:
# To get the XML for the task:
schtasks /query /tn '\My Task Path\MyTask' /xml ONE
# To get a nice formatted list of the task properties:
schtasks /query /tn '\My Task Path\MyTask' /v /fo LIST
您还可以通过 PowerShell 使用Export-ScheduledTask
. 您可以在此处找到所有 PowerShell 计划任务 cmdlet 的文档。文档没问题。其中一些是误导性的,大部分是不完整的(即它假设您除了 PowerShell cmdlet 本身之外还了解任务计划程序)。有关任务计划程序的文档,它的 COM 接口、XML 模式等,可以在这里找到。
结论
我希望这对某人有所帮助,因为我花了很长时间才弄清楚这一切。主要是,很多,“为什么即使它说它成功了,这个任务也没有做任何事情?” 或“为什么帐户一直被锁定?” (错误的登录类型、密码或用户权限分配,或全部 3 个!)