65.9K
CodeProject 正在变化。 阅读更多。
Home

在Visual Studio 2017中创建WCF DataService

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.89/5 (8投票s)

2016 年 3 月 25 日

CPOL

13分钟阅读

viewsIcon

72905

downloadIcon

2273

发布一个具有实体模型、可更新视图、过滤和身份验证的 WCF DataService

 

引言

市面上有很多关于创建 Web 服务的文章,但根据我的经验,其中一些信息有点过时(使用了较旧的工具/框架),而且我还需要从许多来源拼凑信息来绕过 bug 或实现所有功能。本文旨在提供一份权威指南,介绍如何在 WCF 中使用 Entity Framework 6 和 WCF Data Services 5.6 来创建 WCF Data Service。在下一篇文章中,我将介绍如何在通用 Windows 平台应用程序中消耗服务中的数据。

WCF Data Services(以前称为“ADO.NET Data Services”)是 .NET Framework 的一个组件,它允许您创建使用 Open Data Protocol (OData) 的服务,通过 Web 或内网使用 表述性状态转移 (REST) 的语义来公开和消耗数据。OData 将数据公开为可以通过 URI 寻址的资源。数据使用标准的 HTTP 动词 GETPUTPOSTDELETE 进行访问和修改。OData 使用 实体数据模型 的实体-关系约定,将资源公开为由关联连接的实体集。

WCF Data Services 使用 OData 协议进行资源寻址和更新。这样,您就可以从任何支持 OData 的客户端访问这些服务。OData 允许您使用众所周知的传输格式请求和写入资源:Atom,这是一组用于以 XML 格式交换和更新数据的标准;以及 JavaScript Object Notation (JSON),一种广泛用于 AJAX 应用程序的基于文本的数据交换格式。

WCF Data Services 可以将来自各种源的数据公开为 OData 源。Visual Studio 工具使您能够更轻松地通过 ADO.NET Entity Framework 数据模型创建基于 OData 的服务。您还可以基于公共语言运行时 (CLR) 类,甚至基于延迟绑定的或非类型化数据来创建 OData 源。

WCF Data Services 还包含一组客户端库,一个用于通用的 .NET Framework 客户端应用程序,另一个专门用于基于 Silverlight 的应用程序。当您从 .NET Framework 和 Silverlight 等环境访问 OData 源时,这些客户端库提供了面向对象的编程模型。

背景

关于 odata 和 WCF 的一些优秀文章有

关于 OData 的一些原则

一些关于身份验证的好的信息

关于过滤数据(查询拦截)的信息

关于错误的解决方法

关于具有多个结果集的存储过程实体的信息

创建服务

  1. 转到 **文件 -> 新建项目**。
  2. 在已安装模板列表中,选择 **Visual C# | WCF** 树节点,然后选择 **WCF 服务应用程序**。

  3. 从生成的项目中删除 _IService1.cs_ 和 _Service1.svc_。

  4. 向项目中添加一个新项。在已安装模板列表中,选择 **Visual C# | Data** 树节点,然后选择 **ADO.NET 实体数据模型**。

  5. 出于本文的目的,我选择从现有数据库构建我的实体模块。

  6. 定义您的连接。通常是云服务器。

  7. 将您的连接保存到 _web.config_ 文件。

  8. 指定实体版本 6.0。

  9. 选择要包含在实体模型中的表/视图/存储过程。

  10. 将随附的数据服务模板 WcfDataServiceItemTemplate.zip 解压到您的 Visual Studio C# 项模板文件夹,该文件夹通常是 "{Documents}\Visual Studio 2017\Templates\ItemTemplates\Visual C#"(其中 {Documents} 是您的用户配置文件文档文件夹)。请注意,解压后的层次结构应为单层,模板文件位于例如 ...\ItemTemplates\Visual C#\WcfDataServiceItemTemplate\WebDataService.vstemplate。现在,在 Visual Studio 中,右键单击您的项目并选择添加新项。搜索已安装模板中的 WCF Data Service,并将新的 WCF Data Service 5.8.3 项添加到您的项目中。如果模板未显示,请重启 Visual Studio。

  11. 编辑 _WcfDataService1.svc_ 文件,将 <TODOReplaceWithYourEntitySetName> 替换为您的实体模型的名称,在本例中是 futaTillHOEntities

    可选

    • 设置实体名称的访问规则。使用星号 (*) 表示所有实体。
    • 设置 UseVerboseErrors 属性,以便看到正确的错误反馈。
    using System.Data.Services.Providers;
    
    namespace WcfService1
    {
        public class WcfDataService1 : EntityFrameworkDataService<FutaTillHOEntities>
        {
            // This method is called only once to initialize service-wide policies.
            public static void InitializeService(DataServiceConfiguration config)
            {
                // TODO: set rules to indicate which entity sets and service operations are visible, updatable, etc.
                // Examples:
                config.SetEntitySetAccessRule("*", EntitySetRights.AllRead);
                // config.SetServiceOperationAccessRule("MyServiceOperation", ServiceOperationRights.All);
                config.DataServiceBehavior.MaxProtocolVersion = DataServiceProtocolVersion.V3;
    	    config.UseVerboseErrors = true;
            }
        }
    }
  12. 从 Visual Studio 服务器资源管理器或您的 Azure 门户创建新的 Web 应用程序。然后右键单击您的项目并单击“发布”。在发布屏幕中,选择 Azure Web Apps。

  13. 选择您要发布的 Web 应用。在本例中,名称是 UtilitiesDataService

  14. 如果设置正确,请单击“下一步”。

  15. 如果设置正确,请单击“下一步”。

  16. 单击“发布”。

  17. 现在您应该能够使用您的 Web 应用的 URL(可在您的 Azure 门户中找到)+ 服务名称 + 实体名称来测试您的 Web 服务。

    示例

通过在 URL 中包含主键来检索特定记录。

如果键是复合键,则使用以下表示法:

默认情况下,信息将序列化为 Atom 源,但您可以包含格式说明符以 JSON 格式获取数据。

您还可以包含 top/skip/expand 等函数,并使用 & 字符将它们组合起来。

有关更多示例,请参阅 http://www.odata.org/

使视图可更新

