如何在WebAPI方法中获取用户的角色,而不需要查找AspNetUserRoles表?

26

我有一个存储过程用于更新状态。根据用户的角色,该存储过程可能允许或不允许更改状态。因此,我需要将角色名称传递给存储过程。我的角色名称存储在客户端的JavaScript代码中,但是当然我需要在服务器上进行第二次检查。每个用户仅具有三个角色中的一个,当请求更新状态时,我可以根据客户端所拥有的角色调用其中一个方法。这是我尝试的:

我正在使用基于Bearer Token的认证的WebApi和ASP.NET Identity 2.1,应用程序始终在浏览器中运行。我的用户已设置了适当的角色。

我编写了一些代码来获取userId,然后进入AspNetUserRoles表以在方法开始时获取角色。但是我注意到这需要大约500毫秒才能运行。作为替代方案,我正在考虑以下做法:

    [HttpPut]
    [Authorize(Roles = "Admin")]
    [Route("AdminUpdateStatus/{userTestId:int}/{userTestStatusId:int}")]
    public async Task<IHttpActionResult> AdminUpdateStatus(int userTestId, int userTestStatusId)
    {
        return await UpdateStatusMethod(userTestId, userTestStatusId, "Admin");
    }

    [HttpPut]
    [Authorize(Roles = "Student")]
    [Route("StudentUpdateStatus/{userTestId:int}/{userTestStatusId:int}")]
    public async Task<IHttpActionResult> StudentUpdateStatus(int userTestId, int userTestStatusId)
    {
        return await UpdateStatusMethod(userTestId, userTestStatusId, "Student");
    }

    [HttpPut]
    [Authorize(Roles = "Teacher")]
    [Route("TeacherUpdateStatus/{userTestId:int}/{userTestStatusId:int}")]
    public async Task<IHttpActionResult> TeacherUpdateStatus(int userTestId, int userTestStatusId)
    {
        return await UpdateStatusMethod(userTestId, userTestStatusId, "Teacher");
    }

    private async Task<IHttpActionResult> UpdateStatusMethod(int userTestId, int userTestStatusId, string roleName)
    {
        // Call the stored procedure here and pass in the roleName
    }

这是一种高效的方法吗?或者也许有另一种更干净的方式。我不太清楚前端还是后端缓存了用户的角色。我认为这应该已经完成了,或者有一些设置可以允许这样做。

请注意,我在此处使用声明将角色信息发送到我的客户端:

public static AuthenticationProperties CreateProperties(
            string userName,
            ClaimsIdentity oAuthIdentity,
            string firstName,
            string lastName,
            int organization)
        {
            IDictionary<string, string> data = new Dictionary<string, string>
                {
                    { "userName", userName},
                    { "firstName", firstName},
                    { "lastName", lastName},
                    { "organization", organization.ToString()},
                    { "roles",string.Join(":",oAuthIdentity.Claims.Where(c=> c.Type == ClaimTypes.Role).Select(c => c.Value).ToArray())}

                };
            return new AuthenticationProperties(data);
        }

然而,我的问题与服务器有关,我在方法中如何检查用户是否具有特定角色而不必访问数据库。也许有一种安全的方法可以使用声明来实现,但我不知道如何做到这一点。

非常感谢任何帮助和建议。


你有没有考虑使用声明? - Excommunicated
2
你确定是角色查找导致了延迟吗?这应该非常高效。你的存储过程在做什么?你有多少个用户? - Pleun
我之前手动调用数据库表来查找角色。经过计时,它大约需要500毫秒。我相信在生产环境中速度会更快,但我想更多地了解WebAPI框架是否每次在运行方法之前都要去数据库检查角色,或者是否有某种缓存机制。我还有许多其他的延迟,所以我想尽可能加快所有东西的速度。 - Samantha J T Star
你的用户角色是在数据库中定义的吗?如果是,为什么你还要将角色传回到过程中呢?难道你不能只通过用户名查询它吗? - Ray Cheng
用户角色在不同的SQL Azure数据库中定义。使用SQL Azure云数据库,据我所知,不可能在数据库之间进行查询。 - Samantha J T Star
把角色移动到同一个数据库中怎么样? - Pleun
3个回答

34

根据您所说,您正在使用令牌来保护您的端点。我认为关于这些令牌神奇字符串中包含的内容有一些误解。 这些令牌包含了您为其发放令牌的用户的所有角色,如果您在Web API中使用默认数据保护DPAPI而不是(JWT Tokens),那么这些令牌会被签名和加密,因此除非拥有此令牌的Web服务器的machineKey,否则无法篡改令牌内部的数据,因此不必担心数据保护。

我的建议是从数据库中读取用户的角色/声明,不需要使用您尝试做的这些变通方法和技巧,您只需要在用户登录的方法GrantResourceOwnerCredentials中设置声明即可。您可以通过获取用户然后从DB中读取角色并将其设置为类型为“Role”的声明的方式进行设置。

 var identity = new ClaimsIdentity(context.Options.AuthenticationType);
 identity.AddClaim(new Claim(ClaimTypes.Name, context.UserName));
 identity.AddClaim(new Claim(ClaimTypes.Role, "Admin"));
 identity.AddClaim(new Claim(ClaimTypes.Role, "Supervisor"));

请记住,这只会在用户登录时发生一次。然后,您将收到一个带有所有此用户声明的签名和加密令牌,无需进行任何数据库访问即可验证它。

