防伪标记问题

142

我在反伪造令牌方面遇到了问题 :( 我创建了自己的用户类,这很好用,但是当我访问/Account/Register页面时,出现以下错误:

提供的ClaimsIdentity中未包含'ttp://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier'或'http://schemas.microsoft.com/accesscontrolservice/2010/07/claims/identityprovider'类型的声明。要使用基于声明的身份验证支持反伪造令牌,请验证配置的声明提供程序是否在生成的ClaimsIdentity实例上提供这两个声明。如果配置的声明提供程序使用不同的声明类型作为唯一标识符,则可以通过设置静态属性AntiForgeryConfig.UniqueClaimTypeIdentifier来进行配置。

我找到了这篇文章:

http://stack247.wordpress.com/2013/02/22/antiforgerytoken-a-claim-of-type-nameidentifier-or-identityprovider-was-not-present-on-provided-claimsidentity/

所以我将我的 Application_Start 方法更改为:

protected void Application_Start()
{
    AreaRegistration.RegisterAllAreas();

    FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
    RouteConfig.RegisterRoutes(RouteTable.Routes);
    BundleConfig.RegisterBundles(BundleTable.Bundles);

    AntiForgeryConfig.UniqueClaimTypeIdentifier = ClaimTypes.Email;
}

但是当我这样做时,我会遇到以下错误:

提供的 ClaimsIdentity 上不存在类型为 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress' 的声明。

有人遇到过这个问题吗?如果是,你知道如何解决吗?


这是我的自定义用户类:

public class Profile : User, IProfile
{
    public Profile()
        : base()
    {
        this.LastLoginDate = DateTime.UtcNow;
        this.DateCreated = DateTime.UtcNow;
    }

    public Profile(string userName)
        : base(userName)
    {
        this.CreatedBy = this.Id;

        this.LastLoginDate = DateTime.UtcNow;
        this.DateCreated = DateTime.UtcNow;

        this.IsApproved = true;
    }
    
    [NotMapped]
    public HttpPostedFileBase File { get; set; }

    [Required]
    public string CompanyId { get; set; }

    [Required]
    public string CreatedBy { get; set; }
    public string ModifiedBy { get; set; }

    public DateTime DateCreated { get; set; }
    public DateTime? DateModified { get; set; }
    public DateTime LastLoginDate { get; set; }

    [Required(ErrorMessageResourceType = typeof(Resources.Resources), ErrorMessageResourceName = "RequiredTitle")]
    public string Title { get; set; }
    [Required(ErrorMessageResourceType = typeof(Resources.Resources), ErrorMessageResourceName = "RequiredFirstName")]
    public string Forename { get; set; }
    [Required(ErrorMessageResourceType = typeof(Resources.Resources), ErrorMessageResourceName = "RequiredLastName")]
    public string Surname { get; set; }

    [Required(ErrorMessageResourceType = typeof(Resources.Resources), ErrorMessageResourceName = "RequiredEmail")]
    public string Email { get; set; }
    public string JobTitle { get; set; }
    public string Telephone { get; set; }
    public string Mobile { get; set; }
    public string Photo { get; set; }
    public string LinkedIn { get; set; }
    public string Twitter { get; set; }
    public string Facebook { get; set; }
    public string Google { get; set; }
    public string Bio { get; set; }

    public string CompanyName { get; set; }

    [Required(ErrorMessageResourceType = typeof(Resources.Resources), ErrorMessageResourceName = "RequiredCredentialId")]
    public string CredentialId { get; set; }
    [Required(ErrorMessageResourceType = typeof(Resources.Resources), ErrorMessageResourceName = "RequiredSecurityCode")]
    public bool IsLockedOut { get; set; }
    public bool IsApproved { get; set; }

    [Display(Name = "Can only edit own assets")]
    public bool CanEditOwn { get; set; }
    [Display(Name = "Can edit assets")]
    public bool CanEdit { get; set; }
    [Display(Name = "Can download assets")]
    public bool CanDownload { get; set; }
    [Display(Name = "Require approval to upload assets")]
    public bool RequiresApproval { get; set; }
    [Display(Name = "Can approve assets")]
    public bool CanApprove { get; set; }
    [Display(Name = "Can synchronise assets")]
    public bool CanSync { get; set; }

    public bool AgreedTerms { get; set; }
    public bool Deleted { get; set; }
}

public class ProfileContext : IdentityStoreContext
{
    public ProfileContext(DbContext db)
        : base(db)
    {
        this.Users = new UserStore<Profile>(this.DbContext);
    }
}

public class ProfileDbContext : IdentityDbContext<Profile, UserClaim, UserSecret, UserLogin, Role, UserRole>
{
}

我为我的代码库创建了一个简单的个人资料,它看起来像这样:

public interface IProfile
{
    string Id { get; set; }
    string CompanyId { get; set; }
    
    string UserName { get; set; }
    string Email { get; set; }

    string CredentialId { get; set; }
}

User 类是 Microsoft.AspNet.Identity.EntityFramework.User 类。 我的 AccountController 如下所示:

[Authorize]
public class AccountController : Controller
{
    public IdentityStoreManager IdentityStore { get; private set; }
    public IdentityAuthenticationManager AuthenticationManager { get; private set; }
    
    public AccountController() 
    {
        this.IdentityStore = new IdentityStoreManager(new ProfileContext(new ProfileDbContext()));
        this.AuthenticationManager = new IdentityAuthenticationManager(this.IdentityStore);
    }

    //
    // GET: /Account/Register
    [AllowAnonymous]
    public ActionResult Register()
    {
        return View();
    }

    //
    // POST: /Account/Register
    [HttpPost]
    [AllowAnonymous]
    public async Task<ActionResult> Register(RegisterViewModel model)
    {
        if (ModelState.IsValid)
        {
            try
            {
                // Create a profile, password, and link the local login before signing in the user
                var companyId = Guid.NewGuid().ToString();
                var user = new Profile(model.UserName)
                {
                    CompanyId = companyId,
                    Title = model.Title,
                    Forename = model.Forename,
                    Surname = model.Surname,
                    Email = model.Email,
                    CompanyName = model.CompanyName,
                    CredentialId = model.CredentialId
                };

                if (await IdentityStore.CreateLocalUser(user, model.Password))
                {
                    //Create our company
                    var company = new Skipstone.Web.Models.Company()
                    {
                        Id = companyId,
                        CreatedBy = user.Id,
                        ModifiedBy = user.Id,
                        Name = model.CompanyName
                    };

                    using (var service = new CompanyService())
                    {
                        service.Save(company);
                    }

                    await AuthenticationManager.SignIn(HttpContext, user.Id, isPersistent: false);
                    return RedirectToAction("Setup", new { id = companyId });
                }
                else
                {
                    ModelState.AddModelError("", "Failed to register user name: " + model.UserName);
                }
            }
            catch (IdentityException e)
            {
                ModelState.AddModelError("", e.Message);
            }
        }

        // If we got this far, something failed, redisplay form
        return View(model);
    }

    //
    // POST: /Account/Setup
    public ActionResult Setup(string id)
    {
        var userId = User.Identity.GetUserId();
        using (var service = new CompanyService())
        {
            var company = service.Get(id);
            var profile = new Profile()
            {
                Id = userId,
                CompanyId = id
            };

            service.Setup(profile);

            return View(company);
        }
    }
}

曾经用[ValidateAntiForgeryToken]属性进行装饰,但那时就停止工作了。

为什么?


你能展示一下自定义的用户类以及你如何使用它吗? - LostInComputer
我已经添加了自定义的User类,并说明了如何使用它。 - r3plica
您正在使用测试版。我建议您升级到正式版本,然后查看问题是否仍然存在。 - LostInComputer
7个回答

267

尝试在 global.cs 中设置:

AntiForgeryConfig.UniqueClaimTypeIdentifier = ClaimTypes.NameIdentifier;

40
我认为需要说明这个如何工作:这告诉AntiForgery类使用NameIdentifier(也就是由GetUserId找到的用户id字符串)。感谢Mike Goodwin的回答帮助我学习! - Matt DeKrey
我尝试使用 "AntiForgeryConfig.UniqueClaimTypeIdentifier = ClaimTypes.Name;",但出现错误 "Sequence contains more than one matching element",在我的情况下有多个声明(名称、角色和电子邮件地址)。我该如何解决这个问题? - Dhanuka777
15
我把它设置在Global.asax.cs中。 - Mike Taverne
8
如果您使用OpenId(例如Azure ActiveDirectory)进行身份验证,这也是解决方案。 - guysherman
11
完整的命名空间...我不得不仔细查找才能找到ClaimTypes所在的位置。 System.Web.Helpers.AntiForgeryConfig.UniqueClaimTypeIdentifier = System.Security.Claims.ClaimTypes.NameIdentifier; - Mark Rowe
1
这个在 System.Web.WebPages.dll, v2.0.0.0 中,所以不要忘记将此引用包含到项目中。 - niz_sh

76

您知道在ClaimsIdentity中可以获得哪些声明吗?如果不知道:

  1. 删除[ValidateAntiForgeryToken]属性
  2. 在控制器的某个位置设置断点并暂停执行
  3. 然后查看当前的ClaimsIdentity并检查声明
  4. 找到一个您认为可以唯一标识用户的声明
  5. AntiForgeryConfig.UniqueClaimTypeIdentifier设置为该声明类型
  6. 重新添加[ValidateAntiForgeryToken]属性

6
不仅提供直接的答案,还讲述了背景并帮助自我发现。 :) 非常感谢。 - NitinSingh
1
这真的帮了我很多。结果发现我在本地主机上运行另一个应用程序时得到了一个声明,在我的应用程序中没有使用任何声明(这就是为什么声明听起来很奇怪的原因)。所以当我退出其他应用程序时,声明消失了,错误也消失了。在实时测试环境中,这些站点更加分离。所以我认为我需要上面提到的解决方案,但只适用于本地开发。 - Michel
我觉得这个答案更有帮助。在我的情况下,我正在将现有的应用程序转换为使用IdentityServer,并且出现了这个错误。使用Mike的方法,我能够检查声明并使用适当的声明,比如主题ID(sub claim)。 - Marc Levesque

