29

我正在开发一个客户端-服务器应用程序,我希望客户端使用用户的登录凭据对服务器进行身份验证,但我不希望用户必须输入他们的用户名和密码。我当然不想负责安全地处理密码。我只需要用户向我证明他们是他们所说的人,然后我的服务器可以继续并根据需要授予/拒绝命令。

我的用户是域的一部分,因此我希望能够使用他们在登录时创建的登录凭据。

我没有使用任何类型的网络服务,也不想使用。我控制客户端和服务器软件,两者都是用纯 C# 编写的,并使用良好的 ol' 套接字来完成工作。

我更喜欢使用纯 C#/.Net 来执行此操作,但如果这意味着我会完成工作,我愿意使用不安全的 C# 和 pinvokes 到 win32 API。

我已经阅读了一些关于 Windows 中的 SSPI 的信息,但我有点摸不着头脑,因为这种应用程序开发对我来说是新的。

有人知道怎么做这个吗?是 SSPI 的方式吗?如何在 C# 中使用 SSPI?有没有一种 .Net-native 方式可以让我的代码保持可移植性?

4

5 回答 5

27

更新:

SSPI 是解决此问题的正确方法。API 使用起来并不难,但确实需要一个相当大的项目来包装到 C# 中。

在研究解决这个问题的必要位的过程中,我编写了一个项目以在 .Net 中提供 SSPI。下面我描述了与 Windows SSPI API 接口的基础知识,以便任何人都可以复制我的结果。如果你发现自己想在 .Net 中使用 SSPI,我建议你使用我创建的项目来解决这个问题:

NSspi - SSPI API 的 .Net 接口

SSPI 为您提供包含身份验证令牌的原始字节数组,然后您决定如何传输 - 无论是通过带有二进制格式消息的套接字、自定义 XML 通道、.Net Remoting、某种形式的 WCF、哎呀,甚至是串行端口。你可以决定如何处理它们。使用 SSPI,服务器可以验证客户端,安全地识别客户端,甚至使用与客户端建立的安全上下文执行基本的消息处理过程,例如加密/签名。

SSPI API 记录在这里:SSPI API 概述

具体看一下以下函数:

典型的工作流程是每一方都将使用 AcquireCredentialsHandle 初始化他们的凭据。然后,身份验证周期开始并按如下方式进行:

  • 客户端调用 InitializeSecurityContext,不提供输入令牌,它以字节数组的形式返回输出令牌。ISC 返回“ContinueNeeded”以指示身份验证周期未完成。
  • 客户端通过它想要的任何方式将令牌发送到服务器。
  • 服务器将接收到的令牌作为输入提供给 AcceptSecurityContext 并生成自己的输出令牌。ASC 还返回“ContinueNeeded”以指示身份验证周期未完成。
  • 然后服务器将其输出令牌发送给客户端。
  • 客户端提供服务器令牌作为 InitializeSecurityContext 的输入,它返回新的输出令牌。
  • 客户端将他的新输出令牌发送到服务器。
  • ...

这个循环一直持续到客户端看到 InitializeSecurityContext 返回“OK”并且服务器看到 AcceptSecurityContext 返回“OK”。每个函数都可以返回“OK”并且仍然提供一个输出标记(如非空返回所示),以表明它仍然必须向另一端发送数据。这就是客户端知道它的一半已经完成但服务器仍然不完整的方式;如果服务器在客户端之前完成,反之亦然。哪一方首先完成(返回“OK”)取决于 SSPI 在后台使用的特定安全包,任何 SSPI 消费者都应该意识到这一点。

上面的信息应该足以让任何人与 SSPI 系统交互,以便在他们的应用程序中提供“Windows 集成身份验证”并复制我的结果。

以下是我在学习如何调用 SSPI API 时的早期回答。


这个问题我忘记了,前几天偶然一时兴起又回到了这个问题上。我确实需要在一两年内解决这个问题:)

这在 .Net 中是可能的,我目前正在开发一个我打算发布的 .Net SSPI 包装器。