或者,如果您想从数据库中创建身份,则可以使用以下内容:

 public async Task<ClaimsIdentity> GenerateUserIdentityAsync(UserManager<ApplicationUser> manager, string authenticationType)
    {
        // Note the authenticationType must match the one defined in CookieAuthenticationOptions.AuthenticationType
        var userIdentity = await manager.CreateIdentityAsync(this, authenticationType);
        // Add custom user claims here
        return userIdentity;
    }

然后在GrantResourceOwnerCredentials中执行以下操作:

Translated text:

然后在GrantResourceOwnerCredentials中执行以下操作:

ClaimsIdentity oAuthIdentity = await user.GenerateUserIdentityAsync(userManager, OAuthDefaults.AuthenticationType);

现在,一旦您将承载令牌发送到具有授权属性的受保护终结点,例如[Authorize(Roles = "Teacher")],我可以向您保证,您的代码不会去访问数据库执行任何查询打开 SQL Profiler 并检查,它将从加密令牌中读取声明以及角色声明,并检查此用户是否属于Teacher角色并允许或拒绝请求。

我写了一个详细的系列文章关于基于令牌的身份验证,以及授权服务器JWT 令牌。我建议您阅读这些文章以更好地理解承载令牌。


感谢您的帮助和建议。我会阅读您的博客并在接下来的几天内尝试实施您的解决方案。如果我有问题,我会在这里更新。现在只有一个快速的问题。您认为我应该如何处理需要处理多个不同的授权属性的问题?目前,具有一个私有方法和三个或更多公共方法(每个具有不同的授权)的方法是可行的。但我认为可能有更好的方法。您能否建议任何一种只使用一个方法并在其中进行角色测试的方法呢? - Samantha J T Star
欢迎,@SamanthaJ。 您所做的并没有错,您可以在操作方法中使用嵌套的if else,例如if(User.IsInRole("Teacher")){},这样您将只拥有一个公共端点而不是三个,然后您可以对此操作方法进行[Authorize(Roles = "Teacher,Student,Admin")]属性标记。 同时,此检查不会触发数据库访问,因为声明主体是基于您的访问令牌创建的。 - Taiseer Joudeh
@SamanthaJ 你试过建议的解决方案了吗?在你那边工作了吗? - Taiseer Joudeh
@TaiseerJoudeh 你能帮我回答这个问题吗?http://goo.gl/qiOdmT - Axel
1
我觉得我可能漏掉了一些非常明显的东西,但是你如何获取在令牌上设置的声明呢? - Carlos Martinez
@TaiseerJoudeh:我可以像这样添加逗号分隔的角色到声明中吗? identity.AddClaim(new Claim(ClaimTypes.Role, "Client,User")); - Amit Kumar

12

不必使用3个单独的方法,您只需在控制器类中检查User.IsInRole("RoleName")即可。它使用与[Authorize]属性相同的逻辑。

public class DataApiController : ApiController
{
    [HttpPut]
    [Route("UpdateStatus/{userTestId:int}/{userTestStatusId:int}")]
    public async Task<IHttpActionResult> UpdateStatus(int userTestId, int userTestStatusId)
    {
        if(User.IsInRole("Admin"))
        {
            //Update method etc....
        }
        //else if(....) else etc
    {
}

但是每个User.IsInRole("Admin")都不涉及数据库访问吗?如果我有5个角色,那么这将是五个数据库访问来检查用户是否在最后一个角色中,不是吗? - Samantha J T Star
3
根据你的主体而定。如果你使用的是ClaimsPrincipal,则不需要为每个角色进行数据库调用,因为它会从Claim中获取角色信息。 - Excommunicated
2
+1,这就是正确答案。@Pleun 身份框架不会为角色检查发出DB调用。请参见 Hao Kung 的答案解释。 - trailmax
此外,可以通过在web.config中的RoleProvider配置上指定一个属性来将角色集合缓存到cookie中。这意味着当您使用User.IsInRole(rolename)时,不需要进行数据库查找。 - Paul Taylor
@PaulTaylor 给你:ClaimsPrincipal principal = Request.GetRequestContext().Principal as ClaimsPrincipal;var name = User.Identity.Name;var claimValue = principal.Claims.Where(c => c.Type == "sub").Single().Value; - Taiseer Joudeh
对于那些不知道的人,用户对象在HttpContext中: HttpContext.Current.User.IsInRole - Sal

2
默认情况下,角色声明存储在用户的登录 cookie 中,因此 [Authorize(Roles = "Teacher")] 应该很快。唯一的数据库查询将在您首次登录时(或每当登录 cookie 刷新时)进行。授权检查是针对从登录 cookie 创建的 IClaimsPrincipal 进行的。
您可能还需要更新其他代码才能使其正常工作,请参见此问题,该问题类似于您要做的事情:Authorize with Roles

这个用户登录cookie是否可以被用户更改?我担心用户更改他们的cookie,然后做一些不应该做的事情。此外,我是否有更好的方法可以在方法内部进行角色检查,而不是我提出的三个具有[Authorize]的方法都指向同一个私有方法的方式? - Samantha J T Star
3
这个 cookie 已经加密,应该不会被篡改,我认为你不能轻松地使用或逻辑进行单一授权,因此拥有三种分别映射到一个具体角色的方法似乎是可以的。但是你可以直接在方法中为每个角色编写手动的 If User.IsInRole()。 - Hao Kung
Authorize 属性的 Roles 参数接受一个逗号分隔的 Rolenames 列表。 - Paul Taylor

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