ASP.NET 核心身份 [Authorize(Roles ="ADMIN")] 无效

ASP.NET Core Identity [Authorize(Roles ="ADMIN")] not work

我正在使用 .NET 版本 5.0.100-rc.1.20452.10,ASP.NET Core Web API,Microsoft SQL Server 2019,JWT 令牌。我有 Startup.cs

using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Hosting;
using Microsoft.IdentityModel.Tokens;
using shadow.Data;
using shadow.Models;
using shadow.Services;
using System.IO;
using System.Text;

namespace shadow
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
            services.AddHttpContextAccessor();
            //services.AddCors();
            services.AddCors(options =>
            {
                options.AddPolicy("CorsPolicy",
                    builder => builder.AllowAnyOrigin()
                        .AllowAnyMethod()
                        .AllowAnyHeader());
            });
            services.AddSingleton<IUriService>(o =>
            {
                var accessor = o.GetRequiredService<IHttpContextAccessor>();
                var request = accessor.HttpContext.Request;
                var uri = string.Concat(request.Scheme, "://", request.Host.ToUriComponent());
                return new UriService(uri);
            });
            services.AddIdentity<ApplicationUser, IdentityRole>(options =>
            {
                //options.Password.RequireDigit = true;
                //options.Password.RequireLowercase = true;
                options.Password.RequiredLength = 6;
            }).AddEntityFrameworkStores<ApplicationDbContext>().AddDefaultTokenProviders();
            services
                .AddAuthentication(auth =>
                {
                    auth.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
                    auth.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
                })

                  .AddJwtBearer(options =>
                {
                    options.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters
                    {
                        ValidateIssuer = true,
                        ValidateAudience = true,
                        ValidAudience = Configuration["AuthSettings:Audience"],
                        ValidIssuer = Configuration["AuthSettings:Issuer"],
                        RequireExpirationTime = true,
                        IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["AuthSettings:Key"])),
                        ValidateIssuerSigningKey = true
                    };
                });
            services.AddAuthorization(options =>
            {
                options.FallbackPolicy = new AuthorizationPolicyBuilder()
                    .RequireAuthenticatedUser()
                    .Build();

            });
            services.AddScoped<IUserService, UserService>();
            services.AddTransient<IMailService, SendGridMailService>();            
            services.AddControllers();
            services.AddRazorPages();
            services.AddControllers(options => options.SuppressAsyncSuffixInActionNames = false);
            services.AddMvc().AddJsonOptions(options =>
            {
                //options.JsonSerializerOptions.DictionaryKeyPolicy = JsonNamingPolicy.CamelCase;
                options.JsonSerializerOptions.PropertyNamingPolicy = null;
            });
        }

        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                app.UseHsts();
            }
            //app.UseHttpsRedirection();
            app.UseRouting();
            // app.UseCors(options => options.WithOrigins("http://localhost:4200")
            //.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader());
            app.UseCors("CorsPolicy");
            app.UseAuthentication();
            app.UseAuthorization();
            // Server: https://shorten.news/static-file/content/10992.mp3
            // Local: http://localhost:5000/static-file/content/10992.mp3
            app.UseStaticFiles(new StaticFileOptions
            {
                FileProvider = new PhysicalFileProvider(
                    // Path.Combine(env.ContentRootPath, @"c:\audio\")),
                    // Path.Combine(@"c:\audio\")),
                    Path.Combine(@"D:\shadow_backend\Upload\files\")
                ),
                RequestPath = "/static-file",
                OnPrepareResponse = context =>
                {
                    context.Context.Response.Headers["Access-Control-Allow-Origin"] = "*";
                }
            });

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllers();
            });
            app.Run(async (context) =>
            {
                await context.Response.WriteAsync("cound not find anything");
            });
        }
    }

}

文件IUserService.cs

using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Extensions.Configuration;
using Microsoft.IdentityModel.Tokens;
using shadow.Models;
using shadow.Shared;
using System;
using System.IdentityModel.Tokens.Jwt;
using System.Linq;
using System.Security.Claims;
using System.Text;
using System.Threading.Tasks;

namespace shadow.Services
{

    public interface IUserService
    {

        Task<UserManagerResponse> RegisterUserAsync(RegisterViewModel model);
        Task<UserManagerResponse> LoginUserAsync(LoginViewModel model);
        Task<UserManagerResponse> LogoutUserAsync(LoginViewModel model);
        Task<UserManagerResponse> ConfirmEmailAsync(string userId, string token);
        Task<UserManagerResponse> ForgetPasswordAsync(string email);
        Task<UserManagerResponse> ResetPasswordAsync(ResetPasswordViewModel model);
        Task<UserManagerResponse> ChangePasswordAsync(ChangePasswordViewModel model);
    }