28

只需将此代码放入 global.asax.cs 文件中即可。

AntiForgeryConfig.UniqueClaimTypeIdentifier = ClaimsIdentity.DefaultNameClaimType;

谢谢。我不明白为什么我必须做出这个改变,昨晚我解决了几个代码问题,一切都正常工作。没有更改任何内容,我在另一台机器上测试了一下,一切都正常,直到几分钟前。 - Artorias2718

16

尝试在隐身窗口中打开链接或清除该域的cookie(例如localhost)。


为什么这个能够工作,问题的根本原因是什么? - Mohsen Kamrani
这是有效的,因为当您拥有一个带有无效nameidentifier的会话cookie时,服务器会尝试使用无效标识符而不将用户重定向到登录页面并获取正确的nameidentifier。 - rawel

7

编辑:此时我对这个问题有了更深入的理解,您可以忽略我下面的答案。

在 Global.asax.cs 的 Application_Start() 中设置 AntiForgeryConfig.UniqueClaimTypeIdentifier = ClaimTypes.NameIdentifier; 对我来说解决了这个问题。尽管我已经设置了声明 http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier,但我仍然遇到了与原始问题相同的错误。但是像上面那样指出它似乎起作用了。



从 MVC4 开始,防伪令牌不再使用 User.Identity.Name 作为唯一标识符。相反,它会查找错误消息中给出的两个声明。