默认情况下,entityset 的设置方式是为了防止对视图进行更新。如果您的视图可更新,请执行以下步骤:

  1. 右键单击 model 元素并选择“打开方式”。

  2. 选择 XML 编辑器,然后单击“确定”。

  3. 查找文本 <DefiningQuery>。您会注意到此元素及其内部查询存在于您的视图中,但不在表中使用。

  4. 删除 DefiningQuery 元素。在此屏幕中,我已显示从 Behaviours 视图中删除的元素,但您需要从所有视图中删除它。

  5. 奇怪的是,您还需要将文本 store:Schema="dbo" 更改为仅 Schema="dbo"。基本上,如果您查看表的定义方式,可以看到区别。
    [表定义]
    <EntitySet Name="Tenders" EntityType="Self.Tenders" 
    	Schema="dbo" store:Type="Tables" />
    [原始视图定义]
    <EntitySet Name="Behaviours" EntityType="Self.Behaviours" 
    store:Type="Views" store:Schema="dbo"/>
    [已更正的视图定义]
    <EntitySet Name="Behaviours" 
    	EntityType="Self.Behaviours" store:Type="Views" Schema="dbo"/>
  6. 编辑并保存 XML 后,有时需要返回实体模型设计器并单击工具栏中的保存按钮。

基本身份验证

为了设置基本身份验证,您的服务中需要另外两个类。我已将这些类的完整源代码附加到文章中。

BasicAuthenticationModule 类设置了身份验证请求的事件处理,并将其转发到 BasicAuthenticationProvider.Authenticate 方法。

public class BasicAuthenticationModule : IHttpModule
    {
        public void Init(HttpApplication context)
        {
            //Attach handling for authentication requests.
            context.AuthenticateRequest
               += new EventHandler(context_AuthenticateRequest);
        }
        void context_AuthenticateRequest(object sender, EventArgs e)
        {
            //Unbox the application.
            HttpApplication application = (HttpApplication)sender;

            //Send to provider for authentication.
            if (!BasicAuthenticationProvider.Authenticate(application.Context))
            {
                application.Context.Response.Status = "401 Unauthorized";
                application.Context.Response.StatusCode = 401;
                application.Context.Response.AddHeader("WWW-Authenticate", "Basic");
                application.CompleteRequest();
            }
        }
        public void Dispose() { }
    }

这是 BasicAuthenticationProvider 类。请注意,基本身份验证本身不安全,因为用户名和密码是以未加密的方式发送的。因此,基本身份验证只应在 SSL 环境中允许。代码中已注释掉(在下面的代码示例中)强制执行此条件的密码,以便进行测试。

    public class BasicAuthenticationProvider
    {
        /// <summary>
        /// Authenticate and the set the current http context user.
        /// </summary>
        /// <param name="context"></param>
        /// <returns></returns>
        public static bool Authenticate(HttpContext context)
        {
            //This needs to be uncommented for live site.
            //This will reject the login when not using SSL.
            //if (!HttpContext.Current.Request.IsSecureConnection)
            //    return false; 
            //I only want to execute code for authorization requests.
            if (!HttpContext.Current.Request.Headers.AllKeys.Contains("Authorization"))
                return false;

            string authHeader = HttpContext.Current.Request.Headers["Authorization"];

            IPrincipal principal;
            if (TryGetPrincipal(authHeader, out principal))
            {
                HttpContext.Current.User = principal;
                return true;
            }
            return false;
        }

        /// <summary>
        ///
        /// </summary>
        /// <param name="authHeader"></param>
        /// <param name="principal"></param>
        /// <returns></returns>
        private static bool TryGetPrincipal(string authHeader, out IPrincipal principal)
        {
            var creds = ParseAuthHeader(authHeader);
            if (creds != null && TryGetPrincipal(creds, out principal))
                return true;

            principal = null;
            return false;
        }

在基本身份验证中,用户名和密码存储在标头中。此方法将用户名和密码凭据提取到数组中。

/// <summary>
        /// In basic authentication the user name and password are in the auth header in base64 encoding.
        /// </summary>
        /// <param name="authHeader"></param>
        /// <returns>The array of credentials i.e. the username and password.</returns>
        private static string[] ParseAuthHeader(string authHeader)
        {
            // Check this is a Basic Auth header
            if (
                authHeader == null ||
                authHeader.Length == 0 ||
                !authHeader.StartsWith("Basic")
            ) return null;

            // Pull out the Credentials with are separated by ':' and Base64 encoded
            string base64Credentials = authHeader.Substring(6);
            string[] credentials = Encoding.ASCII.GetString(
                  Convert.FromBase64String(base64Credentials)
            ).Split(new char[] { ':' }); 
            
            if (credentials.Length != 2 ||
                string.IsNullOrEmpty(credentials[0]) ||
                string.IsNullOrEmpty(credentials[0])
            ) return null;

            // Okay this is the credentials
            return credentials;
        }
}

您必须在此重载的 TryGetPrincipal 中连接您的用户名/密码数据存储。在下面的代码示例中,我使用了另一个实体模型,其中有一个名为 aspnet_GetUserCredentials 的存储过程,用于返回包含所提供用户名的登录信息的多个结果集。第一个结果集返回用户名和密码详细信息,第二个包含用户的角色,第三个包含角色权限。将提供的密码(来自身份验证标头)进行哈希处理,并与存储的哈希值进行比较。如果匹配,则将 principal 对象初始化为用于用户凭据、角色及其关联权限的新容器。

        /// <summary>
        ///
        /// </summary>
        /// <param name="creds"></param>
        /// <param name="principal"></param>
        /// <returns></returns>
        private static bool TryGetPrincipal(string[] creds, out IPrincipal principal)
        {
            bool located = false;
            principal = null;

            //The user match.
            var user = new User_SprocResult();

            //The list of roles.
            var roles = new List<Role_SprocResult>();

            //The list of permissions.
            var permissions = new List<Permission_SprocResult>();    

            //Use the entity context.
            using (var dbContext = new AuthenticationEntities())
            {
                //Get first enumerate result set.
                var result = dbContext.aspnet_GetUserCredentials("Utilities", creds[0]);
                user = result.FirstOrDefault();

                //Get second result set
                var result2 = result.GetNextResult<Role_SprocResult>();
                roles.AddRange(result2);

                //Get third result set
                permissions.AddRange(result2.GetNextResult<Permission_SprocResult>());
            }

            //If there are any user matches.
            if (user != null)
            {
                //Get the hash of this users password using the salt provided.
                byte[] bytes = Encoding.Unicode.GetBytes(creds[1]);
                byte[] src = Convert.FromBase64String(user.PasswordSalt);
                byte[] dst = new byte[src.Length + bytes.Length];
                Buffer.BlockCopy(src, 0, dst, 0, src.Length);
                Buffer.BlockCopy(bytes, 0, dst, src.Length, bytes.Length);
                HashAlgorithm algorithm = HashAlgorithm.Create("SHA1");
                byte[] inArray = algorithm.ComputeHash(dst);

                //If the resulting hash is equal to the stored hash for this user.
                if (string.Compare(Convert.ToBase64String(inArray), user.Password) == 0)
                {
                    //Tag as located.
                    located = true;

                    //Set new principal.
                    principal = new CustomPrincipal(user.UserName,
                        roles.Select(r=>r.RoleName).ToArray(),
                        permissions.Select(r=>r.PermissionId).ToArray());
                }
            }

            //Return result.
            return located;
        }