    public class UserService : IUserService
    {
        private Microsoft.AspNetCore.Identity.UserManager<ApplicationUser> _userManger;
        private IConfiguration _configuration;
        private IMailService _mailService;

        public UserService(Microsoft.AspNetCore.Identity.UserManager<ApplicationUser> userManager, IConfiguration configuration, IMailService mailService)
        {
            _userManger = userManager;
            _configuration = configuration;
            _mailService = mailService;
        }

        public async Task<UserManagerResponse> RegisterUserAsync(RegisterViewModel model)
        {
            if (model == null)
            {
                throw new NullReferenceException("Reigster Model is null");
            }
            if (model.Password != model.ConfirmPassword)
            {
                return new UserManagerResponse
                {
                    Message = "Confirm password doesn't match the password",
                    IsSuccess = false,
                };
            }
            var identityUser = new ApplicationUser
            {
                Email = model.Email,
                UserName = model.Email,
                About = model.About,
                SecondMobile = model.SecondMobile,
                Fullname = model.Fullname,
                AliasName = model.AliasName,
                Created = DateTime.Now,
                Modified = DateTime.Now
            };
            var result = await _userManger.CreateAsync(identityUser, model.Password);

            if (result.Succeeded)
            {
                var confirmEmailToken = await _userManger.GenerateEmailConfirmationTokenAsync(identityUser);
                var encodedEmailToken = Encoding.UTF8.GetBytes(confirmEmailToken);
                var validEmailToken = WebEncoders.Base64UrlEncode(encodedEmailToken);
                string url = $"{_configuration["AppUrl"]}/api/auth/ConfirmEmail?userId={identityUser.Id}&token={validEmailToken}";
                await _mailService.SendEmailAsync(identityUser.Email, "Confirm your email", $"<h1>Welcome to Trustee app</h1>" +
                    $"<p>Please confirm your email by <a href='{url}'>clicking here</a></p>");
                return new UserManagerResponse
                {
                    Message = "User created successfully!",
                    IsSuccess = true,
                };
            }
            return new UserManagerResponse
            {
                Message = "User did not create",
                IsSuccess = false,
                Errors = result.Errors.Select(e => e.Description)
            };
        }

