【问题标题】:How to authorize a user to only see his own records with asp.net Identity 2.0如何授权用户使用 asp.net Identity 2.0 仅查看自己的记录
【发布时间】:2017-06-27 15:04:32
【问题描述】:

对于这样一个笼统的问题,必须有一个简单的解决方案,所以我为我的无知预先道歉:

我有一个多用户 Web 应用程序(带有 EF6 的 Asp.net MVC5)。允许用户查看和/或修改存储在多个相关表(Company、Csearch、Candidate)中的相关数据。 (有关更多详细信息,请参见下文)。他们不应看到任何其他数据(例如通过篡改 URL)。

我使用 Asp.net Identity 2.0 进行身份验证,也希望将其用于上述授权。用户数据存储在标准 AspNetUser 表中。我对 Identity 和我的业务表只使用一个上下文。

我想我必须使用角色或声明来解决这个问题,但我找不到任何关于如何做到这一点的指导。谁能指出我正确的方向?

我目前已经通过向 CompanyController 添加 LINQ 条件来解决它(对于公司模型),但这似乎不是解决问题的一种非常安全和正确的方法。

public ActionResult Index(int? id, int? csearchid)
        {
            var companies = db.Companies
              .OrderBy(i => i.CompanyName)
              .Where(t => t.UserName == User.Identity.Name);
        return View(companies);

我的 DataModel 很简单,我使用 Visual Studio 2017 搭建了它 首先通过EF6代码我构建了一个关系数据模型,大致如下:

一个公司可以有多个搜索(一对多)。 每个搜索可以有多个候选(一对多)。 一个公司可以有多个用户登录。 用户保存在由 ASP.Net Identity 生成的 AspNetUsers 表中。

我的公司模型如下所示:

public class Company
{
    public int CompanyID { get; set; }

    // Link naar de Userid in Identity: AspNetUsers.Id
    [Display(Name = "Username")]
    public string UserName { get; set; }        
    public string CompanyName { get; set;}
    public string CompanyContactName { get; set; }
    [DataType(DataType.EmailAddress)]        
    public string CompanyEmail { get; set; }
    public string CompanyPhone { get; set; }        

    [Timestamp]
    public byte[] RowVersion { get; set; }

    //One to Many Navigatie links
    public virtual ICollection<Csearch> Csearches { get; set; }

【问题讨论】:

  • 你所做的就是其中的一部分。您可能需要更精细的访问权限,对于角色,使用带有角色或自定义属性的 Authorize 属性。

标签: c# asp.net-mvc linq asp.net-identity


【解决方案1】:

一旦用户被识别,您可以确保用户只能访问自己的数据。您不能为此使用角色,因为这只会定义访问级别。但您可以使用声明。

开箱即用有一个关注点分离。保持这种分离。您不应该直接查询 Identity 表。为此使用 userManager。另外从不使用 Identity 对象作为 ViewModel。你可能会暴露比你想要的更多。如果你保持这种分离,你会发现它实际上要容易得多。

身份上下文包含识别用户的所有数据,业务上下文包含所有业务信息,包括用户信息。您可能认为这是多余的,但登录用户与业务用户确实没有任何共同之处。登录电子邮件地址可能与 business.user.emailaddress 不同(这两种情况下的电子邮件地址是什么意思?)。还要考虑让用户无法登录(不再)的可能性。

根据经验,请始终考虑信息是身份的一部分还是业务的一部分。

什么时候需要ApplicationUser?仅适用于当前用户或管理用户时。查询用户时,请始终使用 business.user。因为那里应该提供您需要的所有信息。

对于当前用户,使用您需要的信息添加声明。声明的优点是您不必在每次调用时查询数据库来检索此信息,例如相应的 UserId 和(显示)UserName。

如何添加声明

您可以通过在 AspNetUserClaims 表中添加一行来向用户添加声明,而无需扩展 ApplicationUser 类。比如:

userManager.AddClaim(id, new Claim("UserId", UserId));

登录后,声明将自动添加到 ClaimsIdentity。

您还可以为扩展 ApplicationUser 的属性添加声明:

public class ApplicationUser : IdentityUser
{
    public int UserId { get; set; }

    public string DisplayUserName { get; set; }

    public async Task<ClaimsIdentity> GenerateUserIdentityAsync(UserManager<ApplicationUser> manager)
    {
        // Note the authenticationType must match the one defined in CookieAuthenticationOptions.AuthenticationType
        var userIdentity = await manager.CreateIdentityAsync(this, DefaultAuthenticationTypes.ApplicationCookie);

        // Add custom user claims here
        userIdentity.AddClaim(new Claim("UserId", UserId));
        userIdentity.AddClaim(new Claim("DisplayUserName", DisplayUserName));

        return userIdentity;
    }
}

如何解读声明

在控制器中,您可以使用如下代码读取声明:

var user = (System.Security.Claims.ClaimsIdentity)User.Identity;
var userId = user.FindFirstValue("UserId");

您可以在查询中使用 userId 来过滤当前用户的数据,甚至可以使用 business.users 作为检索数据的唯一条目。赞db.Users(u =&gt; u.Id == userId).Companies.ToList();

请注意,代码只是一个示例。我没有全部测试。这只是给你一个想法。如果有不清楚的地方,请告诉我。

【讨论】:

  • 感谢您提供非常广泛的答案。我将开始实施它。一旦我成功了(这可能需要一段时间 :)),我一定会告诉你的。
  • 如果您创建一个新的 Asp.Net Mvc 项目,您将不必实现它。因为它都是模板的一部分。您所要做的就是添加声明并将用户表添加到业务上下文中。
【解决方案2】:

真的很简单。以您提供的示例公司进行说明。请注意,您应该使用 UserId 而不是 UserName 来加入,因为 UserName 可以更改,但 UserId 将始终是唯一的。)