这是用户 principal 的专用容器。

 public class CustomPrincipal : IPrincipal
        {
            string[] _roles;
            string[] _permissions;
            IIdentity _identity;

            public CustomPrincipal(string name, string[] roles, string[] permissions)
            {
                this._roles = roles;
                this._permissions = permissions;
                this._identity = new GenericIdentity(name);
            }

            public IIdentity Identity
            {
                get { return _identity; }
            }

            public bool IsInRole(string role)
            {
                return _roles.Contains(role);
            }

            public bool HasPermission(string permission)
            {
                return _permissions.Contains(permission);
            }
        }

为了让您的 Web 服务真正实现身份验证模块,您必须将其添加到 _web.config_ 的 modules 节点中。

  <modules runAllManagedModulesForAllRequests="true">
      <add name="BasicAuthentication" type="UtilitiesWcfService.BasicAuthenticationModule" />
      <remove name="ApplicationInsightsWebTracking" />
      <add name="ApplicationInsightsWebTracking" 
      type="Microsoft.ApplicationInsights.Web.ApplicationInsightsHttpModule, 
      Microsoft.AI.Web" preCondition="managedHandler" />
    </modules>

Azure AD OAuth 2.0 身份验证

首先,如果还没有,您需要为租户设置一些用户。在您的 Azure 门户中搜索“用户”刀片。

单击“添加”按钮并添加一个新用户。

接下来,您需要注册您的数据服务到您的租户中。在您的 Azure 门户中搜索“应用注册”。

然后单击“添加”按钮并创建一个新的 Web API 注册。

记下登录 URL 和生成的应用程序 ID GUID。您将在数据服务中使用它们。您还需要登录 URL 来为任何从 WCF 数据服务消耗数据的应用程序。

回到数据服务,安装以下 NuGet 包:

PM> Install-Package System.IdentityModel.Protcols.OpenIdConnect

然后添加一个新OAuthProtectionModule此类用于与BasicAuthenticationModule 相同目的,即附加 AuthenicateRequest 事件。在事件处理程序内部,调用 OAuthAuthenticationProvider 类进行身份验证,或者在适用时返回错误响应。

 /// <summary>
    /// This class is an IHttpModule is used to check the access token on every incoming request to the site.
    /// </summary>
    public class OAuthProtectionModule : IHttpModule
    {
        /// <summary>
        /// This method is used to do all the initialization for this class.
        /// </summary>
        /// <param name="context">The <see cref="HttpApplication"/> object which contains this module.</param>
        public void Init(HttpApplication context)
        {
            context.AuthenticateRequest += OnAuthenticateRequest;
        }

        /// <summary>
        /// Handle the HTTP pipeline AuthenticateRequest event, after ensuring that the module has been initialized.
        /// </summary>      
        /// <param name="sender">Sender of this event.</param>
        /// <param name="args">Event arguments.</param>
        void OnAuthenticateRequest(object sender, EventArgs args)
        {
            //Unbox the application.
            HttpApplication application = (HttpApplication)sender;

            //Send to provider for authentication.
            if (!OAuthAuthenticationProvider.Authenticate(
                out int statusCode,
                out string httpStatus,
                out string wwwAuthenticateResponse))
            {
                //Set the status and status code.
                application.Context.Response.Status = httpStatus;
                application.Context.Response.StatusCode = statusCode;

                //If there is a WWW-Authenticate payload.
                if (!String.IsNullOrEmpty(wwwAuthenticateResponse))
                {
                    //Add the Authenticate header.
                    application.Context.Response.AddHeader("WWW-Authenticate", wwwAuthenticateResponse);
                }
                application.CompleteRequest();
            }
        }

        public void Dispose() { }
    }

这是 OAuthAuthenticationProvider 类的完整定义。它与BasicAuthenticationProvider 共享一些相似之处,例如它有一个 Authenticate 方法,该方法将授权标头传递给 TryGetPrinciple 方法,并将返回的 principal(如果有)分配给用户上下文。 请注意,大部分代码来自以下 Microsoft 示例:https://azure.microsoft.com/en-us/resources/samples/active-directory-dotnet-webapi-manual-jwt-validation/

使用 OAuth 时,与基本身份验证不同,我们不查找授权标头中的用户凭据。OAuth2.0 的授权标头应为“bearer”一词,后跟一个空格,再后跟 base64url 编码的访问令牌。在身份验证方法中,我使用简单的子字符串从授权标头中剥离“bearer”一词,然后再将其传递给 TryGetPrinciple 方法。

请注意,您需要填写您的租户 ID(例如 mytenent.onmicrosoft.com)、您的数据服务应用程序 ID GUID 和您的数据服务登录 URL。