        /// <summary>
        /// Đăng nhập.
        /// </summary>
        /// <param name="model"></param>
        /// <returns></returns>
        public async Task<UserManagerResponse> LoginUserAsync(LoginViewModel model)
        {
            var user = await _userManger.FindByEmailAsync(model.Email);
            if (user == null)
            {
                return new UserManagerResponse
                {
                    Message = "There is no user with that Email address",
                    IsSuccess = false,
                };
            }
            var result = await _userManger.CheckPasswordAsync(user, model.Password);
            if (!result)
            {
                return new UserManagerResponse
                {
                    Message = "Invalid password",
                    IsSuccess = false,
                };
            }
            var claims = new[]
            {
                new Claim("Email", model.Email),
                new Claim(ClaimTypes.NameIdentifier, user.Id),
            };
            var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["AuthSettings:Key"]));
            var token = new JwtSecurityToken(
                issuer: _configuration["AuthSettings:Issuer"],
                audience: _configuration["AuthSettings:Audience"],
                claims: claims,
                expires: DateTime.Now.AddDays(30),
                signingCredentials: new SigningCredentials(key, SecurityAlgorithms.HmacSha256));
            string tokenAsString = new JwtSecurityTokenHandler().WriteToken(token);
            return new UserManagerResponse
            {
                Message = tokenAsString,
                IsSuccess = true,
                ExpireDate = token.ValidTo
            };
        }

        // Đăng xuất.
        public async Task<UserManagerResponse> LogoutUserAsync(LoginViewModel model)
        {
            var user = await _userManger.FindByEmailAsync(model.Email);
            if (user == null)
            {
                return new UserManagerResponse
                {
                    Message = "There is no user with that Email address",
                    IsSuccess = false,
                };
            }
            var result = await _userManger.CheckPasswordAsync(user, model.Password);
            if (!result)
            {
                return new UserManagerResponse
                {
                    Message = "Invalid password",
                    IsSuccess = false,
                };
            }
            var claims = new[]
            {
                new Claim("Email", model.Email),
                new Claim(ClaimTypes.NameIdentifier, user.Id),
            };
            var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["AuthSettings:Key"]));
            var token = new JwtSecurityToken(
                issuer: _configuration["AuthSettings:Issuer"],
                audience: _configuration["AuthSettings:Audience"],
                claims: claims,
                expires: DateTime.Now.AddDays(30),
                signingCredentials: new SigningCredentials(key, SecurityAlgorithms.HmacSha256));
            string tokenAsString = new JwtSecurityTokenHandler().WriteToken(token);
            return new UserManagerResponse
            {
                Message = tokenAsString,
                IsSuccess = true,
                ExpireDate = token.ValidTo
            };
        }

        public async Task<UserManagerResponse> ConfirmEmailAsync(string userId, string token)
        {
            var user = await _userManger.FindByIdAsync(userId);
            if (user == null)
            {
                return new UserManagerResponse { IsSuccess = false, Message = "User not found" };
            }
            var decodedToken = WebEncoders.Base64UrlDecode(token);
            string normalToken = Encoding.UTF8.GetString(decodedToken);
            var result = await _userManger.ConfirmEmailAsync(user, normalToken);
            if (result.Succeeded)
            {
                return new UserManagerResponse { Message = "Email confirmed successfully!", IsSuccess = true };
            }
            return new UserManagerResponse
            {
                IsSuccess = false,
                Message = "Email did not confirm",
                Errors = result.Errors.Select(e => e.Description)
            };
        }

        public async Task<UserManagerResponse> ForgetPasswordAsync(string email)
        {
            var user = await _userManger.FindByEmailAsync(email);
            if (user == null)
            {
                return new UserManagerResponse { IsSuccess = false, Message = "No user associated with email", };
            }
            var token = await _userManger.GeneratePasswordResetTokenAsync(user);
            var encodedToken = Encoding.UTF8.GetBytes(token);
            var validToken = WebEncoders.Base64UrlEncode(encodedToken);
            string url = $"{_configuration["AppUrl"]}/ResetPassword?email={email}&token={validToken}";
            await _mailService.SendEmailAsync(email, "Reset Password", "<h1>Follow the instructions to reset your password</h1>" +
                $"<p>To reset your password <a href='{url}'>Click here</a></p>");
            return new UserManagerResponse
            {
                IsSuccess = true,
                Message = "Reset password URL has been sent to the email successfully!"
            };
        }

        public async Task<UserManagerResponse> ResetPasswordAsync(ResetPasswordViewModel model)
        {
            var user = await _userManger.FindByEmailAsync(model.Email);
            if (user == null)
            {
                return new UserManagerResponse { IsSuccess = false, Message = "No user associated with email", };
            }
            if (model.NewPassword != model.ConfirmPassword)
            {
                return new UserManagerResponse { IsSuccess = false, Message = "Password doesn't match its confirmation", };
            }
            var decodedToken = WebEncoders.Base64UrlDecode(model.Token);
            string normalToken = Encoding.UTF8.GetString(decodedToken);
            var result = await _userManger.ResetPasswordAsync(user, normalToken, model.NewPassword);
            if (result.Succeeded)
            {
                return new UserManagerResponse { Message = "Password has been reset successfully!", IsSuccess = true };
            }
            return new UserManagerResponse
            {
                Message = "Something went wrong",
                IsSuccess = false,
                Errors = result.Errors.Select(e => e.Description)
            };
        }
    
        public async Task<UserManagerResponse> ChangePasswordAsync(ChangePasswordViewModel model)
        {
            var tokenString = model.Token;            
            var jwtEncodedString = tokenString.Substring(7); // trim 'Bearer ' from the start since its just a prefix for the token string
            var token = new JwtSecurityToken(jwtEncodedString: jwtEncodedString);
            string email = token.Claims.First(c => c.Type == "Email").Value;
            Console.WriteLine("email => " + email);
            var user = await _userManger.FindByEmailAsync(email);
            if (user == null)
            {
                return new UserManagerResponse { IsSuccess = false, Message = "No user associated with email", };
            }
            if (model.NewPassword != model.ConfirmPassword)
            {
                return new UserManagerResponse { IsSuccess = false, Message = "Password doesn't match its confirmation", };
            }
            var token2 = await _userManger.GeneratePasswordResetTokenAsync(user);
            var result = await _userManger.ResetPasswordAsync(user, token2, model.NewPassword);
            if (result.Succeeded)
            {
                return new UserManagerResponse { Message = "Password has been changed successfully!", IsSuccess = true };
            }
            return new UserManagerResponse
            {
                Message = "Something went wrong",
                IsSuccess = false,
                Errors = result.Errors.Select(e => e.Description)
            };
        }     
    }

}