您需要将其更改为 UserId,而不是在 Company 表中包含 UserName。然后将 AspNetUsers 表与 UserId 上的 Company 表连接起来。

例如(我更喜欢使用查询语法而不是流利的语法):

var companies = from c in db.Companies join u in db.AspNetUsers
                on c.UserId equals u.UserId
                orderby c.CompanyName
                where u.UserName = User.Identity.Name 
                select c;

如果您还需要用户名,请将其包含在您的选择中

select new { Company = c, User = u.UserName };

但是,如果您希望每个公司拥有多个用户,则此模型不起作用。您要么需要将 CompanyId 添加到 users 表(假设用户不能是多个公司的成员),要么如果用户可以是多个公司的成员,则创建多对多联接。

因此,不是将用户链接到公司,而是将公司链接到用户。您当前的模型每个公司只允许一个用户。

我在这里看到的另一件事是在您的实体对象中使用 DisplayName。这似乎表明您正在使用 MVC 视图中的实体,这是您不应该这样做的。您应该创建一个单独的 ViewModel。

对于每个公司的多个用户来说,它应该是这样的:

public class Company
{
    public int CompanyID { get; set; }

    // Link naar de Userid in Identity: AspNetUsers.Id
    // [Display(Name = "Username")] <-- Get rid of these
    // public string UserName { get; set; } <-- get rid of these
...
}

public class ApplicationUser : IdentityUser
{
    public int CompanyId { get; set; }
}

然后将您的查询更改为:

var companies = from c in db.Companies join u in db.AspNetUsers
            on c.CompanyId equals u.CompanyId // <-- Change to this
            orderby c.CompanyName
            where u.UserName = User.Identity.Name 
            select c;

【讨论】:

  • 谢谢埃里克!这似乎确实是最简单的方法。您是否有机会知道这是否也是一个安全的解决方案?
  • 我收到一条错误消息,指出“ApplicationDbContext 不包含 AspNetUsers 的定义”...。我尝试将 DbSet 添加到上下文中,但没有解决任何问题。有什么建议吗?
  • 谢谢埃里克。它适用于特定视图,但我仍然可以访问其他视图上的所有数据。例如,当我打开详细信息视图时,我仍然可以看到未经授权的公司数据。 (.../Company/Details/2) 直接在浏览器命令行中输入。
  • @CharlesdeM - 如果您正确地将用户加入他们的公司,那应该是不可能的。如果用户只与单个公司相关联,您甚至不应该让用户指定公司 ID。只需自动与用户一起加入公司。我上面的查询就是这样做的。我不确定您指的是什么“特定视图”,但在所有情况下,如果您加入用户-> 公司,它将过滤掉不可访问的数据。
【解决方案3】:

我是通过以下方式制作的:

我在 Company 类中添加了 UserId 属性。 (它是字符串类型,因为在 SQL 中它是 NVARCHAR 类型)

public class Company
{
    public string UserId { get; set; }

    public int CompanyID { get; set; }
    // Link naar de Userid in Identity: AspNetUsers.Id
    [Display(Name = "Username")]
    public string UserName { get; set; }        
    public string CompanyName { get; set;}
    public string CompanyContactName { get; set; }
    [DataType(DataType.EmailAddress)]        
    public string CompanyEmail { get; set; }
    public string CompanyPhone { get; set; }        

    [Timestamp]
    public byte[] RowVersion { get; set; }

    //One to Many Navigatie links
    public virtual ICollection<Csearch> Csearches { get; set; }
}

在用于获取当前登录用户 ID 的创建控制器中,我使用了 How to get the current logged in user ID in ASP.NET Core? 帖子。简而言之UserId = User.FindFirstValue(ClaimTypes.NameIdentifier)

public class CompanyController : Controller
{
    private readonly ApplicationDbContext _context;
    private readonly IWebHostEnvironment webHostEnvironment;
    public CompanyController (ApplicationDbContext context, IWebHostEnvironment hostEnvironment)
        {
            _context = context;
            webHostEnvironment = hostEnvironment;
        }
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create(RecordViewModel model)
   {
       if (ModelState.IsValid)
       {                
            Company company = new Company 
            {
                 UserId = User.FindFirstValue(ClaimTypes.NameIdentifier),
                 FirstName = model.FirstName,
                 CompanyName = model.CompanyName,
                 CompanyContactName = model.CompanyContactName,
                 CompanyEmail = model.CompanyEmail,
                 CompanyPhone = model.CompanyPhone 
            };  
                _context.Add(company);
                await _context.SaveChangesAsync();
                return RedirectToAction(nameof(Index));
        }
        return View(model);
    }
}

为了只显示当前登录用户的记录,我使用以下操作:

public async Task<IActionResult> Index()
{
    var companyLoggedInUser = from c in _context.Company
                             where c.UserId == 
                             User.FindFirstValue(ClaimTypes.NameIdentifier)
                             select c;
    return View(companyLoggedInUser);                
}

【讨论】:

  • 感谢 Vasil 或您的意见。这会奏效!
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2014-06-08
  • 1970-01-01
  • 1970-01-01
  • 2020-10-15
  • 1970-01-01
  • 2017-04-22
  • 2014-07-04
相关资源
最近更新 更多