客户端-服务器身份验证 - 使用SSPI?

30
我正在开发一个客户端-服务器应用程序,我希望客户端可以使用用户登录凭据对服务器进行身份验证,但是我不想让用户输入用户名和密码。我当然不想负责安全地处理密码。我只需要用户向我证明他们是自己所说的人,然后我的服务器就可以自由地授予/拒绝命令。
我的用户是域的一部分,因此我希望能够使用他们在登录时创建的登录凭据。
我没有使用任何类型的 Web 服务,也不想这样做。我控制着客户端和服务器软件,两者都是用纯 C# 编写,并使用好老的套接字来完成工作。
我更喜欢使用纯 C#/.Net 来实现,但如果这意味着完成任务,我也愿意使用不安全的 C# 和 pinvoke 到 win32 API。
我已经了解了一些有关 Windows 的 SSPI,但是由于这种应用程序开发对我来说是新的,所以我感到有些摸不着头脑。
有谁知道如何做到这一点吗?SSPI 是正确的方法吗?如何在 C# 中使用 SSPI?是否有 .Net 原生方式,使我的代码可以保持可移植性?

你看过使用Windows身份验证吗?这种类型的身份验证将使用用户的域用户名来验证对服务器的访问权限。连接字符串应该类似于这样:Server=myServerAddress;Database=myDataBase;Trusted_Connection=True; - Mo Patel
我认为没有.NET本地方法可以做到这一点。然而,微软提供了一个示例来演示如何使用SSPI。它涉及创建一个处理SSPI并提供与.NET接口的托管C++。坦率地说,我无法在Windows 8.1上运行它,也没有调试它,但这可能是一个很好的阅读材料。http://msdn.microsoft.com/en-us/library/ms973911.aspx当您在.NET中有更简单的解决方案时,我建议您重新考虑使用普通套接字的决定。 - Nikola Radosavljević
2
澄清一下:由于WCF可以使用普通套接字而不是IIS/ASP.NET来完成,所以我也应该能够这样做。WCF源代码(通过参考源代码许可证可用)很混乱,难以跟踪。 - jgauffin
5个回答

29

更新:

对于这个问题,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"并仍然提供一个输出令牌(如非null返回所示),以表示它仍需向另一方发送数据。这就是客户端知道自己的一半已经完成但服务器的尚未完成的方法;如果服务器在客户端之前完成,则反之亦然。哪一方先完成(返回"OK")取决于SSPI底层使用的特定安全包,任何SSPI消费者都应该注意这一点。

以上信息应该足以让任何人开始与SSPI系统进行接口,以在其应用程序中提供"Windows集成身份验证"并复制我的结果。

下面是我早期学习如何调用SSPI API时的答案。


我忘记了这个问题,巧合的是几天前我又回到了这个问题上 :)

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

我在微软的SSPI示例中找到了一些工作的基础。

这个样例包含一个C++/CLI托管程序集,实现了SSPI API的必要部分(从“Microsoft\Samples\Security\SSPI\SSPI”文件夹中提取REMSSPI.exe文件)。然后有两个UI,一个客户端应用程序和一个服务器应用程序,两者都使用此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示例自解压缩文件。
  • 提取REMSSPI.exe文件(两次...)
  • Microsoft\Samples\Security\SSPI\
    • bin\ - 包含已编译的dll Microsoft.Samples.Security.SSPI.dll
    • SSPI\ - 包含dll源代码
    • Sample\ - 包含UI源代码
      • bin\ - 包含构建UI样例的文件。将SSPI.dll文件复制到此处,然后运行ControlPanel.Client.exeControlPanel.Server.exe

1
那个 DLL 是一个托管的 .Net DLL,只是用 C++/CLI 编写的。我正在用 C# 写自己的版本,出于我的个人原因,但我同样可以轻松使用那个版本。 - antiduh
1
啊,好的。很好,请发布吧 :) - jgauffin
1
我觉得我们有些混淆了。Microsoft.Samples.Security.SSPI.dll 是一个托管的 .Net DLL,用 C++/CLI 编写而成。你可以从示例的下载中获取二进制文件。我正在用 C# 写自己的版本的 DLL,打算发布出去,但你可能只需要直接使用 Microsoft.Samples.Security.SSPI.dll。 - antiduh
你觉得把它放在nuget.org上怎么样? - CodeFox
@antiduh,感谢您的精彩帖子。但是,我有一个问题。在服务器端是否需要使用SSPI机制来完成整个通信过程的排序?当我看到SSPI的样例文档时,我感觉服务器上需要运行一个进程?我想将此应用于本地代理服务器,并完成身份验证过程。但我怀疑我是否可以在那里运行一个进程!通常,服务器现在支持基本身份验证,并且控制台客户端可以像普通浏览器使用基本身份验证机制一样得到认证。 - kuldeep
显示剩余14条评论