文件appsettings.json

{
  "ConnectionStrings": {
    "DefaultConnection": "Server=.;Database=foo;User Id=sa; Password=SecrEt_STring;Trusted_Connection=False;MultipleActiveResultSets=True"
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*",
  "AuthSettings": {
    "Key": "This is the key that we will use in the encryption",
    "Audience": "http://example.io",
    "Issuer": "http://example.io"
  },
  "SendGridAPIKey": "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",
  "AppUrl": "http://localhost:5002"
}

在控制器中

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using shadow.Data;
using shadow.DTO;
using shadow.Models;
using shadow.Services;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;

namespace shadow.Controllers
{
    [Route("[controller]")]
    [ApiController]
    public class AssetItemController : ControllerBase
    {
        private IUserService _userService;
        private IMailService _mailService;
        private IConfiguration _configuration;
        private ApplicationDbContext _db;

        public AssetItemController(IUserService userService, IMailService mailService, IConfiguration configuration, ApplicationDbContext context /*, SignInManager<IdentityUser> signInManager */)
        {
            _userService = userService;
            _mailService = mailService;
            _configuration = configuration;
            _db = context;
            // _signInManager = signInManager;
        }

        // Admin liệt kê tất cả các tài sản.
        [HttpGet]
        [Route("all")]
        [Authorize(Roles ="ADMIN")]        
        public async Task<ActionResult<AssetItem>> GetAllAssetItems()
        {
            var a = await Task.Run(() => _db.AssetItems);
            if (a != null)
            {
                return Ok(a.ToList());
            }
            else
            {
                return NoContent();
            }
        }

        // Liệt kê tất cả các tài sản theo user_id.
        [HttpGet]
        [Route("user/{userId}")]
        public async Task<ActionResult<IEnumerable<AssetItem>>> GetAllAssetItemsByUserId(string userId)
        {
            var assetListByUserId = await Task.Run(() => _db.AssetItems.Where(x => x.UserId == userId).ToList());
            if (assetListByUserId != null)
            {
                return Ok(assetListByUserId);
            }
            else
            {
                return NoContent();
            }

        }

        // Xem chi tiet asset_item theo id cua no.
        [HttpGet]
        [Route("{assetItemId}")]
        public async Task<ActionResult<IEnumerable<AssetItem>>> ViewAssetItemById(int assetItemId)
        {
            var assetListByUserId = await Task.Run(() => _db.AssetItems.Where(x => x.Id == assetItemId).FirstOrDefault());
            if (assetListByUserId != null)
            {
                return Ok(assetListByUserId);
            }
            else
            {
                return NoContent();
            }
        }

        // Xóa tài sản.
        [HttpDelete("{id}")]
        public async Task<ActionResult<string>> DeleteAssetItem(int id)
        {
            var item = _db.AssetItems.Find(id);
            if (item != null)
            {
                _db.AssetItems.Remove(item);
                await _db.SaveChangesAsync();
                return Ok("ok");
            }
            else
            {
                return NotFound();
            }

        }

        // POST http://localhost:5002/AssetItem
        [HttpPost]
        [ProducesResponseType(StatusCodes.Status200OK)]
        [ProducesResponseType(typeof(string), StatusCodes.Status400BadRequest)]
        [Authorize]
        public async Task<ActionResult<AssetItem>> Add([FromForm] AssetItemDTO input)
        {
            if (ModelState.IsValid)
            {
                AssetItem assetItem = new AssetItem();
                if (input.AssetTypeId != null)
                {
                    assetItem.AssetTypeId = input.AssetTypeId;
                }

                if (input.UserId != null)
                {
                    assetItem.UserId = input.UserId;
                }

                if (input.TrustedPersonId != null)
                {
                    assetItem.TrustedPersonId = input.TrustedPersonId;
                }

                if (input.TrustedContent != null)
                {
                    assetItem.TrustedContent = input.TrustedContent;
                }

                if (input.Description != null)
                {
                    assetItem.Description = input.Description;
                }

                assetItem.Created = DateTime.Now;
                if (input.file != null)
                {
                    if (IsImageFile(input.file))
                    {
                        string imgPath = await WriteFile(input.file);
                        assetItem.ImagePath = imgPath;
                    }
                    else
                    {
                        return BadRequest(new { message = "Invalid file extension" });
                    }
                }
                _db.AssetItems.Add(assetItem);
                await _db.SaveChangesAsync();
                return Ok(assetItem);
            }
            else
            {
                return BadRequest(ModelState);
            }
        }

        // Edit 1 tài sản.
        // PUT http://localhost:5002/AssetItem
        [HttpPut("{id}")]
        [ProducesResponseType(StatusCodes.Status200OK)]
        [ProducesResponseType(typeof(string), StatusCodes.Status400BadRequest)]
        [Authorize]
        public async Task<ActionResult<AssetItem>> Edit([FromForm] AssetItemDTO input, int id)
        {
            if (ModelState.IsValid)
            {
                var foo = _db.AssetItems.Find(id);
                if (!String.IsNullOrEmpty(input.TrustedContent))
                {
                        foo.TrustedContent = input.TrustedContent;                    
                }else              
                {
                    foo.TrustedContent = null;
                }

                if (input.file != null)
                {
                    string imgPath = await WriteFile(input.file);
                    foo.ImagePath = imgPath;
                }
                else
                {
                    foo.ImagePath = null;
                }

                if (!String.IsNullOrEmpty(input.Description))
                {
                    foo.Description = input.Description;
                }
                else
                {
                    foo.Description = null;
                }

                foo.AssetTypeId = input.AssetTypeId;

                if (input.TrustedPersonId != null)
                {
                    foo.TrustedPersonId = input.TrustedPersonId;
                }
                // Hiện chưa ghi nhận user_modified. Cần xem lại Requirement.
                if (!String.IsNullOrEmpty(input.UserModified))
                {
                    foo.UserModified = input.UserModified;
                }
                else
                {
                    foo.UserModified = null;
                }
                foo.Modified = DateTime.Now;
                _db.Update(foo);
                await _db.SaveChangesAsync();
                return Ok(foo);
            }
            else
            {
                return BadRequest(ModelState);
            }
        }

        private bool IsImageFile(IFormFile file)
        {
            try
            {
                var extension = "." + file.FileName.Split('.')[file.FileName.Split('.').Length - 1];
                return (extension == ".png" || extension == ".jpg" || extension == ".bmp" || extension == ".gif" || extension == ".tif");
            }
            catch
            {
                return false;
            }
        }

        private async Task<string> WriteFile(IFormFile file)
        {
            string fileName = "";
            try
            {
                var extension = "." + file.FileName.Split('.')[file.FileName.Split('.').Length - 1];
                fileName = DateTime.Now.Ticks + extension; //Create a new Name for the file due to security reasons.
                var pathBuilt = Path.Combine(Directory.GetCurrentDirectory(), "Upload\file");
                if (!Directory.Exists(pathBuilt))
                {
                    Directory.CreateDirectory(pathBuilt);
                }
                var path = Path.Combine(Directory.GetCurrentDirectory(), "Upload\files", fileName);           
                using (var stream = new FileStream(path, FileMode.Create))
                {
                    await file.CopyToAsync(stream);
                }
                return fileName;
            }
            catch (Exception e)
            {
                //
            }
            return fileName;
        }
    }

}

数据库

select * from AspNetUsers;
select * from AspNetUserRoles;
select * from AspNetRoles;

结果

[Authorize(Roles ="ADMIN")]时显示403 Forbidden.

如何使按角色授权工作?

您实际上并没有向令牌添加角色。每个角色都应该是一个声明,像这样。

new Claim(ClaimTypes.Role, "<role name">));

例如

var claims = new[]
{
      new Claim("Email", model.Email),
      new Claim(ClaimTypes.NameIdentifier, user.Id),
};

var roles = await _userManger.GetRolesAsync(user); // note: you have a typo in "_userManger"

var claimsWithRoles = roles.Select(role => new Claim(ClaimTypes.Role, role));
var allClaims = claims.Concat(claimsWithRoles);

然后将allClaims添加到JWT中,像这样:

var token = new JwtSecurityToken(
            issuer: _configuration["AuthSettings:Issuer"],
            audience: _configuration["AuthSettings:Audience"],
            claims: allClaims , // note how we add all claims, including the ones for roles, here
            expires: DateTime.Now.AddDays(30),
            signingCredentials: new SigningCredentials(key, SecurityAlgorithms.HmacSha256));

告诉我它是如何工作的。

编辑:抱歉,忘记了 AddRange returns void;