public static class OAuthAuthenticationProvider
    {
        #region Fields

        /// <summary>
        /// The name of the azure AD tenant.
        /// </summary>
        static string tenant = "%YourTenantName%";

        /// <summary>
        /// The signin authority.
        /// </summary>
        static string authority = $"https://login.microsoftonline.com/{tenant}";

        /// <summary>
        /// OpenIdConnect configuration
        /// </summary>
        static ConfigurationManager<OpenIdConnectConfiguration> configurationManager =
            new ConfigurationManager<OpenIdConnectConfiguration>($"{authority}/.well-known/openid-configuration",
                new OpenIdConnectConfigurationRetriever());

        /// <summary>
        /// The application id of the web app and the app id url.
        /// </summary>
        static string[] audiences = { "%DataService Application ID%", "%DataService Sign-On URL%" };

        /// <summary>
        ///
        /// </summary>
        static string scopeClaimType = "http://schemas.microsoft.com/identity/claims/scope";

        /// <summary>
        /// The standard www-authenticate response header.
        /// </summary>
        static string wwwAuthenticate = $"Bearer realm=\"{audiences[1]}\", authorization_uri=\"{authority}\", resource_id=\"{audiences[1]}\"";

        #endregion

        /// <summary>
        /// Authenticate and the set the current http context user.
        /// </summary>
        /// <param name="context">The http context.</param>
        /// <returns>True if the user has been authenticated.</returns>
        public static bool Authenticate(out int statusCode, out string httpStatus, out string wwwAuthenticateResponse)
        {
            //Set inital result.
            bool result = false;

            //If no authorization was provided.
            if (!HttpContext.Current.Request.Headers.AllKeys.Contains("Authorization"))
            {
                httpStatus = "401 Unauthorized";
                statusCode = (int)HttpStatusCode.Unauthorized;
                wwwAuthenticateResponse = wwwAuthenticate;
            }
            //Try to resolve the claims principle.
            else if (TryGetPrincipal(HttpContext.Current.Request.Headers["Authorization"].Substring(7),
                out IPrincipal principal,
                out statusCode,
                out httpStatus,
                out wwwAuthenticateResponse))
            {
                //Set the claims principle.
                HttpContext.Current.User = principal;
                Thread.CurrentPrincipal = principal;
                result = true;
            }

            //Return the result.
            return result;
        }

        /// <summary>
        /// This method parses the incoming token and validates it.
        /// </summary>
        /// <param name="accessToken">The incoming access token.</param>
        /// <param name="error">This out paramter is set if any error occurs.</param>
        /// <returns>True on success, False on error.</returns>
        static bool TryGetPrincipal(string accessToken, out IPrincipal principal, out int statusCode, out string httpStatus, out string wwwAuthenticateResponse)
        {
            bool overallResult = false;
            principal = null;
            statusCode = 0;
            httpStatus = null;
            wwwAuthenticateResponse = null;

#if DEBUG
            //Show token in execeptions.
            IdentityModelEventSource.ShowPII = true;
#endif
            try
            {
                //Retrieve the configuration for the tennent.
                OpenIdConnectConfiguration config = GetConfigurationNonAsync();

                // validate the token
                var claimsPrincipal = new JwtSecurityTokenHandler().ValidateToken(accessToken,
                      new TokenValidationParameters
                      {
                          ValidateAudiences = audiences,
                          ValidIssuer = config.Issuer,
                          IssuerSigningKeys = config.SigningKeys
                      }
                      , out SecurityToken validatedToken); ;

                // If the token is scoped, verify that required permission is set in the scope claim.
                if (claimsPrincipal.FindFirst(scopeClaimType) != null &&
                    claimsPrincipal.FindFirst(scopeClaimType).Value != "user_impersonation")
                {
                    statusCode = (int)HttpStatusCode.Forbidden;
                    httpStatus = "403 Forbidden";
                    wwwAuthenticateResponse = wwwAuthenticate;
                }
                else
                {
                    //Set out parameter.
                    principal = claimsPrincipal;

                    //Indicate overall success.
                    overallResult = true;
                }
            }           
            catch (SecurityTokenException ex)
            {
                statusCode = (int)HttpStatusCode.Unauthorized;
                httpStatus = "401 Unauthorized";
                wwwAuthenticateResponse = wwwAuthenticate + $" error=\"invalid_token\", error_description=\"{ex.Message}\"";
            }
            catch (Exception)
            {
                statusCode = (int)HttpStatusCode.InternalServerError;
                httpStatus = "500 InternalServerError";
            }

            //Return result.
            return overallResult;
        }

        static string BuildWWWAuthenticateResponseHeader()
        {
            return $"Bearer authorization_uri =\"{authority}\", resource_id=\"{audiences[0]}";
        }

      
        /// <summary>
        /// Retrieve configuration information used to validate the access token.
        /// </summary>
        static OpenIdConnectConfiguration GetConfigurationNonAsync()
        {
            //Get the configuration.
            OpenIdConnectConfiguration config = null;
            Task.Run(async () =>
            {
                // Get open id connect configuration.
                config = await configurationManager.GetConfigurationAsync();
            }).Wait();

            //Return the configuration.
            return config;
        }

    }
 

最后,您需要更改 web.config 以重定向到新的身份验证模型。

 <system.webServer>
    <modules runAllManagedModulesForAllRequests="true">
    <!--<add name="BasicAuthentication" type="UtilitiesWcfService.BasicAuthenticationModule" />-->
      <add name="OAuthProtectionModule" type="UtilitiesWcfService.OAuthProtectionModule" /> 
    </modules>

 

AD 角色

要使角色显示在访问令牌中,从而可以从经过验证的声明 principal 中获取,您首先需要设置角色本身。在 Azure 门户的应用注册刀片中,选择您的应用,然后单击“Manifest”按钮。

按需修改 JSON manifest 中的 roles 字段。这是一个 3 个角色的示例规范。请注意,GUID 可以是任何值;我使用了以下网站 https://guidgenerator.com/