更新注释:不应该需要这样做当用户登录时,您可以向 ClaimsIdentity 添加缺少的声明,例如:

string userId = TODO;
var identity = System.Web.HttpContext.Current.User.Identity as ClaimsIdentity;
identity.AddClaim(new Claim("http://schemas.microsoft.com/accesscontrolservice/2010/07/claims/identityprovider", userId));
identity.AddClaim(new Claim("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier", userId));

请注意,之前已经可能有其中一个声明,如果您添加两个声明将会出现重复声明的错误。如果是这样,请只添加缺失的那个声明。

1
我理解为什么你使用userId作为“/nameidentifier”,但是为什么你要将userId放入“/identityprovider”中呢? - AaronLS

7
在Global.asax.cs文件中,
1.添加以下命名空间:
using System.Web.Helpers;
using System.Security.Claims;

2.在Application_Start方法中添加以下代码:

 protected void Application_Start()
 {
       .......
       AntiForgeryConfig.UniqueClaimTypeIdentifier = ClaimsIdentity.DefaultNameClaimType;
 } 

1
它如何比上面回答的任何一个更增加价值? - NitinSingh
1
感谢您添加了usings。@NitinSingh我认为这增加了更多的价值,因为我不知道在我的项目中应该使用哪个潜在的命名空间。 - Keisha W
每当您添加新功能时,它都会要求正确的引用。一旦编译完成,您应该通过右键单击“重构”菜单来删除未使用的引用。 - NitinSingh

2

AntiForgeryConfig.UniqueClaimTypeIdentifier = ClaimTypes.Email;

这对我的情况有用,我正在使用ADFS身份验证。


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