【问题标题】:ASP.NET Core JWT mapping role claims to ClaimsIdentityASP.NET Core JWT 将角色声明映射到 ClaimsIdentity
【发布时间】:2017-06-21 13:35:39
【问题描述】:

我想使用 JWT 保护 ASP.NET Core Web API。此外,我希望可以选择直接在控制器操作属性中使用令牌有效负载中的角色。

现在,虽然我确实找到了如何将它与策略一起使用:

Authorize(Policy="CheckIfUserIsOfRoleX")
ControllerAction()...

我希望有一个选项来使用通常的东西,例如:

Authorize(Role="RoleX")

角色将从 JWT 有效负载自动映射。

{
    name: "somename",
    roles: ["RoleX", "RoleY", "RoleZ"]
}

那么,在 ASP.NET Core 中完成此任务的最简单方法是什么?有没有办法通过一些设置/映射自动使其工作(如果是,在哪里设置它?)或者我应该在验证令牌后拦截ClaimsIdentity的生成并手动添加角色声明(如果是这样,在哪里/怎么做?)?

【问题讨论】:

    标签: c# asp.net-core asp.net-core-webapi


    【解决方案1】:

    生成 JWT 时需要获取有效的声明。这是示例代码:

    登录逻辑:

    [HttpPost]
    [AllowAnonymous]
    public async Task<IActionResult> Login([FromBody] ApplicationUser applicationUser) {
        var result = await _signInManager.PasswordSignInAsync(applicationUser.UserName, applicationUser.Password, true, false);
        if(result.Succeeded) {
            var user = await _userManager.FindByNameAsync(applicationUser.UserName);
    
            // Get valid claims and pass them into JWT
            var claims = await GetValidClaims(user);
    
            // Create the JWT security token and encode it.
            var jwt = new JwtSecurityToken(
                issuer: _jwtOptions.Issuer,
                audience: _jwtOptions.Audience,
                claims: claims,
                notBefore: _jwtOptions.NotBefore,
                expires: _jwtOptions.Expiration,
                signingCredentials: _jwtOptions.SigningCredentials);
            //...
        } else {
            throw new ApiException('Wrong username or password', 403);
        }
    }
    

    获取基于UserRolesRoleClaimsUserClaims 表(ASP.NET 身份)的用户声明:

    private async Task<List<Claim>> GetValidClaims(ApplicationUser user)
    {
        IdentityOptions _options = new IdentityOptions();
        var claims = new List<Claim>
            {
                new Claim(JwtRegisteredClaimNames.Sub, user.UserName),
                new Claim(JwtRegisteredClaimNames.Jti, await _jwtOptions.JtiGenerator()),
                new Claim(JwtRegisteredClaimNames.Iat, ToUnixEpochDate(_jwtOptions.IssuedAt).ToString(), ClaimValueTypes.Integer64),
                new Claim(_options.ClaimsIdentity.UserIdClaimType, user.Id.ToString()),
                new Claim(_options.ClaimsIdentity.UserNameClaimType, user.UserName)
            };
        var userClaims = await _userManager.GetClaimsAsync(user);
        var userRoles = await _userManager.GetRolesAsync(user);
        claims.AddRange(userClaims);
        foreach (var userRole in userRoles)
        {
            claims.Add(new Claim(ClaimTypes.Role, userRole));
            var role = await _roleManager.FindByNameAsync(userRole);
            if(role != null)
            {
                var roleClaims = await _roleManager.GetClaimsAsync(role);
                foreach(Claim roleClaim in roleClaims)
                {
                    claims.Add(roleClaim);
                }
            }
        }
        return claims;
    }
    

    请在Startup.cs 中将所需的策略添加到授权中:

    void ConfigureServices(IServiceCollection service) {
       services.AddAuthorization(options =>
        {
            // Here I stored necessary permissions/roles in a constant
            foreach (var prop in typeof(ClaimPermission).GetFields(BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy))
            {
                options.AddPolicy(prop.GetValue(null).ToString(), policy => policy.RequireClaim(ClaimType.Permission, prop.GetValue(null).ToString()));
            }
        });
    }
    

    ClaimPermission:

    public static class ClaimPermission
    {
        public const string
            CanAddNewService = "Tự thêm dịch vụ",
            CanCancelCustomerServices = "Hủy dịch vụ khách gọi",
            CanPrintReceiptAgain = "In lại hóa đơn",
            CanImportGoods = "Quản lý tồn kho",
            CanManageComputers = "Quản lý máy tính",
            CanManageCoffees = "Quản lý bàn cà phê",
            CanManageBillards = "Quản lý bàn billard";
    }
    

    使用类似的sn-p获取所有预定义的权限,并插入到asp.net权限声明表中:

    var staffRole = await roleManager.CreateRoleIfNotExists(UserType.Staff);
    
    foreach (var prop in typeof(ClaimPermission).GetFields(BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy))
    {
        await roleManager.AddClaimIfNotExists(staffRole, prop.GetValue(null).ToString());
    }
    

    我是 ASP.NET 的初学者,如果您有更好的解决方案,请告诉我。

    而且,当我将所有声明/权限都放入 JWT 时,我不知道有多糟糕。太长?表现 ?我是否应该将生成的 JWT 存储在数据库中并稍后检查以获取有效用户的角色/声明?

    【讨论】:

    • 这是完美的答案!大多数其他解决方案都没有角色声明
    • 对象ClaimPermission来自哪里?
    • 任何人都在寻找ClaimPermission,请查看编辑后的答案
    【解决方案2】:

    这是我的工作代码! ASP.NET 核心 2.0 + JWT。将角色添加到 JWT 令牌。

    appsettings.json

    "JwtIssuerOptions": {
       "JwtKey": "4gSd0AsIoPvyD3PsXYNrP2XnVpIYCLLL",
       "JwtIssuer": "http://yourdomain.com",
       "JwtExpireDays": 30
    }
    

    Startup.cs

    // ===== Add Jwt Authentication ========
    JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); // => remove default claims
    // jwt
    // get options
    var jwtAppSettingOptions = Configuration.GetSection("JwtIssuerOptions");
    services
        .AddAuthentication(options =>
        {
            options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
            options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
            options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
        })
        .AddJwtBearer(cfg =>
        {
            cfg.RequireHttpsMetadata = false;
            cfg.SaveToken = true;
            cfg.TokenValidationParameters = new TokenValidationParameters
            {
                ValidIssuer = jwtAppSettingOptions["JwtIssuer"],
                ValidAudience = jwtAppSettingOptions["JwtIssuer"],
                IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtAppSettingOptions["JwtKey"])),
                ClockSkew = TimeSpan.Zero // remove delay of token when expire
            };
        });
    

    AccountController.cs

    [HttpPost]
    [AllowAnonymous]
    [Produces("application/json")]
    public async Task<object> GetToken([FromBody] LoginViewModel model)
    {
        var result = await _signInManager.PasswordSignInAsync(model.Email, model.Password, false, false);
    
        if (result.Succeeded)
        {
            var appUser = _userManager.Users.SingleOrDefault(r => r.Email == model.Email);
            return await GenerateJwtTokenAsync(model.Email, appUser);
        }
    
        throw new ApplicationException("INVALID_LOGIN_ATTEMPT");
    }
    
    // create token
    private async Task<object> GenerateJwtTokenAsync(string email, ApplicationUser user)
    {
        var claims = new List<Claim>
        {
            new Claim(JwtRegisteredClaimNames.Sub, email),
            new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
            new Claim(ClaimTypes.NameIdentifier, user.Id)
        };
    
        var roles = await _userManager.GetRolesAsync(user);
    
        claims.AddRange(roles.Select(role => new Claim(ClaimsIdentity.DefaultRoleClaimType, role)));
    
        // get options
        var jwtAppSettingOptions = _configuration.GetSection("JwtIssuerOptions");
    
        var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtAppSettingOptions["JwtKey"]));
        var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
        var expires = DateTime.Now.AddDays(Convert.ToDouble(jwtAppSettingOptions["JwtExpireDays"]));
    
        var token = new JwtSecurityToken(
            jwtAppSettingOptions["JwtIssuer"],
            jwtAppSettingOptions["JwtIssuer"],
            claims,
            expires: expires,
            signingCredentials: creds
        );
    
        return new JwtSecurityTokenHandler().WriteToken(token);
    }
    

    提琴手测试GetToken 方法。要求:

    POST https://localhost:44355/Account/GetToken HTTP/1.1
    content-type: application/json
    Host: localhost:44355
    Content-Length: 81
    
    {
        "Email":"admin@admin.site.com",
        "Password":"ukj90ee",
        "RememberMe":"false"
    }
    

    调试响应令牌https://jwt.io/#debugger-io

    有效载荷数据:

    {
      "sub": "admin@admin.site.com",
      "jti": "520bc1de-5265-4114-aec2-b85d8c152c51",
      "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier": "8df2c15f-7142-4011-9504-e73b4681fb6a",
      "http://schemas.microsoft.com/ws/2008/06/identity/claims/role": "Admin",
      "exp": 1529823778,
      "iss": "http://yourdomain.com",
      "aud": "http://yourdomain.com"
    }
    

    角色管理员工作!

    【讨论】:

    • 您可以将循环foreach (var role in roles) …替换为单行claims.AddRange(roles.Select(role =&gt; new Claim(ClaimsIdentity.DefaultRoleClaimType, role)));
    • 感谢 Vadim 更正关于循环的描述和建议。我做出了改变!
    • 为什么不使用 new Claim(JwtRegisteredClaimNames.Sub, email) 而不是 new Claim(JwtRegisteredClaimNames.Sub, user.Email),
    • 正确。您只能传递 ApplicationUser 参数。然后将其全部拉出以获取令牌。重构将改进我的代码。
    【解决方案3】:

    为了生成 JWT 令牌,我们需要 AuthJwtTokenOptions 辅助类

    public static class AuthJwtTokenOptions
    {
        public const string Issuer = "SomeIssuesName";
    
        public const string Audience = "https://awesome-website.com/";
    
        private const string Key = "supersecret_secretkey!12345";
    
        public static SecurityKey GetSecurityKey() =>
            new SymmetricSecurityKey(Encoding.ASCII.GetBytes(Key));
    }
    

    帐户控制器代码:

    [HttpPost]
    public async Task<IActionResult> GetToken([FromBody]Credentials credentials)
    {
        // TODO: Add here some input values validations
    
        User user = await _userRepository.GetUser(credentials.Email, credentials.Password);
        if (user == null)
            return BadRequest();
    
        ClaimsIdentity identity = GetClaimsIdentity(user);
    
        return Ok(new AuthenticatedUserInfoJsonModel
        {
            UserId = user.Id,
            Email = user.Email,
            FullName = user.FullName,
            Token = GetJwtToken(identity)
        });
    }
    
    private ClaimsIdentity GetClaimsIdentity(User user)
    {
        // Here we can save some values to token.
        // For example we are storing here user id and email
        Claim[] claims = new[]
        {
            new Claim(ClaimTypes.Name, user.Id.ToString()),
            new Claim(ClaimTypes.Email, user.Email)
        };
        ClaimsIdentity claimsIdentity = new ClaimsIdentity(claims, "Token");
    
        // Adding roles code
        // Roles property is string collection but you can modify Select code if it it's not
        claimsIdentity.AddClaims(user.Roles.Select(role => new Claim(ClaimTypes.Role, role)));
        return claimsIdentity;
    }
    
    private string GetJwtToken(ClaimsIdentity identity)
    {
        JwtSecurityToken jwtSecurityToken = new JwtSecurityToken(
            issuer: AuthJwtTokenOptions.Issuer,
            audience: AuthJwtTokenOptions.Audience,
            notBefore: DateTime.UtcNow,
            claims: identity.Claims,
            // our token will live 1 hour, but you can change you token lifetime here
            expires: DateTime.UtcNow.Add(TimeSpan.FromHours(1)),
            signingCredentials: new SigningCredentials(AuthJwtTokenOptions.GetSecurityKey(), SecurityAlgorithms.HmacSha256));
        return new JwtSecurityTokenHandler().WriteToken(jwtSecurityToken);
    }
    

    Startup.cs 中,在services.AddMvc 调用之前将以下代码添加到ConfigureServices(IServiceCollection services) 方法中:

    public void ConfigureServices(IServiceCollection services)
    {
        // Other code here…
    
        services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
            .AddJwtBearer(options =>
            {
                options.TokenValidationParameters = new TokenValidationParameters
                {
                    ValidateIssuer = true,
                    ValidIssuer = AuthJwtTokenOptions.Issuer,
    
                    ValidateAudience = true,
                    ValidAudience = AuthJwtTokenOptions.Audience,
                    ValidateLifetime = true,
    
                    IssuerSigningKey = AuthJwtTokenOptions.GetSecurityKey(),
                    ValidateIssuerSigningKey = true
                };
            });
    
        // Other code here…
    
        services.AddMvc();
    }
    

    还要在app.UseMvc 调用之前将app.UseAuthentication() 调用添加到Startup.csConfigureMethod

    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {
        // Other code here…
    
        app.UseAuthentication();
        app.UseMvc();
    }
    

    现在您可以使用[Authorize(Roles = "Some_role")] 属性。

    要在任何控制器中获取用户 ID 和电子邮件,您应该这样做

    int userId = int.Parse(HttpContext.User.Claims.First(c => c.Type == ClaimTypes.Name).Value);
    
    string email = HttpContext.User.Claims.First(c => c.Type == ClaimTypes.Email).Value;
    

    userId 也可以通过这种方式检索(这是由于声明类型名称 ClaimTypes.Name

    int userId = int.Parse(HttpContext.User.Identity.Name);
    

    最好将这样的代码移到一些控制器扩展助手中:

    public static class ControllerExtensions
    {
        public static int GetUserId(this Controller controller) =>
            int.Parse(controller.HttpContext.User.Claims.First(c => c.Type == ClaimTypes.Name).Value);
    
        public static string GetCurrentUserEmail(this Controller controller) =>
            controller.HttpContext.User.Claims.First(c => c.Type == ClaimTypes.Email).Value;
    }
    

    您添加的任何其他Claim 也是如此。您应该只指定有效的密钥。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2021-05-19
      • 2018-05-08
      • 1970-01-01
      • 2021-02-26
      • 2023-03-19
      • 2019-10-01
      • 2019-04-14
      • 1970-01-01
      相关资源
      最近更新 更多