我的工作基于我发现的一些来自 Microsoft的SSPI 样本。

该示例包含一个 C++/CLI 托管程序集,该程序集实现了 SSPI API 的必要部分(在Microsoft\Samples\Security\SSPI\SSPI从 REMSSPI.exe 文件中提取的文件夹中)。然后,他们有两个 UI,一个客户端应用程序和一个服务器应用程序,都是用 C# 编写的,它们都使用这个 API 来执行 SSPI 身份验证。

UI 使用 .Net 远程处理工具将它们联系在一起,但如果我正确理解 SSPI API,客户端和服务器需要交换的唯一信息由包含安全上下文令牌数据的字节 [] 组成,它可以轻松集成到您想要的任何通信基础设施中;就我而言,是我自己设计的二进制协议。

关于让示例工作的一些注意事项 - 他们有“SSPI”库源,它最好在 VS 2005 下编译,尽管我已经让它在 2008 下工作;2010 或更高版本需要一些返工,因为它们使用已弃用的语言结构。您可能还需要修改作为平台 SDK 一部分的头文件,因为它们使用 const 指针分配给 unconst 变量,而且我不知道让编译器满意的更好方法(我从未使用过 C++/ CLI 之前)。

它们确实在 Microsoft\Samples\Security\SSPI\bin 文件夹中包含已编译的 SSPI dll。要使客户端/服务器二进制文件正常工作,您必须将该 dll 复制到它们的 bin 目录中,否则将无法解决组装问题。

所以总结一下:

  • 转到此处下载 REMSSPI.exe 示例自解压 zip。
  • 提取 REMSSPI.exe 文件(两次..)
  • Microsoft\Samples\Security\SSPI\
    • bin\- 包含已编译的 dllMicrosoft.Samples.Security.SSPI.dll
    • SSPI\- 包含 dll 的源代码
    • Sample\- 包含 UI 源代码
      • bin\- 包含构建 UI 示例。在此处复制 SSPI.dll 文件并ControlPanel.Client.exe运行ControlPanel.Server.exe
于 2014-06-19T17:32:32.103 回答
1

你可能会问:“服务器如何确认客户是他们所说的人?”

答:如果握手的所有往返都可以成功完成,即两者

  • InitializeSecurityContext返回“确定”
  • AcceptSecurityContext返回“确定”

这意味着客户端的 Windows 登录凭据已被确认为真实。

AcceptSecurityContext输出一个CtxtHandle安全上下文(通过第 6参数)。

此上下文句柄包括客户端的 Windows 登录用户名。服务器可以通过调用QueryContextAttributesEx获取客户端的 windows 用户名:

SecPkgContext_NativeNames pinfo;
QueryContextAttributesEx(&m_securitycontext, SECPKG_ATTR_NATIVE_NAMES, &pinfo);

这会填充一个Native Names结构:

SecPkgContext_NativeNames 
{
   SEC_CHAR *sClientName;
   SEC_CHAR *sServerName;
}

该值pinfo.sClientName是客户端的真实登录用户名。

注意:前面的握手已经保证了安全上下文的真实性,所以服务器会认为pinfo.sClientName只是客户端的真实 Windows 用户名。

于 2019-02-28T02:05:07.593 回答
0

我在使用 WindowsIdentity 时完全出错(这对授权很有用),因为我忘记了 WCF 处理了很多与配置文件、端点安全和消息/传输安全有关的事情。

你试过NegotiateStream吗?给定的示例似乎更符合您的需求:它在允许任何读/写之前使用 Kerberos 进行身份验证。使用CredentialCache.DefaultNetworkCredentials应该避免您查询密码。

于 2014-06-25T21:57:39.577 回答
0

尼古拉是正确的;没有 .NET 原生方式来完成您正在做的事情(至少,不使用低级 .NET 套接字支持)。你当然可以潜入幕后做一些互操作的黑魔法,但如果客户端和服务器都在你的控制之下,你可能需要考虑将堆栈向上移动一点并使用更高级别的 API,例如 WCF,它确实具有对 Windows 集成身份验证的 .NET 本机支持。