{
  "appRoles": [
    {
      "allowedMemberTypes": [
        "User"
      ],
      "displayName": "LocalUser",
      "id": "04a25ad2-ebd2-46fc-b85a-646ef2c9c5c9",
      "isEnabled": true,
      "description": "Add or edit menus for a specific site.",
      "value": "LocalUser"
    },
    {
      "allowedMemberTypes": [
        "User"
      ],
      "displayName": "GroupUser",
      "id": "434723c9-90ef-4320-bd68-cad24ca66c92",
      "isEnabled": true,
      "description": "Add or edit sites and site menus for a specific group",
      "value": "GroupUser"
    },
    {
      "allowedMemberTypes": [
        "User"
      ],
      "displayName": "Admin",
      "id": "06579139-bd57-402c-b29f-b69532de2117",
      "isEnabled": true,
      "description": "Adit or edit groups, sites and site menus.",
      "value": "Admin"
    }
  ],

接下来,您需要将用户分配到特定角色。在 Azure 门户中搜索“企业应用程序”。

选择您的数据服务应用程序,然后单击“用户和组”。

单击“添加用户”按钮并将用户分配到角色。

现在,一旦您拥有声明 principal 对象,就可以使用 IsInRole 方法检查角色成员资格。

claimsPrincipal.IsInRole("GroupUser")

 

AD 组

为了使组出现在访问令牌以及生成的声明 principal 中,首先需要创建一些组并分配一些用户。在您的 Azure 门户中搜索“组”。

然后创建组并分配用户。

为了使组出现在访问令牌和生成的声明 principal 中,您必须再次编辑数据服务应用程序的 manifest。在 approles 字段下方找到 "groupMembershipClaims" 字段,并将值更改为 "SecurityGroup"。

  "groupMembershipClaims": "SecurityGroup",

解析组对象 ID

不幸的是,您在访问令牌和生成的声明 principal 中获得的是组对象 ID GUID,而不是组名称。要获取组名称,您必须查询 Graph API。

首先,您需要允许您的数据服务自行查询 Graph API 的权限。在 Azure 门户中,转到“应用注册”,选择您的应用程序,然后转到“设置”>“必需的权限”。打开“Active Directory API”并启用“应用程序”权限:Directory.Read.All。

接下来,转到“密钥”刀片(直接位于“必需的权限”下方)。创建一个新密钥,指定您自己的密钥名称和所需的到期时间。在密钥隐藏之前记下它。

回到数据服务,您需要安装 MSAL 客户端 NuGet 包。

PM> Install-Package Microsoft.Identity.Client

然后在OAuthAuthenticationProvider类中声明 msal 机的客户端实例以及 Azure 密钥刀片中的密钥。

	 /// <summary>
        /// MSAL client for graph API lookups.
        /// </summary>
        static ConfidentialClientApplication msaClient;

         /// <summary>
        /// The password from App Registrations – App Name – Settings – Keys
        /// </summary>
         static string clientSecret = "%Secret Key%";

然后添加以下方法。AddGroupNameClaim 将接受一个声明 principal,并为每个 group object id 声明添加相应的 group_name 声明,方法是查询 Graph API。

 /// <summary>
        /// Updates the claims principle with the group name claim.
        /// </summary>
        /// <param name="claimsPrincipal">The claims principle to update.</param>
        public static void AddGroupNameClaim(ClaimsPrincipal claimsPrincipal)
        {
            (claimsPrincipal.Identity as ClaimsIdentity).AddClaims(
                claimsPrincipal.FindAll("groups").Select(r =>
                new Claim("group_name", GetGroupNameByObjectId(r.Value))));
        }

        /// <summary>
        /// Gets the group name for the specified object id.
        /// </summary>
        /// <param name="objectId">The guid for the group.</param>
        public static string GetGroupNameByObjectId(string objectId)
        {
            //Initialize graph client.
            ActiveDirectoryClient activeDirectoryClient = new ActiveDirectoryClient(new Uri($"https://graph.windows.net/{tenant}"), async () =>
            {
                return await Task.Run(async () =>
                { 
                    //If the msal client does not exist.
                    if (msaClient == null)
                    {
                        //Initialize the msal client.
                        msaClient = new ConfidentialClientApplication(
                            audiences[0],
                            authority,
                            audiences[1],
                            new Microsoft.Identity.Client.ClientCredential(clientSecret),
                            new Microsoft.Identity.Client.TokenCache(),
                            new Microsoft.Identity.Client.TokenCache());
                    }

                    //Get the token.
                    var authResult = await msaClient.AcquireTokenForClientAsync(new string[] { "https://graph.windows.net/.default" });
                    return authResult.AccessToken;
                });
            });

            IGroup group = null;
            Task.Run(async () =>
            {
                //Get the group object.
                group = await activeDirectoryClient.Groups.GetByObjectId(objectId).ExecuteAsync();
            }).Wait();

            //Return the group name.
            return group?.DisplayName;
        }

最后,更改 Authenticate 方法以调用新方法,以便经过验证的声明 principal 已更新为 group_name 声明。

 // If the token is scoped, verify that required permission is set in the scope claim.
                if (claimsPrincipal.FindFirst(scopeClaimType) != null &&
                    claimsPrincipal.FindFirst(scopeClaimType).Value != "user_impersonation")
                {
                    statusCode = (int)HttpStatusCode.Forbidden;
                    httpStatus = "403 Forbidden";
                    wwwAuthenticateResponse = wwwAuthenticate;
                }
                else
                {
                    //Update the claims principle with the group names.
                    AddGroupNameClaim(claimsPrincipal);

                    //Set out parameter.
                    principal = claimsPrincipal;

                    //Indicate overall success.
                    overallResult = true;
                }

因此,如果您随后使用声明 principal,可以通过 group_name 声明获取组名。

 claimsprinciple.FindFirst("group_name")?.Value 

存储过程实体带有多个结果集。

我提到我的存储过程 aspnet_GetUserCredentials 具有多个结果集。要实现这一点,您必须再次通过 XML 编辑器编辑定义存储过程的模型并进行一些更改。

在本例中,我的存储过程接受两个输入参数(应用程序名称和用户名)并返回 3 个结果集。

  1. 第一个结果集包含 usernamepasswordpasswordsalt 字段。
  2. 第二个结果集包含 rolename 字段。
  3. 第三个结果集包含 permissionId 字段。

以下是我需要进行的修改。

<EntityContainer Name="AuthenticationEntities" 
annotation:LazyLoadingEnabled="true" >
          <FunctionImport Name="aspnet_GetUserCredentials">
            <ReturnType Type="Collection(UtilitiesLightswitchModel.User_SprocResult)" />
            <ReturnType Type="Collection(UtilitiesLightswitchModel.Role_SprocResult)" />
            <ReturnType Type="Collection(UtilitiesLightswitchModel.Permission_SprocResult)" />
            <Parameter Name="ApplicationName" Mode="In" Type="String" />
            <Parameter Name="UserName" 
            Mode="In" Type="String" />
          </FunctionImport>
        </EntityContainer>
        <ComplexType Name="User_SprocResult">
          <Property Type="String" Name="UserName" 
          Nullable="false" MaxLength="256" />
          <Property Type="String" Name="Password" 
          Nullable="false" MaxLength="128" />
          <Property Type="String" Name="PasswordSalt" 
          Nullable="false" MaxLength="128" />
        </ComplexType>
        <ComplexType Name="Role_SprocResult">
          <Property Type="String" Name="RoleName" 
          Nullable="false" MaxLength="256" />
        </ComplexType>
        <ComplexType Name="Permission_SprocResult">
          <Property Type="String" Name="PermissionId" 
          Nullable="false" MaxLength="322" />
        </ComplexType>

...

 <FunctionImportMapping FunctionImportName="aspnet_GetUserCredentials" 
 FunctionName="UtilitiesLightswitchModel.Store.aspnet_GetUserCredentials">
            <ResultMapping>
              <ComplexTypeMapping TypeName="UtilitiesLightswitchModel.User_SprocResult">
                <ScalarProperty Name="UserName" ColumnName="UserName" />
                <ScalarProperty Name="Password" ColumnName="Password" />
                <ScalarProperty Name="PasswordSalt" ColumnName="PasswordSalt" />
              </ComplexTypeMapping>
            </ResultMapping>
            <ResultMapping>
              <ComplexTypeMapping TypeName="UtilitiesLightswitchModel.Role_SprocResult">
                <ScalarProperty Name="RoleName" ColumnName="RoleName" />
              </ComplexTypeMapping>
            </ResultMapping>
            <ResultMapping>
              <ComplexTypeMapping TypeName="UtilitiesLightswitchModel.Permission_SprocResult">
                <ScalarProperty Name="PermissionId" ColumnName="PermissionId" />
              </ComplexTypeMapping>
            </ResultMapping>
          </FunctionImportMapping>

过滤数据

您可能需要按用户筛选实体。现在我们已经通过身份验证模块设置了用户上下文,我们可以使用用户上下文来筛选结果集。您可以通过添加查询拦截器来筛选实体。此代码(如下所示)需要添加到主服务类中,在本例中是 _WcfDataService1.svc_。此示例显示根据用户所属的角色筛选 Groups 实体。

        /// <summary>
        /// Intercept entity query.
        /// </summary>
        /// <returns>Filtered recordset.</returns>
        [QueryInterceptor("Groups")]
        public Expression<Func<Group, bool>> OnQueryGroups()
        {
            //If this is a group user.
            if (HttpContext.Current.User.IsInRole("GroupUser"))
            {
                //Filter for the specific group id.
                return (Group e) => e.GroupID == HttpContext.Current.User.Identity.Name;
            }
            //If this is a local user.
            else if (HttpContext.Current.User.IsInRole("LocalUser"))
            {
                //Filter for the group containing their site id.
                return (Group e) => e.Sites.Any(r => r.SiteID == HttpContext.Current.User.Identity.Name);
            }
            else
            {
                //Return all.
                return (Group e) => true;
            }
        }

您可能还希望基于用户来确定您的数据访问规则。为此,我们可以添加一个 ChangeInterceptor。在下面的代码中,用户需要 CanAddOrEditGroups 权限才能对 Groups 实体进行更改。

  [ChangeInterceptor("Groups")]
        public void OnChangeGroups(Group group, UpdateOperations operations)
        {
            //Unbox the user principle.
            var u = (BasicAuthenticationProvider.CustomPrincipal)HttpContext.Current.User;

            if (!u.HasPermission("CanAddOrEditGroups"))
            {
                throw new DataServiceException(400, "You do not have permission to add or edit new groups.");
            }
        }

替代托管

出于任何原因,您可能希望离线托管数据服务。可以通过 <font face="Courier New">WebServiceHost </font> 类将 Web 服务托管在任何 .NET 程序中,例如 Windows 服务或控制台应用程序。

1. 首先,向现有解决方案添加一个新的 Windows 服务或控制台应用程序项目。

2. 通过 NuGet 命令行添加 OData 的 Entity Framework 提供程序,该命令还应添加 Microsoft.Data.Services API。

PM> Install-Package Microsoft.OData.EntityFrameworkProvider -Pre

3. 向项目添加应用程序配置文件(如果尚不存在),并将数据服务项目中的 _web.config_ 文件中的 configSections, system.serviceModel, connectionStrings, entityFramework runtime 配置节复制到新项目的 _app.config_ 中。

4. 在 system.serviceModel 部分内添加以下元素。

    <bindings>
      <webHttpBinding>
        <binding>
          <security mode="TransportCredentialOnly">
            <transport clientCredentialType="Basic" />
          </security>
        </binding>
      </webHttpBinding>
    </bindings>

完整的 app.config 应如下所示。请注意,在此示例中,我已将数据实体模型和身份验证实体模型的连接字符串留空,但在实际情况中它们应保持完整。

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <configSections>
    <!-- For more information on Entity Framework configuration, visit http://go.microsoft.com/fwlink/?LinkID=237468 -->
    <section name="entityFramework" type="System.Data.Entity.Internal.ConfigFile.EntityFrameworkSection, EntityFramework, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" requirePermission="false" />
    <!-- For more information on Entity Framework configuration, visit http://go.microsoft.com/fwlink/?LinkID=237468 -->
  </configSections>
  <startup>
    <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5.2" />
  </startup>
  <system.serviceModel>
    <behaviors>
      <serviceBehaviors>
        <behavior>
          <!-- To avoid disclosing metadata information, set the values below to false before deployment -->
          <serviceMetadata httpGetEnabled="true" httpsGetEnabled="true" />
          <!-- To receive exception details in faults for debugging purposes, set the value below to true.  Set to false before deployment to avoid disclosing exception information -->
          <serviceDebug includeExceptionDetailInFaults="true" httpHelpPageEnabled="true" />
        </behavior>
      </serviceBehaviors>
    </behaviors>
    <protocolMapping>
      <add binding="basicHttpsBinding" scheme="https" />
    </protocolMapping>
    <serviceHostingEnvironment aspNetCompatibilityEnabled="true" multipleSiteBindingsEnabled="true" />
    <bindings>
      <webHttpBinding>
        <binding>
          <security mode="TransportCredentialOnly">
            <transport clientCredentialType="Basic" />
          </security>
        </binding>
      </webHttpBinding>
    </bindings>
  </system.serviceModel>
  <connectionStrings>
    <add name="dfsdfsfsEntities" connectionString="" />
    <add name="AuthenticationEntities" connectionString="" />
  </connectionStrings>
  <entityFramework>
    <defaultConnectionFactory type="System.Data.Entity.Infrastructure.LocalDbConnectionFactory, EntityFramework">
      <parameters>
        <parameter value="mssqllocaldb" />
      </parameters>
    </defaultConnectionFactory>
    <providers>
      <provider invariantName="System.Data.SqlClient" type="System.Data.Entity.SqlServer.SqlProviderServices, EntityFramework.SqlServer" />
    </providers>
  </entityFramework>
  <runtime>
    <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
      <dependentAssembly>
        <assemblyIdentity name="Microsoft.Data.Services" publicKeyToken="31bf3856ad364e35" culture="neutral" />
        <bindingRedirect oldVersion="0.0.0.0-5.7.0.0" newVersion="5.7.0.0" />
      </dependentAssembly>
      <dependentAssembly>
        <assemblyIdentity name="Microsoft.Data.Services.Client" publicKeyToken="31bf3856ad364e35" culture="neutral" />
        <bindingRedirect oldVersion="0.0.0.0-5.7.0.0" newVersion="5.7.0.0" />
      </dependentAssembly>
      <dependentAssembly>
        <assemblyIdentity name="Microsoft.Data.Edm" publicKeyToken="31bf3856ad364e35" culture="neutral" />
        <bindingRedirect oldVersion="0.0.0.0-5.7.0.0" newVersion="5.7.0.0" />
      </dependentAssembly>
      <dependentAssembly>
        <assemblyIdentity name="System.Spatial" publicKeyToken="31bf3856ad364e35" culture="neutral" />
        <bindingRedirect oldVersion="0.0.0.0-5.7.0.0" newVersion="5.7.0.0" />
      </dependentAssembly>
    </assemblyBinding>
  </runtime>
</configuration>

4. 添加对数据服务库的引用,即原始数据服务项目生成的 DLL,到新项目中。

5. 这是控制台应用程序的完整代码。

using System;
using System.Linq;
using System.ServiceModel;
using System.ServiceModel.Web;
using System.ServiceModel.Channels;
using System.IdentityModel.Selectors;
using System.Configuration;

namespace WcfConsole
{
    class Program
    {
        static WebServiceHost serviceHost = null;

        static void Main(string[] args)
        {
            // Create a ServiceHost for the data service type and
            // provide the base address.
            serviceHost = new WebServiceHost(typeof(MyWcfServiceNameSpace.MyWcfDataService), new Uri[] { new Uri("https://:8195/MyDataService.svc") });
            serviceHost.Authentication.ServiceAuthenticationManager = new MyAuthentication();
            serviceHost.Credentials.UserNameAuthentication.UserNamePasswordValidationMode = System.ServiceModel.Security.UserNamePasswordValidationMode.Custom;
            serviceHost.Credentials.UserNameAuthentication.CustomUserNamePasswordValidator = new CustomUserNamePasswordValidator();

            // Open the ServiceHostBase to create listeners and start
            // listening for messages.
            serviceHost.Open();

            //Keep service alive.
            Console.ReadKey();

            //Close the service.
            serviceHost.Close();
        }

        /// <summary>
        /// Custom username and password validation.
        /// </summary>
        public class CustomUserNamePasswordValidator : UserNamePasswordValidator
        {
            /// <summary>
            /// Validate the username and password.
            /// </summary>
            /// <param name="userName">The username delivered in the message header.</param>
            /// <param name="password">The password delivered in the message header.</param>
            public override void Validate(string userName, string password)
            {
                //Store the custom principle.
                System.Security.Principal.IPrincipal principal;

                //If the user is authenicated.
                if (MyWcfServiceNameSpace.BasicAuthenticationProvider.TryGetPrincipal(new string[] { userName, password }, out principal))
                {
                    //Store the custom principle.
                    System.Threading.Thread.CurrentPrincipal = principal;
                }
                else
                {
                    //Throw an error message.
                    throw new FaultException("Unknown Username or Incorrect Password");
                }
            }
        }

        public class MyAuthentication : ServiceAuthenticationManager
        {
            /// <summary>
            /// Store the custom authorization principle in the
            /// message properties so its available to query intercepters.
            /// </summary>
            /// <param name="authPolicy">The authorization policy collection.</param>
            /// <param name="listenUri">The uri.</param>
            /// <param name="message">The message received.</param>
            /// <returns></returns>
            public override System.Collections.ObjectModel.ReadOnlyCollection<System.IdentityModel.Policy.IAuthorizationPolicy> Authenticate(
                System.Collections.ObjectModel.ReadOnlyCollection<System.IdentityModel.Policy.IAuthorizationPolicy> authPolicy,
                Uri listenUri, ref Message message)
            {
                //Store the custom principle in the message properties so its available to query intercepters.
                OperationContext.Current.IncomingMessageProperties.Add("Principal", System.Threading.Thread.CurrentPrincipal);

                //Return the policy.
                return authPolicy;
            }
        }
    }
}

5. 这是 Windows 服务的完整代码。

using System;
using System.Linq;
using System.ComponentModel;
using System.Diagnostics;
using System.ServiceProcess;
using System.ServiceModel;
using System.Configuration;
using System.Configuration.Install;
using System.ServiceModel.Web;
using System.ServiceModel.Channels;
using System.IdentityModel.Selectors;

namespace WcfWindowsService
{
    public partial class Service1 : ServiceBase
    {
        /// <summary>
        /// Instance of web service host.
        /// </summary>
        WebServiceHost serviceHost = null;

        /// <summary>
        /// Default constructor.
        /// </summary>
        public Service1()
        {
            InitializeComponent();
           
        }

        /// <summary>
        /// On startup.
        /// </summary>
        /// <param name="args">The starting arguments for the service.</param>
        protected override void OnStart(string[] args)
        {
            //If the service is not null, close it.
            serviceHost?.Close();

  
            // Create a ServiceHost for the data service type and
            // provide the base address.
            serviceHost = new WebServiceHost(typeof(MyWcfServiceNameSpace.MyWcfDataService), new Uri[] { new Uri("https://:8195/MyDataService.svc") });
	    serviceHost.Authentication.ServiceAuthenticationManager = new MyAuthentication();
            serviceHost.Credentials.UserNameAuthentication.UserNamePasswordValidationMode = System.ServiceModel.Security.UserNamePasswordValidationMode.Custom;
            serviceHost.Credentials.UserNameAuthentication.CustomUserNamePasswordValidator = new CustomUserNamePasswordValidator();

            // Open the ServiceHostBase to create listeners and start
            // listening for messages.
            serviceHost.Open();
        }

        /// <summary>
        /// Close the service.
        /// </summary>
        protected override void OnStop()
        {
            serviceHost?.Close();
            serviceHost = null;
        }
    }

    /// <summary>
    /// Custom username and password validation.
    /// </summary>
    public class CustomUserNamePasswordValidator : UserNamePasswordValidator
    {
        /// <summary>
        /// Validate the username and password.
        /// </summary>
        /// <param name="userName">The username delivered in the message header.</param>
        /// <param name="password">The password delivered in the message header.</param>
        public override void Validate(string userName, string password)
        {
            //Store the custom principle.
            System.Security.Principal.IPrincipal principal;

            //If the user is authenicated.
            if (MyWcfServiceNameSpace.BasicAuthenticationProvider.TryGetPrincipal(new string[] { userName, password }, out principal))
            {
                //Store the custom principle.
                System.Threading.Thread.CurrentPrincipal = principal;
            }
            else
            {
                //Throw an error message.
                throw new FaultException("Unknown Username or Incorrect Password");
            }
        }
    }

    /// <summary>
    /// Custom authication manager to embed authentication principle in message properties.
    /// </summary>
    public class MyAuthentication : ServiceAuthenticationManager
    {
        /// <summary>
        /// Store the custom authorization principle in the
        /// message properties so its available to query intercepters.
        /// </summary>
        /// <param name="authPolicy">The authorization policy collection.</param>
        /// <param name="listenUri">The uri.</param>
        /// <param name="message">The message received.</param>
        /// <returns></returns>
        public override System.Collections.ObjectModel.ReadOnlyCollection<System.IdentityModel.Policy.IAuthorizationPolicy> Authenticate(
            System.Collections.ObjectModel.ReadOnlyCollection<System.IdentityModel.Policy.IAuthorizationPolicy> authPolicy,
            Uri listenUri, ref Message message)
        {
            //Store the custom principle in the message properties so its available later to query intercepters.
            OperationContext.Current.IncomingMessageProperties.Add("Principal", System.Threading.Thread.CurrentPrincipal);

            //Return the policy.
            return authPolicy;
        }
    }

    // Provide the ProjectInstaller class which allows
    // the service to be installed by the Installutil.exe tool
    [RunInstaller(true)]
    public class ProjectInstaller : Installer
    {
        private ServiceProcessInstaller process;
        private ServiceInstaller service;

        public ProjectInstaller()
        {
            process = new ServiceProcessInstaller();
            process.Account = ServiceAccount.LocalSystem;
            service = new ServiceInstaller();
            service.ServiceName = "WcfWindowsService";
           
            Installers.Add(process);
            Installers.Add(service);
        }
    }

}

注意,在使用 WebServiceHost 时需要一些新的身份验证挂钩。代码仍然利用 BasicAuthenticationProvider 类来验证用户和构建自定义 principal,但此 principal 现在在 WebServiceHost UserNamePasswordValidator Validate 方法中检索。此外,它现在直接调用 TryGetPrincipal 方法,而不是 Authenicate 方法。这是因为 <font face="Courier New">BasicAuthenticationProvider </font>Authenicate 方法涉及 HttpContext ,而在使用 WebServiceHost 时没有HttpContext.此外,因为没有 HttpContext,所以安全 principal 不能存储在 HttpContext.Current.User 属性中,所以我创建一个新的自定义消息属性 Principal 来代替存储该对象。

6. 最后,任何查询或更改拦截器都需要进行修改,以从适当的上下文中访问 principal,具体取决于环境。

 /// <summary>
        /// Intercept entity query.
        /// </summary>
        /// <returns>Filtered recordset.</returns>
        [QueryInterceptor("Groups")]
        public Expression<Func<Group, bool>> OnQueryGroups()
        {
            //Unbox the security principle object.
            //f there is no httpcontext then we are not operating in a web enviroment.
            //In that case we will asume authorization is being handled by the service host.
            var u = HttpContext.Current != null ?
                HttpContext.Current.User :
                  (System.Security.Principal.IPrincipal)OperationContext.Current.IncomingMessageProperties["Principal"];

            //If this is a group user.
            if (u.IsInRole("GroupUser"))
            {
                //Filter for the specific group id.
                return (Group e) => e.GroupID == u.Identity.Name;
            }
            //If this is a local user.
            else if (u.IsInRole("LocalUser"))
            {
                //Filter for the group containing their site id.
                return (Group e) => e.Sites.Any(r => r.SiteID == u.Identity.Name);
            }
            else
            {
                //Return all.
                return (Group e) => true;
            }
        }

        [ChangeInterceptor("Groups")]
        public void OnChangeGroups(Group group, UpdateOperations operations)
        {
            //Unbox the security principle object.
            //f there is no httpcontext then we are not operating in a web enviroment.
            //In that case we will asume authorization is being handled by the service host.
            var u = HttpContext.Current != null ?
                (BasicAuthenticationProvider.CustomPrincipal)HttpContext.Current.User :
                  (BasicAuthenticationProvider.CustomPrincipal)OperationContext.Current.IncomingMessageProperties["Principal"];

            if (!u.HasPermission("LightSwitchApplication:CanAddOrEditGroups"))
            {
                throw new DataServiceException(400, "You do not have permission to add or edit new groups.");
            }

        }

历史

  • 2016-03-25:首次上传
  • BasicAuthenticationProviderHttpContext 
  • 2018-05-09:OAuth 身份验证
© . All rights reserved.