1
您可能会问:“服务器如何确认客户端的身份?” 答案:如果握手的所有往返都能够成功完成,也就是说
  • InitializeSecurityContext 返回“OK”
  • AcceptSecurityContext 返回“OK”
这意味着已经确认了客户端的 Windows 登录凭据是真实的。 AcceptSecurityContext 通过第六个参数输出 CtxtHandle 安全上下文。
该上下文包括客户端的 Windows 登录用户名。服务器可以通过调用 QueryContextAttributesEx 获取客户端的 Windows 用户名。
SecPkgContext_NativeNames pinfo;
QueryContextAttributesEx(&m_securitycontext, SECPKG_ATTR_NATIVE_NAMES, &pinfo);

这将填充一个本地名称结构:
SecPkgContext_NativeNames 
{
   SEC_CHAR *sClientName;
   SEC_CHAR *sServerName;
}

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

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


0

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

你尝试过NegotiateStream吗?给出的示例似乎更适合您的需求:它使用Kerberos进行身份验证,然后才允许任何读/写操作。使用CredentialCache.DefaultNetworkCredentials应该避免查询密码。


0
Nikola是正确的,没有一种.NET本地方法可以完成你正在做的事情(至少不使用低级别的.NET套接字支持)。你可以深入了解内部情况来进行一些互操作黑科技,但如果客户端和服务器都在你的控制下,你可能需要考虑向上移动一点堆栈,并使用高级API,如WCF,它具有对Windows集成身份验证的.NET本地支持。
根据你的问题和所描述的环境,你可以使用NetTcpBinding,它提供高性能以及你所需的身份验证/身份流程的管道(它还提供了一个相当干净的方法来使用ServiceAuthorizationManager类处理授权)。我无法提供“如何”来实现你要做的事情,因为我不知道你的应用程序/服务的具体细节,但我可以 指向文档,其中有一个相当简单的例子。

我想使用.NET方式(用于自己的客户端/服务器)。WCF似乎可以使用它的netTcpBinding来做到这一点,但是代码很混乱,难以跟踪。 - jgauffin
当你说“一种.NET方式”时,我不确定你的意思 - WCF是.NET的一个特性。我确实理解WCF的学习曲线可能会让人望而生畏,但从我的经验来看,学习如何使用现有的框架比尝试实现自己的框架更好(就开发时间和维护开销而言)。 - Andy Hopper
问题非常具体。我不想使用WCF。我想能够在自己的客户端/服务器中使用Windows身份验证。WCF可以在其netTcpBinding中执行此操作,因此这是可能的。如果可能,我想知道如何在.NET中使用而不需要p / invokes。这是为了我的客户端/服务器库http://blog.gauffin.org/2014/05/griffin-framework-performant-networking-in-net/。我再次声明:任何建议使用WCF的答案都不适用。 - jgauffin
我明白了。祝你好运! - Andy Hopper

-1

你有没有尝试过使用WindowsIdentity?
纯C#/.Net,可序列化且GetCurrent()返回执行账户。


安全令牌 用户通过请求携带一组声明传递给您的应用程序。在 Web 服务中,这些声明包含在 SOAP 信封的安全头中。在基于浏览器的 Web 应用程序中,这些声明通过用户的浏览器通过 HTTP POST 到达,并且如果需要会在 cookie 中缓存。无论它们如何到达,它们都必须以某种方式进行序列化,这就是安全令牌的作用。安全令牌是一组已序列化的声明,由发行机构数字签名。签名很重要-它确保用户没有编造一堆声明并将其发送给您。在低安全性情况下,不需要或不希望使用加密技术,可以使用未签名的令牌,但这不是我在本文中关注的场景。WIF 的核心功能之一是创建和读取安全令牌。WIF 和 .NET Framework 中的基础架构处理所有加密操作,并向您的应用程序提供一组可读取的声明。

引用自Windows Identity Foudation WhitePaper

你的主要问题是:

“是否有一种纯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();
        }
    }
}

希望这能有所帮助。


WindowsIdentity(GetCurrent())对象是用户登录时获取的标识令牌。在服务器端使用模拟将允许打开具有Integrated_Security设置为true的SQL Server连接(如果授予域用户权限),即使服务器工作进程帐户无法执行此操作。对于其他类型的资源(I / O权限...),它应该仍然有效。 - xum59
我认为你不需要任何服务器端身份验证 - 这与安全的服务器开发实践背道而驰。 - antiduh
2
Kerberos - 设计一个认证系统:四幕对话 - http://web.mit.edu/kerberos/dialogue.html - antiduh
1
SSPI - 请阅读此处的介绍和身份验证部分:http://msdn.microsoft.com/zh-cn/library/ms973911.aspx - antiduh
好的,那么如何安全地将WindowsIdentity传输到服务器?您的代码没有显示出来,而我所知道的唯一安全的方法是使用SSPI。 - antiduh
显示剩余14条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接