根据您的问题和您描述的环境,您将能够使用 NetTcpBinding,它提供高性能以及您正在寻找的身份验证/身份流的管道(它还提供了一种相当干净的方式来处理授权使用 ServiceAuthorizationManager 类)。在不知道您的应用程序/服务的细节的情况下,我不可能提供“如何做”来实现您想要做的事情,但我可以向您指出具有相当简单示例的文档。

于 2014-06-19T12:24:07.873 回答
-1

曾经尝试过使用 WindowsIdentity 吗?
纯 C#/.Net,可序列化和 GetCurrent() 返回执行帐户。


安全令牌
用户连同她的请求一起向您的应用程序提供一组声明。在 Web 服务中,这些声明是在 SOAP 信封的安全标头中携带的。在基于浏览器的 Web 应用程序中,声明通过来自用户浏览器的 HTTP POST 到达,如果需要会话,以后可能会缓存在 cookie 中。无论它们如何到达,它们都必须以某种方式进行序列化,这就是安全令牌的用武之地。安全令牌是一组序列化的声明,由发行机构进行数字签名。签名很重要——它可以让您确保用户不会只是编造一堆声明并将它们发送给您。在不需要或不需要加密的低安全性情况下,您可以使用未签名的令牌,但这不是我将在本文中关注的场景。WIF 的核心功能之一是创建和读取安全令牌的能力。WIF 和 .NET Framework 中的底层管道处理所有繁重的加密工作,并为您的应用程序提供一组您可以阅读的声明。

引用自Windows 身份基金会白皮书

您的主要问题是:

“是否有一种纯 C#/.NET 方式来使用他们的登录凭据对用户进行身份验证?”

WindowsIdentity “是”您的域控制器颁发的身份验证令牌,在我看来,这是目前最好的方法。


当我第一次发布时,我对 WindowsIdentity 知之甚少,但我也觉得它有助于解决您的问题和限制。我做了很多阅读,终于来到了这个页面
WIF 的引入是不言自明的,WindowsIdentity 是一组新的 .NET 框架,专为基于 Windows/基于角色的安全问题而设计。
SSPI、Kerberos 是整个 Windows 身份验证过程的一部分,用户/机器/进程获得的登录令牌是由域控制器授予的,不能通过“简单”实例化一个新的 WindowsIdentity 对象来获得。如果存在这种非法实例化,整个 Windows 安全模型(域、UAC 等)就会死掉。

这是一个(非常!)小控制台程序,如果您不属于“BUILTIN\Administrateurs”(根据您自己的需要更改组名),则会引发异常。每当“以管理员身份运行”时,程序完成时不会出现错误。
有非常多的权限集,每个需求都是基于声明的(是 xxx 的身份成员吗?)

using System;
using System.Security;
using System.Security.Permissions;
using System.Security.Principal;

namespace WindowsIdentityTest
{
    class Program
    {
        [PrincipalPermission(SecurityAction.Demand, Authenticated = true)]
        static string SomeServerAction()
        { return "Authenticated users can access"; }

        [PrincipalPermission(SecurityAction.Demand, Role = "BUILTIN\\Administrateurs")]
        static string SomeCriticalServerAction()
        { return "Only Admins can access"; }

        static void Main(string[] args)
        {
            //This allows to perform security checks against the current Identity.   
            AppDomain.CurrentDomain.SetPrincipalPolicy(PrincipalPolicy.WindowsPrincipal);

            try
            {
                Console.WriteLine(SomeServerAction());
                Console.WriteLine(SomeCriticalServerAction());

            }
            catch (SecurityException sec)
            {
                Console.WriteLine(string.Format("{0} : {1}\n------------\n{2}"
                    , sec.GetType()
                    , sec.Message
                    , sec.StackTrace));
            }
            catch (Exception ex)
            {
                Console.WriteLine("This shall not appen.");
            }
            Console.WriteLine("Press enter to quit.");
            Console.ReadLine();
        }
    }
}

我希望这将有所帮助。

于 2014-06-19T21:20:25.087 回答