권한 부여로 보호 되는 사용자 데이터를 사용 하 여 ASP.NET Core 웹 앱 만들기

작성자: Rick AndersonJoe Audette

이 pdf 보기

이 자습서에서는 권한 부여로 보호 되는 사용자 데이터를 사용 하 여 ASP.NET Core 웹 앱을 만드는 방법을 보여 줍니다. 사용자가 만든 인증 (등록) 된 연락처의 목록이 표시 됩니다. 다음 세 가지 보안 그룹이 있습니다.

  • 등록 된 사용자 는 승인 된 모든 데이터를 볼 수 있으며 자신의 데이터를 편집/삭제할 수 있습니다.
  • 관리자 는 연락처 데이터를 승인 하거나 거부할 수 있습니다. 승인 된 연락처만 사용자에 게 표시 됩니다.
  • 관리자 는 데이터를 승인/거부 하 고 편집/삭제할 수 있습니다.

이 문서의 이미지는 최신 템플릿과 정확히 일치 하지 않습니다.

다음 이미지에서는 사용자 Rick ( rick@example.com )가 로그인 되어 있습니다. Rick는 승인 된 연락처만 볼 수 있으며 / Delete Delete 는 / 연락처에 대 한 새 링크를 만듭니다 . Rick에서 만든 마지막 레코드만 편집삭제 링크를 표시 합니다. 관리자 또는 관리자가 상태를 "승인 됨"으로 변경할 때까지 다른 사용자는 마지막 레코드를 볼 수 없습니다.

Rick 로그인을 보여 주는 스크린샷

다음 이미지에서 manager@contoso.com 는 관리자 역할에 로그인 되어 있습니다.

로그인을 보여 주는 스크린샷 manager@contoso.com

다음 그림은 연락처의 관리자 세부 정보 보기를 보여 줍니다.

연락처의 관리자 보기

승인거부 단추는 관리자와 관리자 에게만 표시 됩니다.

다음 이미지에서 admin@contoso.com 는 및 관리자 역할에 로그인 되어 있습니다.

로그인을 보여 주는 스크린샷 admin@contoso.com

관리자에 게는 모든 권한이 있습니다. 연락처를 읽고 편집/삭제 하 고 연락처의 상태를 변경할 수 있습니다.

다음 모델을 스 캐 폴딩 하 여 앱을 만들었습니다 Contact .

public class Contact
{
    public int ContactId { get; set; }
    public string Name { get; set; }
    public string Address { get; set; }
    public string City { get; set; }
    public string State { get; set; }
    public string Zip { get; set; }
    [DataType(DataType.EmailAddress)]
    public string Email { get; set; }
}

이 샘플에는 다음과 같은 권한 부여 처리기가 포함 되어 있습니다.

  • ContactIsOwnerAuthorizationHandler: 사용자가 해당 데이터만 편집할 수 있도록 합니다.
  • ContactManagerAuthorizationHandler: 관리자가 연락처를 승인 하거나 거부할 수 있습니다.
  • ContactAdministratorsAuthorizationHandler: 관리자가 연락처를 승인 또는 거부 하 고 연락처를 편집/삭제할 수 있습니다.

사전 요구 사항

이 자습서는 고급입니다. 다음 사항을 잘 알고 있어야 합니다.

시작 및 완료 된 앱

완성 된 앱을 다운로드 합니다. 보안 기능에 익숙해질 수 있도록 완성 된 앱을 테스트 합니다.

시작 앱

시작 앱을 다운로드 합니다.

앱을 실행 하 고, 연락처 관리자 링크를 탭 하 고, 연락처를 만들고, 편집 하 고, 삭제할 수 있는지 확인 합니다. 시작 앱을 만들려면 스타터 앱 만들기를 참조 하세요.

보안 사용자 데이터

다음 섹션에는 보안 사용자 데이터 앱을 만들기 위한 모든 주요 단계가 나와 있습니다. 완료 된 프로젝트를 참조 하는 것이 유용할 수 있습니다.

사용자에 게 연락처 데이터 연결

사용자 Identity 가 데이터를 편집할 수 있지만 다른 사용자 데이터는 편집할 수 없도록 ASP.NET 사용자 ID를 사용 합니다. OwnerID ContactStatus 모델에 및을 추가 합니다 Contact .

public class Contact
{
    public int ContactId { get; set; }

    // user ID from AspNetUser table.
    public string OwnerID { get; set; }

    public string Name { get; set; }
    public string Address { get; set; }
    public string City { get; set; }
    public string State { get; set; }
    public string Zip { get; set; }
    [DataType(DataType.EmailAddress)]
    public string Email { get; set; }

    public ContactStatus Status { get; set; }
}

public enum ContactStatus
{
    Submitted,
    Approved,
    Rejected
}

OwnerID``AspNetUser데이터베이스에 있는 테이블의 사용자 ID입니다 Identity . Status필드는 일반 사용자가 연락처를 볼 수 있는지 여부를 결정 합니다.

새 마이그레이션을 만들고 데이터베이스를 업데이트 합니다.

dotnet ef migrations add userID_Status
dotnet ef database update

역할 서비스 추가 Identity

역할 서비스를 추가 하려면 Addroles 를 추가 합니다.

public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<ApplicationDbContext>(options =>
        options.UseSqlServer(
            Configuration.GetConnectionString("DefaultConnection")));
    services.AddDefaultIdentity<IdentityUser>(
        options => options.SignIn.RequireConfirmedAccount = true)
        .AddRoles<IdentityRole>()
        .AddEntityFrameworkStores<ApplicationDbContext>();

인증 된 사용자 필요

사용자 인증을 요구 하도록 대체 인증 정책을 설정 합니다.

public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<ApplicationDbContext>(options =>
        options.UseSqlServer(
            Configuration.GetConnectionString("DefaultConnection")));
    services.AddDefaultIdentity<IdentityUser>(
        options => options.SignIn.RequireConfirmedAccount = true)
        .AddRoles<IdentityRole>()
        .AddEntityFrameworkStores<ApplicationDbContext>();

    services.AddRazorPages();

    services.AddAuthorization(options =>
    {
        options.FallbackPolicy = new AuthorizationPolicyBuilder()
            .RequireAuthenticatedUser()
            .Build();
    });

위의 강조 표시 된 코드는 대체 인증 정책을설정 합니다. 대체 인증 정책에서는 Razor 인증 특성이 있는 페이지, 컨트롤러 또는 작업 메서드를 제외 하 고 모든 사용자를 인증 해야 합니다. 예를 들어 Razor 또는를 사용 하는 페이지, 컨트롤러 또는 작업 메서드 [AllowAnonymous] [Authorize(PolicyName="MyPolicy")] 는 대체 인증 정책 대신 적용 된 인증 특성을 사용 합니다.

RequireAuthenticatedUserDenyAnonymousAuthorizationRequirement를 현재 인스턴스에 추가하여 현재 사용자가 인증될 것을 요구합니다.

대체 인증 정책:

  • 는 인증 정책을 명시적으로 지정 하지 않는 모든 요청에 적용 됩니다. 끝점 라우팅을 통해 처리 되는 요청의 경우 권한 부여 특성을 지정 하지 않는 끝점이 여기에 포함 됩니다. 정적 파일과같이 권한 부여 미들웨어 후 다른 미들웨어에서 처리 하는 요청의 경우 모든 요청에 정책을 적용 합니다.

사용자를 인증 하도록 요구 하는 대체 인증 정책을 설정 하면 새로 추가 된 Razor 페이지와 컨트롤러를 보호 합니다. 기본적으로 인증을 요구 하는 것은 특성을 포함 하는 새 컨트롤러 및 페이지에 의존 하는 것 보다 안전 Razor 합니다 [Authorize] .

클래스에는 AuthorizationOptions 도 포함 AuthorizationOptions.DefaultPolicy 됩니다. 는 DefaultPolicy [Authorize] 정책을 지정 하지 않은 경우 특성에 사용 되는 정책입니다. [Authorize] 는와 달리 명명 된 정책을 포함 하지 않습니다 [Authorize(PolicyName="MyPolicy")] .

정책에 대 한 자세한 내용은을 참조 하십시오 ASP.NET Core 정책 기반 권한 부여 .

MVC 컨트롤러 및 Razor 페이지에서 모든 사용자를 인증 하도록 요구 하는 다른 방법은 권한 부여 필터를 추가 하는 것입니다.

public void ConfigureServices(IServiceCollection services)
{

    services.AddDbContext<ApplicationDbContext>(options =>
        options.UseSqlServer(
            Configuration.GetConnectionString("DefaultConnection")));
    services.AddDefaultIdentity<IdentityUser>(
        options => options.SignIn.RequireConfirmedAccount = true)
        .AddRoles<IdentityRole>()
        .AddEntityFrameworkStores<ApplicationDbContext>();

    services.AddRazorPages();

    services.AddControllers(config =>
    {
        // using Microsoft.AspNetCore.Mvc.Authorization;
        // using Microsoft.AspNetCore.Authorization;
        var policy = new AuthorizationPolicyBuilder()
                         .RequireAuthenticatedUser()
                         .Build();
        config.Filters.Add(new AuthorizeFilter(policy));
    });

이전 코드는 권한 부여 필터를 사용 하며, 대체 정책 설정에서는 끝점 라우팅을 사용 합니다. 대체 (fallback) 정책을 설정 하는 것은 모든 사용자를 인증 하도록 요구 하는 기본 방법입니다.

Index Privacy 익명 사용자가 등록 하기 전에 사이트에 대 한 정보를 얻을 수 있도록 및 페이지에 allowanonymous를 추가 합니다.

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Logging;

namespace ContactManager.Pages
{
    [AllowAnonymous]
    public class IndexModel : PageModel
    {
        private readonly ILogger<IndexModel> _logger;

        public IndexModel(ILogger<IndexModel> logger)
        {
            _logger = logger;
        }

        public void OnGet()
        {

        }
    }
}

테스트 계정 구성

SeedData클래스는 두 개의 계정 즉, 관리자와 관리자를 만듭니다. 암호 관리자 도구 를 사용 하 여 이러한 계정에 대 한 암호를 설정 합니다. 프로젝트 디렉터리 ( 프로그램 .cs 를 포함 하는 디렉터리)에서 암호를 설정 합니다.

dotnet user-secrets set SeedUserPW <PW>

강력한 암호를 지정 하지 않으면가 호출 될 때 예외가 throw 됩니다 SeedData.Initialize .

Main테스트 암호를 사용 하도록 업데이트 합니다.

public class Program
{
    public static void Main(string[] args)
    {
        var host = CreateHostBuilder(args).Build();

        using (var scope = host.Services.CreateScope())
        {
            var services = scope.ServiceProvider;

            try
            {
                var context = services.GetRequiredService<ApplicationDbContext>();
                context.Database.Migrate();

                // requires using Microsoft.Extensions.Configuration;
                var config = host.Services.GetRequiredService<IConfiguration>();
                // Set password with the Secret Manager tool.
                // dotnet user-secrets set SeedUserPW <pw>

                var testUserPw = config["SeedUserPW"];

                SeedData.Initialize(services, testUserPw).Wait();
            }
            catch (Exception ex)
            {
                var logger = services.GetRequiredService<ILogger<Program>>();
                logger.LogError(ex, "An error occurred seeding the DB.");
            }
        }

        host.Run();
    }

    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .ConfigureWebHostDefaults(webBuilder =>
            {
                webBuilder.UseStartup<Startup>();
            });
}

테스트 계정을 만들고 연락처를 업데이트 합니다.

Initialize클래스의 메서드를 업데이트 SeedData 하 여 테스트 계정을 만듭니다.

public static async Task Initialize(IServiceProvider serviceProvider, string testUserPw)
{
    using (var context = new ApplicationDbContext(
        serviceProvider.GetRequiredService<DbContextOptions<ApplicationDbContext>>()))
    {
        // For sample purposes seed both with the same password.
        // Password is set with the following:
        // dotnet user-secrets set SeedUserPW <pw>
        // The admin user can do anything

        var adminID = await EnsureUser(serviceProvider, testUserPw, "admin@contoso.com");
        await EnsureRole(serviceProvider, adminID, Constants.ContactAdministratorsRole);

        // allowed user can create and edit contacts that they create
        var managerID = await EnsureUser(serviceProvider, testUserPw, "manager@contoso.com");
        await EnsureRole(serviceProvider, managerID, Constants.ContactManagersRole);

        SeedDB(context, adminID);
    }
}

private static async Task<string> EnsureUser(IServiceProvider serviceProvider,
                                            string testUserPw, string UserName)
{
    var userManager = serviceProvider.GetService<UserManager<IdentityUser>>();

    var user = await userManager.FindByNameAsync(UserName);
    if (user == null)
    {
        user = new IdentityUser {
            UserName = UserName,
            EmailConfirmed = true
        };
        await userManager.CreateAsync(user, testUserPw);
    }

    if (user == null)
    {
        throw new Exception("The password is probably not strong enough!");
    }

    return user.Id;
}

private static async Task<IdentityResult> EnsureRole(IServiceProvider serviceProvider,
                                                              string uid, string role)
{
    IdentityResult IR = null;
    var roleManager = serviceProvider.GetService<RoleManager<IdentityRole>>();

    if (roleManager == null)
    {
        throw new Exception("roleManager null");
    }

    if (!await roleManager.RoleExistsAsync(role))
    {
        IR = await roleManager.CreateAsync(new IdentityRole(role));
    }

    var userManager = serviceProvider.GetService<UserManager<IdentityUser>>();

    var user = await userManager.FindByIdAsync(uid);

    if(user == null)
    {
        throw new Exception("The testUserPw password was probably not strong enough!");
    }
    
    IR = await userManager.AddToRoleAsync(user, role);

    return IR;
}

관리자 사용자 ID와 ContactStatus 연락처를 추가 합니다. 연락처 "제출 됨" 및 "거부 됨" 중 하나를 만듭니다. 모든 연락처에 사용자 ID 및 상태를 추가 합니다. 하나의 연락처만 표시 됩니다.

public static void SeedDB(ApplicationDbContext context, string adminID)
{
    if (context.Contact.Any())
    {
        return;   // DB has been seeded
    }

    context.Contact.AddRange(
        new Contact
        {
            Name = "Debra Garcia",
            Address = "1234 Main St",
            City = "Redmond",
            State = "WA",
            Zip = "10999",
            Email = "debra@example.com",
            Status = ContactStatus.Approved,
            OwnerID = adminID
        },

소유자, 관리자 및 관리자 권한 부여 처리기 만들기

ContactIsOwnerAuthorizationHandler 권한 부여 폴더에 클래스를 만듭니다. 는 ContactIsOwnerAuthorizationHandler 리소스에 대해 작동 하는 사용자가 리소스를 소유 하는지 확인 합니다.

using ContactManager.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Authorization.Infrastructure;
using Microsoft.AspNetCore.Identity;
using System.Threading.Tasks;

namespace ContactManager.Authorization
{
    public class ContactIsOwnerAuthorizationHandler
                : AuthorizationHandler<OperationAuthorizationRequirement, Contact>
    {
        UserManager<IdentityUser> _userManager;

        public ContactIsOwnerAuthorizationHandler(UserManager<IdentityUser> 
            userManager)
        {
            _userManager = userManager;
        }

        protected override Task
            HandleRequirementAsync(AuthorizationHandlerContext context,
                                   OperationAuthorizationRequirement requirement,
                                   Contact resource)
        {
            if (context.User == null || resource == null)
            {
                return Task.CompletedTask;
            }

            // If not asking for CRUD permission, return.

            if (requirement.Name != Constants.CreateOperationName &&
                requirement.Name != Constants.ReadOperationName   &&
                requirement.Name != Constants.UpdateOperationName &&
                requirement.Name != Constants.DeleteOperationName )
            {
                return Task.CompletedTask;
            }

            if (resource.OwnerID == _userManager.GetUserId(context.User))
            {
                context.Succeed(requirement);
            }

            return Task.CompletedTask;
        }
    }
}

ContactIsOwnerAuthorizationHandler호출 컨텍스트입니다. 현재 인증 된 사용자가 연락처 소유자 인 경우 성공 합니다. 권한 부여 처리기 일반적:

  • context.Succeed요구 사항이 충족 되 면를 호출 합니다.
  • Task.CompletedTask요구 사항이 충족 되지 않으면를 반환 합니다. Task.CompletedTask또는에 대 한 이전 호출을 사용 하지 않고 반환 하 context.Success context.Fail 는 것은 성공 또는 실패가 아니므로 다른 권한 부여 처리기를 실행할 수 있습니다.

명시적으로 실패 해야 하는 경우 컨텍스트를 호출 합니다. 실패.

앱에서 연락처 소유자는 자신의 데이터를 편집/삭제/만들 수 있습니다. ContactIsOwnerAuthorizationHandler 는 요구 사항 매개 변수에 전달 된 작업을 확인할 필요가 없습니다.

관리자 권한 부여 처리기 만들기

ContactManagerAuthorizationHandler 권한 부여 폴더에 클래스를 만듭니다. 는 ContactManagerAuthorizationHandler 리소스에 대해 작동 하는 사용자가 관리자 인지 확인 합니다. 관리자만 콘텐츠 변경 내용 (신규 또는 변경 됨)을 승인 또는 거부할 수 있습니다.

using System.Threading.Tasks;
using ContactManager.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Authorization.Infrastructure;
using Microsoft.AspNetCore.Identity;

namespace ContactManager.Authorization
{
    public class ContactManagerAuthorizationHandler :
        AuthorizationHandler<OperationAuthorizationRequirement, Contact>
    {
        protected override Task
            HandleRequirementAsync(AuthorizationHandlerContext context,
                                   OperationAuthorizationRequirement requirement,
                                   Contact resource)
        {
            if (context.User == null || resource == null)
            {
                return Task.CompletedTask;
            }

            // If not asking for approval/reject, return.
            if (requirement.Name != Constants.ApproveOperationName &&
                requirement.Name != Constants.RejectOperationName)
            {
                return Task.CompletedTask;
            }

            // Managers can approve or reject.
            if (context.User.IsInRole(Constants.ContactManagersRole))
            {
                context.Succeed(requirement);
            }

            return Task.CompletedTask;
        }
    }
}

관리자 권한 부여 처리기 만들기

ContactAdministratorsAuthorizationHandler 권한 부여 폴더에 클래스를 만듭니다. 는 ContactAdministratorsAuthorizationHandler 리소스에 대해 작동 하는 사용자가 관리자 인지 확인 합니다. 관리자는 모든 작업을 수행할 수 있습니다.

using System.Threading.Tasks;
using ContactManager.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Authorization.Infrastructure;

namespace ContactManager.Authorization
{
    public class ContactAdministratorsAuthorizationHandler
                    : AuthorizationHandler<OperationAuthorizationRequirement, Contact>
    {
        protected override Task HandleRequirementAsync(
                                              AuthorizationHandlerContext context,
                                    OperationAuthorizationRequirement requirement, 
                                     Contact resource)
        {
            if (context.User == null)
            {
                return Task.CompletedTask;
            }

            // Administrators can do anything.
            if (context.User.IsInRole(Constants.ContactAdministratorsRole))
            {
                context.Succeed(requirement);
            }

            return Task.CompletedTask;
        }
    }
}

권한 부여 처리기 등록

Addscoped를 사용 하 여 종속성 주입 을 위해 Entity Framework Core를 사용 하는 서비스를 등록 해야 합니다. 는 ContactIsOwnerAuthorizationHandler Entity Framework Core를 기반으로 하는 ASP.NET Core을 사용 합니다 Identity . 종속성 주입을 통해에 사용할 수 있도록 서비스 컬렉션에 처리기를 ContactsController 등록 합니다. 끝에 다음 코드를 추가 합니다 ConfigureServices .

public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<ApplicationDbContext>(options =>
        options.UseSqlServer(
            Configuration.GetConnectionString("DefaultConnection")));
    services.AddDefaultIdentity<IdentityUser>(
        options => options.SignIn.RequireConfirmedAccount = true)
        .AddRoles<IdentityRole>()
        .AddEntityFrameworkStores<ApplicationDbContext>();

    services.AddRazorPages();

    services.AddAuthorization(options =>
    {
        options.FallbackPolicy = new AuthorizationPolicyBuilder()
            .RequireAuthenticatedUser()
            .Build();
    });

    // Authorization handlers.
    services.AddScoped<IAuthorizationHandler,
                          ContactIsOwnerAuthorizationHandler>();

    services.AddSingleton<IAuthorizationHandler,
                          ContactAdministratorsAuthorizationHandler>();

    services.AddSingleton<IAuthorizationHandler,
                          ContactManagerAuthorizationHandler>();
}

ContactAdministratorsAuthorizationHandlerContactManagerAuthorizationHandler 는 단일 항목로 추가 됩니다. 이는 EF를 사용 하지 않고 필요한 모든 정보가 메서드의 매개 변수에 단일 항목 때문에 발생 Context HandleRequirementAsync 합니다.

인증 지원

이 섹션에서는 페이지를 업데이트 하 Razor 고 작업 요구 사항 클래스를 추가 합니다.

연락처 작업 요구 사항 클래스를 검토 합니다.

클래스를 검토 ContactOperations 합니다. 이 클래스에는 앱이 지 원하는 요구 사항이 포함 되어 있습니다.

using Microsoft.AspNetCore.Authorization.Infrastructure;

namespace ContactManager.Authorization
{
    public static class ContactOperations
    {
        public static OperationAuthorizationRequirement Create =   
          new OperationAuthorizationRequirement {Name=Constants.CreateOperationName};
        public static OperationAuthorizationRequirement Read = 
          new OperationAuthorizationRequirement {Name=Constants.ReadOperationName};  
        public static OperationAuthorizationRequirement Update = 
          new OperationAuthorizationRequirement {Name=Constants.UpdateOperationName}; 
        public static OperationAuthorizationRequirement Delete = 
          new OperationAuthorizationRequirement {Name=Constants.DeleteOperationName};
        public static OperationAuthorizationRequirement Approve = 
          new OperationAuthorizationRequirement {Name=Constants.ApproveOperationName};
        public static OperationAuthorizationRequirement Reject = 
          new OperationAuthorizationRequirement {Name=Constants.RejectOperationName};
    }

    public class Constants
    {
        public static readonly string CreateOperationName = "Create";
        public static readonly string ReadOperationName = "Read";
        public static readonly string UpdateOperationName = "Update";
        public static readonly string DeleteOperationName = "Delete";
        public static readonly string ApproveOperationName = "Approve";
        public static readonly string RejectOperationName = "Reject";

        public static readonly string ContactAdministratorsRole = 
                                                              "ContactAdministrators";
        public static readonly string ContactManagersRole = "ContactManagers";
    }
}

연락처 페이지에 대 한 기본 클래스 만들기 Razor

연락처 페이지에 사용 되는 서비스를 포함 하는 기본 클래스를 만듭니다 Razor . 기본 클래스는 초기화 코드를 한 위치에 배치 합니다.

using ContactManager.Data;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc.RazorPages;

namespace ContactManager.Pages.Contacts
{
    public class DI_BasePageModel : PageModel
    {
        protected ApplicationDbContext Context { get; }
        protected IAuthorizationService AuthorizationService { get; }
        protected UserManager<IdentityUser> UserManager { get; }

        public DI_BasePageModel(
            ApplicationDbContext context,
            IAuthorizationService authorizationService,
            UserManager<IdentityUser> userManager) : base()
        {
            Context = context;
            UserManager = userManager;
            AuthorizationService = authorizationService;
        } 
    }
}

위의 코드는

  • IAuthorizationService권한 부여 처리기에 액세스 하는 서비스를 추가 합니다.
  • 서비스를 추가 Identity UserManager 합니다.
  • ApplicationDbContext를 추가합니다.

CreateModel 업데이트

기본 클래스를 사용 하도록 create page model 생성자를 업데이트 합니다 DI_BasePageModel .

public class CreateModel : DI_BasePageModel
{
    public CreateModel(
        ApplicationDbContext context,
        IAuthorizationService authorizationService,
        UserManager<IdentityUser> userManager)
        : base(context, authorizationService, userManager)
    {
    }

메서드를 CreateModel.OnPostAsync 다음으로 업데이트 합니다.

  • 모델에 사용자 ID를 추가 Contact 합니다.
  • 권한 부여 처리기를 호출 하 여 사용자에 게 연락처를 만들 수 있는 권한이 있는지 확인 합니다.
public async Task<IActionResult> OnPostAsync()
{
    if (!ModelState.IsValid)
    {
        return Page();
    }

    Contact.OwnerID = UserManager.GetUserId(User);

    // requires using ContactManager.Authorization;
    var isAuthorized = await AuthorizationService.AuthorizeAsync(
                                                User, Contact,
                                                ContactOperations.Create);
    if (!isAuthorized.Succeeded)
    {
        return Forbid();
    }

    Context.Contact.Add(Contact);
    await Context.SaveChangesAsync();

    return RedirectToPage("./Index");
}

IndexModel 업데이트

OnGetAsync승인 된 연락처만 일반 사용자에 게 표시 되도록 메서드를 업데이트 합니다.

public class IndexModel : DI_BasePageModel
{
    public IndexModel(
        ApplicationDbContext context,
        IAuthorizationService authorizationService,
        UserManager<IdentityUser> userManager)
        : base(context, authorizationService, userManager)
    {
    }

    public IList<Contact> Contact { get; set; }

    public async Task OnGetAsync()
    {
        var contacts = from c in Context.Contact
                       select c;

        var isAuthorized = User.IsInRole(Constants.ContactManagersRole) ||
                           User.IsInRole(Constants.ContactAdministratorsRole);

        var currentUserId = UserManager.GetUserId(User);

        // Only approved contacts are shown UNLESS you're authorized to see them
        // or you are the owner.
        if (!isAuthorized)
        {
            contacts = contacts.Where(c => c.Status == ContactStatus.Approved
                                        || c.OwnerID == currentUserId);
        }

        Contact = await contacts.ToListAsync();
    }
}

EditModel 업데이트

사용자가 연락처를 소유 하 고 있는지 확인 하기 위해 권한 부여 처리기를 추가 합니다. 리소스 권한 부여의 유효성을 검사 하는 중이기 때문에 [Authorize] 특성에 충분 하지 않습니다. 특성이 평가 될 때 앱에서 리소스에 액세스할 수 없습니다. 리소스 기반 권한 부여는 필수적 이어야 합니다. 앱이 리소스에 대 한 액세스 권한이 있는 경우 페이지 모델에서 로드 하거나 처리기 자체 내에서 로드 하 여 검사를 수행 해야 합니다. 리소스 키를 전달 하 여 리소스에 자주 액세스 합니다.

public class EditModel : DI_BasePageModel
{
    public EditModel(
        ApplicationDbContext context,
        IAuthorizationService authorizationService,
        UserManager<IdentityUser> userManager)
        : base(context, authorizationService, userManager)
    {
    }

    [BindProperty]
    public Contact Contact { get; set; }

    public async Task<IActionResult> OnGetAsync(int id)
    {
        Contact = await Context.Contact.FirstOrDefaultAsync(
                                             m => m.ContactId == id);

        if (Contact == null)
        {
            return NotFound();
        }

        var isAuthorized = await AuthorizationService.AuthorizeAsync(
                                                  User, Contact,
                                                  ContactOperations.Update);
        if (!isAuthorized.Succeeded)
        {
            return Forbid();
        }

        return Page();
    }

    public async Task<IActionResult> OnPostAsync(int id)
    {
        if (!ModelState.IsValid)
        {
            return Page();
        }

        // Fetch Contact from DB to get OwnerID.
        var contact = await Context
            .Contact.AsNoTracking()
            .FirstOrDefaultAsync(m => m.ContactId == id);

        if (contact == null)
        {
            return NotFound();
        }

        var isAuthorized = await AuthorizationService.AuthorizeAsync(
                                                 User, contact,
                                                 ContactOperations.Update);
        if (!isAuthorized.Succeeded)
        {
            return Forbid();
        }

        Contact.OwnerID = contact.OwnerID;

        Context.Attach(Contact).State = EntityState.Modified;

        if (Contact.Status == ContactStatus.Approved)
        {
            // If the contact is updated after approval, 
            // and the user cannot approve,
            // set the status back to submitted so the update can be
            // checked and approved.
            var canApprove = await AuthorizationService.AuthorizeAsync(User,
                                    Contact,
                                    ContactOperations.Approve);

            if (!canApprove.Succeeded)
            {
                Contact.Status = ContactStatus.Submitted;
            }
        }

        await Context.SaveChangesAsync();

        return RedirectToPage("./Index");
    }
}

DeleteModel 업데이트

권한 부여 처리기를 사용 하 여 사용자에 게 연락처에 대 한 삭제 권한이 있는지 확인 하기 위해 삭제 페이지 모델을 업데이트 합니다.

public class DeleteModel : DI_BasePageModel
{
    public DeleteModel(
        ApplicationDbContext context,
        IAuthorizationService authorizationService,
        UserManager<IdentityUser> userManager)
        : base(context, authorizationService, userManager)
    {
    }

    [BindProperty]
    public Contact Contact { get; set; }

    public async Task<IActionResult> OnGetAsync(int id)
    {
        Contact = await Context.Contact.FirstOrDefaultAsync(
                                             m => m.ContactId == id);

        if (Contact == null)
        {
            return NotFound();
        }

        var isAuthorized = await AuthorizationService.AuthorizeAsync(
                                                 User, Contact,
                                                 ContactOperations.Delete);
        if (!isAuthorized.Succeeded)
        {
            return Forbid();
        }

        return Page();
    }

    public async Task<IActionResult> OnPostAsync(int id)
    {
        var contact = await Context
            .Contact.AsNoTracking()
            .FirstOrDefaultAsync(m => m.ContactId == id);

        if (contact == null)
        {
            return NotFound();
        }

        var isAuthorized = await AuthorizationService.AuthorizeAsync(
                                                 User, contact,
                                                 ContactOperations.Delete);
        if (!isAuthorized.Succeeded)
        {
            return Forbid();
        }

        Context.Contact.Remove(contact);
        await Context.SaveChangesAsync();

        return RedirectToPage("./Index");
    }
}

권한 부여 서비스를 뷰에 삽입 합니다.

현재 UI에는 사용자가 수정할 수 없는 연락처에 대 한 편집 및 삭제 링크가 표시 됩니다.

모든 보기에서 사용할 수 있도록 Pages/_ViewImports 파일에 권한 부여 서비스를 삽입 합니다.

@using Microsoft.AspNetCore.Identity
@using ContactManager
@using ContactManager.Data
@namespace ContactManager.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@using ContactManager.Authorization;
@using Microsoft.AspNetCore.Authorization
@using ContactManager.Models
@inject IAuthorizationService AuthorizationService

위의 태그는 여러 문을 추가 합니다 using .

적절 한 권한이 있는 사용자에 대해서만 렌더링 되도록 Pages/Contacts/Index. s e r v e r/ 삭제 링크를 업데이트 합니다.

@page
@model ContactManager.Pages.Contacts.IndexModel

@{
    ViewData["Title"] = "Index";
}

<h2>Index</h2>

<p>
    <a asp-page="Create">Create New</a>
</p>
<table class="table">
    <thead>
        <tr>
            <th>
                @Html.DisplayNameFor(model => model.Contact[0].Name)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Contact[0].Address)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Contact[0].City)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Contact[0].State)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Contact[0].Zip)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Contact[0].Email)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Contact[0].Status)
            </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
        @foreach (var item in Model.Contact)
        {
            <tr>
                <td>
                    @Html.DisplayFor(modelItem => item.Name)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Address)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.City)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.State)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Zip)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Email)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Status)
                </td>
                <td>
                    @if ((await AuthorizationService.AuthorizeAsync(
                     User, item,
                     ContactOperations.Update)).Succeeded)
                    {
                        <a asp-page="./Edit" asp-route-id="@item.ContactId">Edit</a>
                        <text> | </text>
                    }

                    <a asp-page="./Details" asp-route-id="@item.ContactId">Details</a>

                    @if ((await AuthorizationService.AuthorizeAsync(
                     User, item,
                     ContactOperations.Delete)).Succeeded)
                    {
                        <text> | </text>
                        <a asp-page="./Delete" asp-route-id="@item.ContactId">Delete</a>
                    }
                </td>
            </tr>
        }
    </tbody>
</table>

경고

데이터를 변경할 수 있는 권한이 없는 사용자의 링크를 숨기면 앱이 보호 되지 않습니다. 링크를 숨기면 올바른 링크만 표시 하 여 앱을 보다 사용자에 게 친숙 하 게 만들 수 있습니다. 사용자는 생성 된 Url을 해킹 하 여 자신이 소유 하지 않은 데이터에 대 한 편집 및 삭제 작업을 호출할 수 있습니다. Razor페이지 또는 컨트롤러는 데이터를 보호 하기 위해 액세스 검사를 적용 해야 합니다.

업데이트 세부 정보

관리자가 연락처를 승인 하거나 거부할 수 있도록 세부 정보 보기를 업데이트 합니다.

        @*Precedng markup omitted for brevity.*@
        <dt>
            @Html.DisplayNameFor(model => model.Contact.Email)
        </dt>
        <dd>
            @Html.DisplayFor(model => model.Contact.Email)
        </dd>
        <dt>
            @Html.DisplayNameFor(model => model.Contact.Status)
        </dt>
        <dd>
            @Html.DisplayFor(model => model.Contact.Status)
        </dd>
    </dl>
</div>

@if (Model.Contact.Status != ContactStatus.Approved)
{
    @if ((await AuthorizationService.AuthorizeAsync(
     User, Model.Contact, ContactOperations.Approve)).Succeeded)
    {
        <form style="display:inline;" method="post">
            <input type="hidden" name="id" value="@Model.Contact.ContactId" />
            <input type="hidden" name="status" value="@ContactStatus.Approved" />
            <button type="submit" class="btn btn-xs btn-success">Approve</button>
        </form>
    }
}

@if (Model.Contact.Status != ContactStatus.Rejected)
{
    @if ((await AuthorizationService.AuthorizeAsync(
     User, Model.Contact, ContactOperations.Reject)).Succeeded)
    {
        <form style="display:inline;" method="post">
            <input type="hidden" name="id" value="@Model.Contact.ContactId" />
            <input type="hidden" name="status" value="@ContactStatus.Rejected" />
            <button type="submit" class="btn btn-xs btn-danger">Reject</button>
        </form>
    }
}

<div>
    @if ((await AuthorizationService.AuthorizeAsync(
         User, Model.Contact,
         ContactOperations.Update)).Succeeded)
    {
        <a asp-page="./Edit" asp-route-id="@Model.Contact.ContactId">Edit</a>
        <text> | </text>
    }
    <a asp-page="./Index">Back to List</a>
</div>

세부 정보 페이지 모델을 업데이트 합니다.

public class DetailsModel : DI_BasePageModel
{
    public DetailsModel(
        ApplicationDbContext context,
        IAuthorizationService authorizationService,
        UserManager<IdentityUser> userManager)
        : base(context, authorizationService, userManager)
    {
    }

    public Contact Contact { get; set; }

    public async Task<IActionResult> OnGetAsync(int id)
    {
        Contact = await Context.Contact.FirstOrDefaultAsync(m => m.ContactId == id);

        if (Contact == null)
        {
            return NotFound();
        }

        var isAuthorized = User.IsInRole(Constants.ContactManagersRole) ||
                           User.IsInRole(Constants.ContactAdministratorsRole);

        var currentUserId = UserManager.GetUserId(User);

        if (!isAuthorized
            && currentUserId != Contact.OwnerID
            && Contact.Status != ContactStatus.Approved)
        {
            return Forbid();
        }

        return Page();
    }

    public async Task<IActionResult> OnPostAsync(int id, ContactStatus status)
    {
        var contact = await Context.Contact.FirstOrDefaultAsync(
                                                  m => m.ContactId == id);

        if (contact == null)
        {
            return NotFound();
        }

        var contactOperation = (status == ContactStatus.Approved)
                                                   ? ContactOperations.Approve
                                                   : ContactOperations.Reject;

        var isAuthorized = await AuthorizationService.AuthorizeAsync(User, contact,
                                    contactOperation);
        if (!isAuthorized.Succeeded)
        {
            return Forbid();
        }
        contact.Status = status;
        Context.Contact.Update(contact);
        await Context.SaveChangesAsync();

        return RedirectToPage("./Index");
    }
}

역할에 사용자 추가 또는 제거

에 대 한 자세한 내용은 다음 문제 를 참조 하세요.

  • 사용자가 권한을 제거 합니다. 예를 들어 채팅 앱에서 사용자를 음소거 합니다.
  • 사용자에 게 권한 추가

챌린지와 금지 간의 차이점

이 앱은 인증 된 사용자를 요구하도록 기본 정책을 설정 합니다. 다음 코드는 익명 사용자를 허용 합니다. 익명 사용자는 챌린지 vs 금지 간의 차이점을 표시할 수 있습니다.

[AllowAnonymous]
public class Details2Model : DI_BasePageModel
{
    public Details2Model(
        ApplicationDbContext context,
        IAuthorizationService authorizationService,
        UserManager<IdentityUser> userManager)
        : base(context, authorizationService, userManager)
    {
    }

    public Contact Contact { get; set; }

    public async Task<IActionResult> OnGetAsync(int id)
    {
        Contact = await Context.Contact.FirstOrDefaultAsync(m => m.ContactId == id);

        if (Contact == null)
        {
            return NotFound();
        }

        if (!User.Identity.IsAuthenticated)
        {
            return Challenge();
        }

        var isAuthorized = User.IsInRole(Constants.ContactManagersRole) ||
                           User.IsInRole(Constants.ContactAdministratorsRole);

        var currentUserId = UserManager.GetUserId(User);

        if (!isAuthorized
            && currentUserId != Contact.OwnerID
            && Contact.Status != ContactStatus.Approved)
        {
            return Forbid();
        }

        return Page();
    }
}

위의 코드에서

  • 사용자가 인증 되지 않은 경우 ChallengeResult 이 반환 됩니다. 가 반환 되 면 ChallengeResult 사용자가 로그인 페이지로 리디렉션됩니다.
  • 사용자가 인증 되었지만 권한이 부여 되지 않은 경우 ForbidResult 이 반환 됩니다. 가 반환 되 면 ForbidResult 사용자가 액세스 거부 페이지로 리디렉션됩니다.

완성 된 앱 테스트

시드 된 사용자 계정에 대 한 암호를 아직 설정 하지 않은 경우 비밀 관리자 도구 를 사용 하 여 암호를 설정 합니다.

  • 강력한 암호 선택: 8 개 이상의 문자 및 하나 이상의 대문자, 숫자 및 기호를 사용 합니다. 예를 들어 Passw0rd! 은 강력한 암호 요구 사항을 충족 합니다.

  • 프로젝트의 폴더에서 다음 명령을 실행 합니다 <PW> . 여기서은 암호입니다.

    dotnet user-secrets set SeedUserPW <PW>
    

앱에 연락처가 있는 경우:

  • 테이블의 모든 레코드를 삭제 Contact 합니다.
  • 응용 프로그램을 다시 시작 하 여 데이터베이스를 시드해야 합니다.

완성 된 앱을 테스트 하는 쉬운 방법은 세 가지 브라우저 (또는 incognito/InPrivate 세션)를 시작 하는 것입니다. 한 브라우저에서 새 사용자를 등록 합니다 (예: test@example.com ). 다른 사용자로 각 브라우저에 로그인 합니다. 다음 작업을 확인 합니다.

  • 등록 된 사용자는 승인 된 모든 연락처 데이터를 볼 수 있습니다.
  • 등록 된 사용자는 자신의 데이터를 편집/삭제할 수 있습니다.
  • 관리자는 연락처 데이터를 승인/거부할 수 있습니다. Details보기는 승인거부 단추를 표시 합니다.
  • 관리자는 모든 데이터를 승인/거부 하 고 편집/삭제할 수 있습니다.
사용자 앱에서 시드 옵션
test@example.com 아니요 자신의 데이터를 편집/삭제 합니다.
manager@contoso.com 자신의 데이터를 승인/거부 하 고 편집/삭제 합니다.
admin@contoso.com 모든 데이터를 승인/거부 하 고 편집/삭제 합니다.

관리자의 브라우저에서 연락처를 만듭니다. 관리자 연락처에서 삭제 및 편집에 대 한 URL을 복사 합니다. 이러한 링크를 테스트 사용자의 브라우저에 붙여넣어 테스트 사용자가 이러한 작업을 수행할 수 없는지 확인 합니다.

시작 앱 만들기

  • Razor"ContactManager"라는 Pages 앱 만들기

    • 개별 사용자 계정으로 앱을 만듭니다.
    • 네임스페이스가 샘플에 사용된 네임스페이스와 일치할 수 있도록 이름을 "ContactManager"로 지정합니다.
    • -uld는 SQLite 대신 LocalDB 지정합니다.
    dotnet new webapp -o ContactManager -au Individual -uld
    
  • Models/Contact.cs 추가:

    public class Contact
    {
        public int ContactId { get; set; }
        public string Name { get; set; }
        public string Address { get; set; }
        public string City { get; set; }
        public string State { get; set; }
        public string Zip { get; set; }
        [DataType(DataType.EmailAddress)]
        public string Email { get; set; }
    }
    
  • 모델을 스캐폴드합니다. Contact

  • 초기 마이그레이션을 만들고 데이터베이스를 업데이트합니다.

dotnet add package Microsoft.VisualStudio.Web.CodeGeneration.Design
dotnet tool install -g dotnet-aspnet-codegenerator
dotnet aspnet-codegenerator razorpage -m Contact -udl -dc ApplicationDbContext -outDir Pages\Contacts --referenceScriptLibraries
dotnet ef database drop -f
dotnet ef migrations add initial
dotnet ef database update

명령으로 버그가 발생하는 경우 dotnet aspnet-codegenerator razorpage 이 GitHub 문제를참조하세요.

  • Pages/Shared/_Layout.cshtml 파일에서 ContactManager 앵커를 업데이트합니다.
<a class="navbar-brand" asp-area="" asp-page="/Contacts/Index">ContactManager</a>
  • 연락처를 만들고 편집하고 삭제하여 앱 테스트

데이터베이스 시드

Data 폴더에 SeedData 클래스를 추가합니다.

using ContactManager.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Linq;
using System.Threading.Tasks;

// dotnet aspnet-codegenerator razorpage -m Contact -dc ApplicationDbContext -udl -outDir Pages\Contacts --referenceScriptLibraries

namespace ContactManager.Data
{
    public static class SeedData
    {
        public static async Task Initialize(IServiceProvider serviceProvider, string testUserPw)
        {
            using (var context = new ApplicationDbContext(
                serviceProvider.GetRequiredService<DbContextOptions<ApplicationDbContext>>()))
            {              
                SeedDB(context, "0");
            }
        }        

        public static void SeedDB(ApplicationDbContext context, string adminID)
        {
            if (context.Contact.Any())
            {
                return;   // DB has been seeded
            }

            context.Contact.AddRange(
                new Contact
                {
                    Name = "Debra Garcia",
                    Address = "1234 Main St",
                    City = "Redmond",
                    State = "WA",
                    Zip = "10999",
                    Email = "debra@example.com"
                },
                new Contact
                {
                    Name = "Thorsten Weinrich",
                    Address = "5678 1st Ave W",
                    City = "Redmond",
                    State = "WA",
                    Zip = "10999",
                    Email = "thorsten@example.com"
                },
             new Contact
             {
                 Name = "Yuhong Li",
                 Address = "9012 State st",
                 City = "Redmond",
                 State = "WA",
                 Zip = "10999",
                 Email = "yuhong@example.com"
             },
             new Contact
             {
                 Name = "Jon Orton",
                 Address = "3456 Maple St",
                 City = "Redmond",
                 State = "WA",
                 Zip = "10999",
                 Email = "jon@example.com"
             },
             new Contact
             {
                 Name = "Diliana Alexieva-Bosseva",
                 Address = "7890 2nd Ave E",
                 City = "Redmond",
                 State = "WA",
                 Zip = "10999",
                 Email = "diliana@example.com"
             }
             );
            context.SaveChanges();
        }

    }
}

에서 를 SeedData.Initialize Main 호출합니다.

using ContactManager.Data;
using Microsoft.AspNetCore.Hosting;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System;

namespace ContactManager
{
    public class Program
    {
        public static void Main(string[] args)
        {
            var host = CreateHostBuilder(args).Build();

            using (var scope = host.Services.CreateScope())
            {
                var services = scope.ServiceProvider;

                try
                {
                    var context = services.GetRequiredService<ApplicationDbContext>();
                    context.Database.Migrate();
                    SeedData.Initialize(services, "not used");
                }
                catch (Exception ex)
                {
                    var logger = services.GetRequiredService<ILogger<Program>>();
                    logger.LogError(ex, "An error occurred seeding the DB.");
                }
            }

            host.Run();
        }

        public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .ConfigureWebHostDefaults(webBuilder =>
                {
                    webBuilder.UseStartup<Startup>();
                });
    }
}

앱이 데이터베이스를 시드했는지 테스트합니다. 연락처 DB에 행이 있으면 시드 메서드가 실행되지 않습니다.

이 자습서에서는 권한 부여로 보호되는 사용자 데이터를 사용하여 ASP.NET Core 웹앱을 만드는 방법을 보여줍니다. 인증된(등록된) 사용자가 만든 연락처 목록이 표시됩니다. 세 가지 보안 그룹이 있습니다.

  • 등록된 사용자는 승인된 모든 데이터를 볼 수 있으며 자신의 데이터를 편집/삭제할 수 있습니다.
  • 관리자는 연락처 데이터를 승인하거나 거부할 수 있습니다. 승인된 연락처만 사용자에게 표시됩니다.
  • 관리자는 모든 데이터를 승인/거부 및 편집/삭제할 수 있습니다.

다음 이미지에서는 사용자 Rick( rick@example.com )이 로그인되어 있습니다. Rick은 승인된 연락처만 볼 수 있고 편집 / 삭제 / 연락처에 대한 새 링크 만들기만 볼 수 있습니다. Rick이 만든 마지막 레코드만 편집삭제 링크를 표시합니다. 다른 사용자는 관리자 또는 관리자가 상태를 "승인"으로 변경할 때까지 마지막 레코드를 볼 수 없습니다.

Rick 로그인을 보여주는 스크린샷

다음 이미지에서 manager@contoso.com 은 로그인되고 관리자의 역할로 로그인됩니다.

로그인을 보여주는 스크린샷 manager@contoso.com

다음 이미지는 연락처의 관리자 세부 정보 보기를 보여줍니다.

관리자의 연락처 보기

승인거부 단추는 관리자 및 관리자에 대해서만 표시됩니다.

다음 이미지에서 admin@contoso.com 는 로그인되고 관리자의 역할로 로그인됩니다.

로그인을 보여주는 스크린샷 admin@contoso.com

관리자에게 모든 권한이 있습니다. 연락처를 읽고 편집/삭제하고 연락처 상태를 변경할 수 있습니다.

다음 모델을 스캐폴딩하여 앱을 만들었습니다. Contact

public class Contact
{
    public int ContactId { get; set; }
    public string Name { get; set; }
    public string Address { get; set; }
    public string City { get; set; }
    public string State { get; set; }
    public string Zip { get; set; }
    [DataType(DataType.EmailAddress)]
    public string Email { get; set; }
}

샘플에는 다음과 같은 권한 부여 처리기가 포함되어 있습니다.

  • ContactIsOwnerAuthorizationHandler: 사용자가 자신의 데이터만 편집할 수 있도록 합니다.
  • ContactManagerAuthorizationHandler: 관리자가 연락처를 승인하거나 거부할 수 있습니다.
  • ContactAdministratorsAuthorizationHandler: 관리자가 연락처를 승인 또는 거부하고 연락처를 편집/삭제할 수 있습니다.

사전 요구 사항

이 자습서는 고급입니다. 다음 사항을 잘 알고 있어야 합니다.

시작 및 완료된 앱

완료된 앱을 다운로드합니다. 보안 기능에 익숙해지도록 완성된 앱을 테스트합니다.

시작 앱

시작 앱을 다운로드합니다.

앱을 실행하고 ContactManager 링크를 탭한 다음 연락처를 만들고 편집하고 삭제할 수 있는지 확인합니다.

사용자 데이터 보호

다음 섹션에는 보안 사용자 데이터 앱을 만드는 모든 주요 단계가 있습니다. 완료된 프로젝트를 참조하는 것이 유용할 수 있습니다.

연락처 데이터를 사용자에게 연결

ASP.NET 사용자 ID를 사용하여 Identity 사용자가 데이터를 편집할 수 있지만 다른 사용자 데이터는 편집할 수 없도록 합니다. OwnerID모델에 및 를 ContactStatus 추가합니다. Contact

public class Contact
{
    public int ContactId { get; set; }

    // user ID from AspNetUser table.
    public string OwnerID { get; set; }

    public string Name { get; set; }
    public string Address { get; set; }
    public string City { get; set; }
    public string State { get; set; }
    public string Zip { get; set; }
    [DataType(DataType.EmailAddress)]
    public string Email { get; set; }

    public ContactStatus Status { get; set; }
}

public enum ContactStatus
{
    Submitted,
    Approved,
    Rejected
}

OwnerIDAspNetUser 데이터베이스에 있는 테이블의 사용자 Identity ID입니다. Status필드는 일반 사용자가 연락처를 볼 수 있는지 여부를 결정합니다.

새 마이그레이션을 만들고 데이터베이스를 업데이트합니다.

dotnet ef migrations add userID_Status
dotnet ef database update

에 역할 서비스 추가 Identity

AddRoles를 추가하여 역할 서비스를 추가합니다.

public void ConfigureServices(IServiceCollection services)
{
    services.Configure<CookiePolicyOptions>(options =>
    {
        options.CheckConsentNeeded = context => true;
        options.MinimumSameSitePolicy = SameSiteMode.None;
    });

    services.AddDbContext<ApplicationDbContext>(options =>
        options.UseSqlServer(
            Configuration.GetConnectionString("DefaultConnection")));
    services.AddDefaultIdentity<IdentityUser>().AddRoles<IdentityRole>()
         .AddEntityFrameworkStores<ApplicationDbContext>();

인증된 사용자 필요

기본 인증 정책을 설정하여 사용자의 인증을 요구합니다.

public void ConfigureServices(IServiceCollection services)
{
    services.Configure<CookiePolicyOptions>(options =>
    {
        options.CheckConsentNeeded = context => true;
        options.MinimumSameSitePolicy = SameSiteMode.None;
    });

    services.AddDbContext<ApplicationDbContext>(options =>
        options.UseSqlServer(
            Configuration.GetConnectionString("DefaultConnection")));
    services.AddDefaultIdentity<IdentityUser>().AddRoles<IdentityRole>()
         .AddEntityFrameworkStores<ApplicationDbContext>();

    services.AddMvc(config =>
    {
        // using Microsoft.AspNetCore.Mvc.Authorization;
        // using Microsoft.AspNetCore.Authorization;
        var policy = new AuthorizationPolicyBuilder()
                         .RequireAuthenticatedUser()
                         .Build();
        config.Filters.Add(new AuthorizeFilter(policy));
    })                
       .SetCompatibilityVersion(CompatibilityVersion.Version_2_1);

특성을 사용하여 페이지, 컨트롤러 또는 작업 메서드 수준에서 인증을 옵트아웃할 수 Razor [AllowAnonymous] 있습니다. 사용자를 인증하도록 요구하는 기본 인증 정책을 설정하면 새로 추가된 Razor Pages 및 컨트롤러가 보호됩니다. 기본적으로 필요한 인증은 새 컨트롤러 및 Pages를 사용하여 특성을 포함하는 것보다 더 Razor [Authorize] 안전합니다.

익명 사용자가 등록하기 전에 사이트에 대한 정보를 얻을 수 있도록 인덱스, 정보 및 연락처 페이지에 AllowAnonymous를 추가합니다.

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc.RazorPages;

namespace ContactManager.Pages
{
    [AllowAnonymous]
    public class IndexModel : PageModel
    {
        public void OnGet()
        {

        }
    }
}

테스트 계정 구성

SeedData클래스는 관리자와 관리자라는 두 개의 계정을 만듭니다. 비밀 관리자 도구를 사용하여 이러한 계정에 대한 암호를 설정합니다. 프로젝트 디렉터리(Program.cs를 포함하는 디렉터리)에서 암호를 설정합니다.

dotnet user-secrets set SeedUserPW <PW>

강력한 암호를 지정하지 않으면 가 호출될 때 SeedData.Initialize 예외가 throw됩니다.

테스트 암호를 사용하도록 를 Main 업데이트합니다.

public class Program
{
    public static void Main(string[] args)
    {
        var host = CreateWebHostBuilder(args).Build();

        using (var scope = host.Services.CreateScope())
        {
            var services = scope.ServiceProvider;
            var context = services.GetRequiredService<ApplicationDbContext>();
            context.Database.Migrate();

            // requires using Microsoft.Extensions.Configuration;
            var config = host.Services.GetRequiredService<IConfiguration>();
            // Set password with the Secret Manager tool.
            // dotnet user-secrets set SeedUserPW <pw>

            var testUserPw = config["SeedUserPW"];
            try
            {
                SeedData.Initialize(services, testUserPw).Wait();
            }
            catch (Exception ex)
            {
                var logger = services.GetRequiredService<ILogger<Program>>();
                logger.LogError(ex.Message, "An error occurred seeding the DB.");
            }
        }

        host.Run();
    }

    public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
        WebHost.CreateDefaultBuilder(args)
            .UseStartup<Startup>();
}

테스트 계정 만들기 및 연락처 업데이트

Initialize클래스에서 메서드를 SeedData 업데이트하여 테스트 계정을 만듭니다.

public static async Task Initialize(IServiceProvider serviceProvider, string testUserPw)
{
    using (var context = new ApplicationDbContext(
        serviceProvider.GetRequiredService<DbContextOptions<ApplicationDbContext>>()))
    {
        // For sample purposes seed both with the same password.
        // Password is set with the following:
        // dotnet user-secrets set SeedUserPW <pw>
        // The admin user can do anything

        var adminID = await EnsureUser(serviceProvider, testUserPw, "admin@contoso.com");
        await EnsureRole(serviceProvider, adminID, Constants.ContactAdministratorsRole);

        // allowed user can create and edit contacts that they create
        var managerID = await EnsureUser(serviceProvider, testUserPw, "manager@contoso.com");
        await EnsureRole(serviceProvider, managerID, Constants.ContactManagersRole);

        SeedDB(context, adminID);
    }
}

private static async Task<string> EnsureUser(IServiceProvider serviceProvider,
                                            string testUserPw, string UserName)
{
    var userManager = serviceProvider.GetService<UserManager<IdentityUser>>();

    var user = await userManager.FindByNameAsync(UserName);
    if (user == null)
    {
        user = new IdentityUser { UserName = UserName };
        await userManager.CreateAsync(user, testUserPw);
    }

    return user.Id;
}

private static async Task<IdentityResult> EnsureRole(IServiceProvider serviceProvider,
                                                              string uid, string role)
{
    IdentityResult IR = null;
    var roleManager = serviceProvider.GetService<RoleManager<IdentityRole>>();

    if (roleManager == null)
    {
        throw new Exception("roleManager null");
    }

    if (!await roleManager.RoleExistsAsync(role))
    {
        IR = await roleManager.CreateAsync(new IdentityRole(role));
    }

    var userManager = serviceProvider.GetService<UserManager<IdentityUser>>();

    var user = await userManager.FindByIdAsync(uid);

    if(user == null)
    {
        throw new Exception("The testUserPw password was probably not strong enough!");
    }
    
    IR = await userManager.AddToRoleAsync(user, role);

    return IR;
}

관리자 사용자 ID 및 ContactStatus 를 연락처에 추가합니다. 연락처 중 하나를 "제출됨"과 "거부됨"으로 만듭니다. 모든 연락처에 사용자 ID 및 상태를 추가합니다. 하나의 연락처만 표시됩니다.

public static void SeedDB(ApplicationDbContext context, string adminID)
{
    if (context.Contact.Any())
    {
        return;   // DB has been seeded
    }

    context.Contact.AddRange(
        new Contact
        {
            Name = "Debra Garcia",
            Address = "1234 Main St",
            City = "Redmond",
            State = "WA",
            Zip = "10999",
            Email = "debra@example.com",
            Status = ContactStatus.Approved,
            OwnerID = adminID
        },

소유자, 관리자 및 관리자 권한 부여 처리기 만들기

Authorization 폴더를 만들고 그 ContactIsOwnerAuthorizationHandler 안에 클래스를 만듭니다. 는 ContactIsOwnerAuthorizationHandler 리소스에 대해 작업을 하는 사용자가 리소스를 소유하는지 확인합니다.

using System.Threading.Tasks;
using ContactManager.Data;
using ContactManager.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Authorization.Infrastructure;
using Microsoft.AspNetCore.Identity;

namespace ContactManager.Authorization
{
    public class ContactIsOwnerAuthorizationHandler
                : AuthorizationHandler<OperationAuthorizationRequirement, Contact>
    {
        UserManager<IdentityUser> _userManager;

        public ContactIsOwnerAuthorizationHandler(UserManager<IdentityUser> 
            userManager)
        {
            _userManager = userManager;
        }

        protected override Task
            HandleRequirementAsync(AuthorizationHandlerContext context,
                                   OperationAuthorizationRequirement requirement,
                                   Contact resource)
        {
            if (context.User == null || resource == null)
            {
                // Return Task.FromResult(0) if targeting a version of
                // .NET Framework older than 4.6:
                return Task.CompletedTask;
            }

            // If we're not asking for CRUD permission, return.

            if (requirement.Name != Constants.CreateOperationName &&
                requirement.Name != Constants.ReadOperationName   &&
                requirement.Name != Constants.UpdateOperationName &&
                requirement.Name != Constants.DeleteOperationName )
            {
                return Task.CompletedTask;
            }

            if (resource.OwnerID == _userManager.GetUserId(context.User))
            {
                context.Succeed(requirement);
            }

            return Task.CompletedTask;
        }
    }
}

ContactIsOwnerAuthorizationHandler컨텍스트를 호출합니다. 현재 인증된 사용자가 연락처 소유자인 경우 성공합니다. 권한 부여 처리기는 일반적으로 다음을 수행합니다.

  • 요구 사항이 충족되면 를 context.Succeed 호출합니다.
  • 요구 사항이 충족되지 않으면 를 Task.CompletedTask 반환합니다. 또는 Task.CompletedTask 를 이전에 호출하지 않고 context.Success context.Fail 반환하는 것은 성공 또는 실패가 아니며 다른 권한 부여 처리기를 실행할 수 있습니다.

명시적으로 실패해야 하는 경우 컨텍스트를 호출합니다. 에 실패합니다.

앱을 사용하면 연락처 소유자가 자신의 데이터를 편집/삭제/만들 수 있습니다. ContactIsOwnerAuthorizationHandler 에서는 requirement 매개 변수에 전달된 작업을 확인할 필요가 없습니다.

관리자 권한 부여 처리기 만들기

Authorization ContactManagerAuthorizationHandler 폴더에 클래스를 만듭니다. 는 ContactManagerAuthorizationHandler 리소스에 대해 동작하는 사용자가 관리자인지 확인합니다. 관리자만 콘텐츠 변경(신규 또는 변경)을 승인하거나 거부할 수 있습니다.

using System.Threading.Tasks;
using ContactManager.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Authorization.Infrastructure;
using Microsoft.AspNetCore.Identity;

namespace ContactManager.Authorization
{
    public class ContactManagerAuthorizationHandler :
        AuthorizationHandler<OperationAuthorizationRequirement, Contact>
    {
        protected override Task
            HandleRequirementAsync(AuthorizationHandlerContext context,
                                   OperationAuthorizationRequirement requirement,
                                   Contact resource)
        {
            if (context.User == null || resource == null)
            {
                return Task.CompletedTask;
            }

            // If not asking for approval/reject, return.
            if (requirement.Name != Constants.ApproveOperationName &&
                requirement.Name != Constants.RejectOperationName)
            {
                return Task.CompletedTask;
            }

            // Managers can approve or reject.
            if (context.User.IsInRole(Constants.ContactManagersRole))
            {
                context.Succeed(requirement);
            }

            return Task.CompletedTask;
        }
    }
}

관리자 권한 부여 처리기 만들기

Authorization ContactAdministratorsAuthorizationHandler 폴더에 클래스를 만듭니다. 는 ContactAdministratorsAuthorizationHandler 리소스에 대해 동작하는 사용자가 관리자인지 확인합니다. 관리자는 모든 작업을 수행할 수 있습니다.

using System.Threading.Tasks;
using ContactManager.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Authorization.Infrastructure;

namespace ContactManager.Authorization
{
    public class ContactAdministratorsAuthorizationHandler
                    : AuthorizationHandler<OperationAuthorizationRequirement, Contact>
    {
        protected override Task HandleRequirementAsync(
                                              AuthorizationHandlerContext context,
                                    OperationAuthorizationRequirement requirement, 
                                     Contact resource)
        {
            if (context.User == null)
            {
                return Task.CompletedTask;
            }

            // Administrators can do anything.
            if (context.User.IsInRole(Constants.ContactAdministratorsRole))
            {
                context.Succeed(requirement);
            }

            return Task.CompletedTask;
        }
    }
}

권한 부여 처리기 등록

Entity Framework Core 사용하는 서비스는 AddScoped를 사용하여 종속성 주입을 위해 등록되어야 합니다. 는 ContactIsOwnerAuthorizationHandler Entity Framework Core 기반으로 하는 ASP.NET Core 를 Identity 사용합니다. ContactsController 종속성 주입을통해 에서 사용할 수 있도록 처리기를 서비스 컬렉션에 등록합니다. 의 끝에 다음 코드를 ConfigureServices 추가합니다.

public void ConfigureServices(IServiceCollection services)
{
    services.Configure<CookiePolicyOptions>(options =>
    {
        options.CheckConsentNeeded = context => true;
        options.MinimumSameSitePolicy = SameSiteMode.None;
    });

    services.AddDbContext<ApplicationDbContext>(options =>
        options.UseSqlServer(
            Configuration.GetConnectionString("DefaultConnection")));
    services.AddDefaultIdentity<IdentityUser>().AddRoles<IdentityRole>()
         .AddEntityFrameworkStores<ApplicationDbContext>();

    services.AddMvc(config =>
    {
        // using Microsoft.AspNetCore.Mvc.Authorization;
        // using Microsoft.AspNetCore.Authorization;
        var policy = new AuthorizationPolicyBuilder()
                         .RequireAuthenticatedUser()
                         .Build();
        config.Filters.Add(new AuthorizeFilter(policy));
    })                
       .SetCompatibilityVersion(CompatibilityVersion.Version_2_1);

    // Authorization handlers.
    services.AddScoped<IAuthorizationHandler,
                          ContactIsOwnerAuthorizationHandler>();

    services.AddSingleton<IAuthorizationHandler,
                          ContactAdministratorsAuthorizationHandler>();

    services.AddSingleton<IAuthorizationHandler,
                          ContactManagerAuthorizationHandler>();
}

ContactAdministratorsAuthorizationHandlerContactManagerAuthorizationHandler 는 싱글톤으로 추가됩니다. EF를 사용하지 않고 필요한 모든 정보가 메서드의 매개 변수에 있기 때문에 Context 싱글톤입니다. HandleRequirementAsync

권한 부여 지원

이 섹션에서는 Razor Pages를 업데이트하고 작업 요구 사항 클래스를 추가합니다.

연락처 작업 요구 사항 클래스 검토

ContactOperations클래스를 검토합니다. 이 클래스에는 앱에서 지원하는 요구 사항이 포함됩니다.

using Microsoft.AspNetCore.Authorization.Infrastructure;

namespace ContactManager.Authorization
{
    public static class ContactOperations
    {
        public static OperationAuthorizationRequirement Create =   
          new OperationAuthorizationRequirement {Name=Constants.CreateOperationName};
        public static OperationAuthorizationRequirement Read = 
          new OperationAuthorizationRequirement {Name=Constants.ReadOperationName};  
        public static OperationAuthorizationRequirement Update = 
          new OperationAuthorizationRequirement {Name=Constants.UpdateOperationName}; 
        public static OperationAuthorizationRequirement Delete = 
          new OperationAuthorizationRequirement {Name=Constants.DeleteOperationName};
        public static OperationAuthorizationRequirement Approve = 
          new OperationAuthorizationRequirement {Name=Constants.ApproveOperationName};
        public static OperationAuthorizationRequirement Reject = 
          new OperationAuthorizationRequirement {Name=Constants.RejectOperationName};
    }

    public class Constants
    {
        public static readonly string CreateOperationName = "Create";
        public static readonly string ReadOperationName = "Read";
        public static readonly string UpdateOperationName = "Update";
        public static readonly string DeleteOperationName = "Delete";
        public static readonly string ApproveOperationName = "Approve";
        public static readonly string RejectOperationName = "Reject";

        public static readonly string ContactAdministratorsRole = 
                                                              "ContactAdministrators";
        public static readonly string ContactManagersRole = "ContactManagers";
    }
}

연락처 페이지에 대한 기본 클래스 만들기 Razor

연락처 Pages에 사용되는 서비스를 포함하는 기본 클래스를 Razor 만듭니다. 기본 클래스는 초기화 코드를 한 위치에 넣습니다.

using ContactManager.Data;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc.RazorPages;

namespace ContactManager.Pages.Contacts
{
    public class DI_BasePageModel : PageModel
    {
        protected ApplicationDbContext Context { get; }
        protected IAuthorizationService AuthorizationService { get; }
        protected UserManager<IdentityUser> UserManager { get; }

        public DI_BasePageModel(
            ApplicationDbContext context,
            IAuthorizationService authorizationService,
            UserManager<IdentityUser> userManager) : base()
        {
            Context = context;
            UserManager = userManager;
            AuthorizationService = authorizationService;
        } 
    }
}

위의 코드는

  • 권한 IAuthorizationService 부여 처리기에 액세스할 서비스를 추가합니다.
  • 서비스를 Identity UserManager 추가합니다.
  • ApplicationDbContext를 추가합니다.

CreateModel 업데이트

기본 클래스를 사용하도록 만들기 페이지 모델 생성자를 업데이트합니다. DI_BasePageModel

public class CreateModel : DI_BasePageModel
{
    public CreateModel(
        ApplicationDbContext context,
        IAuthorizationService authorizationService,
        UserManager<IdentityUser> userManager)
        : base(context, authorizationService, userManager)
    {
    }

CreateModel.OnPostAsync메서드를 업데이트하여 다음을 수행합니다.

  • 모델에 사용자 ID를 Contact 추가합니다.
  • 권한 부여 처리기를 호출하여 사용자에게 연락처를 만들 수 있는 권한이 있는지 확인합니다.
public async Task<IActionResult> OnPostAsync()
{
    if (!ModelState.IsValid)
    {
        return Page();
    }

    Contact.OwnerID = UserManager.GetUserId(User);

    // requires using ContactManager.Authorization;
    var isAuthorized = await AuthorizationService.AuthorizeAsync(
                                                User, Contact,
                                                ContactOperations.Create);
    if (!isAuthorized.Succeeded)
    {
        return new ChallengeResult();
    }

    Context.Contact.Add(Contact);
    await Context.SaveChangesAsync();

    return RedirectToPage("./Index");
}

IndexModel 업데이트

OnGetAsync승인된 연락처만 일반 사용자에게 표시되도록 메서드를 업데이트합니다.

public class IndexModel : DI_BasePageModel
{
    public IndexModel(
        ApplicationDbContext context,
        IAuthorizationService authorizationService,
        UserManager<IdentityUser> userManager)
        : base(context, authorizationService, userManager)
    {
    }

    public IList<Contact> Contact { get; set; }

    public async Task OnGetAsync()
    {
        var contacts = from c in Context.Contact
                       select c;

        var isAuthorized = User.IsInRole(Constants.ContactManagersRole) ||
                           User.IsInRole(Constants.ContactAdministratorsRole);

        var currentUserId = UserManager.GetUserId(User);

        // Only approved contacts are shown UNLESS you're authorized to see them
        // or you are the owner.
        if (!isAuthorized)
        {
            contacts = contacts.Where(c => c.Status == ContactStatus.Approved
                                        || c.OwnerID == currentUserId);
        }

        Contact = await contacts.ToListAsync();
    }
}

EditModel 업데이트

권한 부여 처리기를 추가하여 사용자가 연락처를 소유하고 있는지 확인합니다. 리소스 권한 부여의 유효성을 검사하고 있으므로 [Authorize] 특성만으로는 충분하지 않습니다. 특성이 평가될 때 앱은 리소스에 액세스할 수 없습니다. 리소스 기반 권한 부여는 명령적이어야 합니다. 페이지 모델에 로드하거나 처리기 자체 내에서 로드하여 앱이 리소스에 액세스할 수 있게 되면 검사를 수행해야 합니다. 리소스 키를 전달하여 리소스에 자주 액세스합니다.

public class EditModel : DI_BasePageModel
{
    public EditModel(
        ApplicationDbContext context,
        IAuthorizationService authorizationService,
        UserManager<IdentityUser> userManager)
        : base(context, authorizationService, userManager)
    {
    }

    [BindProperty]
    public Contact Contact { get; set; }

    public async Task<IActionResult> OnGetAsync(int id)
    {
        Contact = await Context.Contact.FirstOrDefaultAsync(
                                             m => m.ContactId == id);

        if (Contact == null)
        {
            return NotFound();
        }

        var isAuthorized = await AuthorizationService.AuthorizeAsync(
                                                  User, Contact,
                                                  ContactOperations.Update);
        if (!isAuthorized.Succeeded)
        {
            return new ChallengeResult();
        }

        return Page();
    }

    public async Task<IActionResult> OnPostAsync(int id)
    {
        if (!ModelState.IsValid)
        {
            return Page();
        }

        // Fetch Contact from DB to get OwnerID.
        var contact = await Context
            .Contact.AsNoTracking()
            .FirstOrDefaultAsync(m => m.ContactId == id);

        if (contact == null)
        {
            return NotFound();
        }

        var isAuthorized = await AuthorizationService.AuthorizeAsync(
                                                 User, contact,
                                                 ContactOperations.Update);
        if (!isAuthorized.Succeeded)
        {
            return new ChallengeResult();
        }

        Contact.OwnerID = contact.OwnerID;

        Context.Attach(Contact).State = EntityState.Modified;

        if (contact.Status == ContactStatus.Approved)
        {
            // If the contact is updated after approval, 
            // and the user cannot approve,
            // set the status back to submitted so the update can be
            // checked and approved.
            var canApprove = await AuthorizationService.AuthorizeAsync(User,
                                    contact,
                                    ContactOperations.Approve);

            if (!canApprove.Succeeded)
            {
                contact.Status = ContactStatus.Submitted;
            }
        }

        await Context.SaveChangesAsync();

        return RedirectToPage("./Index");
    }

    private bool ContactExists(int id)
    {
        return Context.Contact.Any(e => e.ContactId == id);
    }
}

DeleteModel 업데이트

권한 부여 처리기를 사용하여 사용자에게 연락처에 대한 삭제 권한이 있는지 확인하도록 삭제 페이지 모델을 업데이트합니다.

public class DeleteModel : DI_BasePageModel
{
    public DeleteModel(
        ApplicationDbContext context,
        IAuthorizationService authorizationService,
        UserManager<IdentityUser> userManager)
        : base(context, authorizationService, userManager)
    {
    }

    [BindProperty]
    public Contact Contact { get; set; }

    public async Task<IActionResult> OnGetAsync(int id)
    {
        Contact = await Context.Contact.FirstOrDefaultAsync(
                                             m => m.ContactId == id);

        if (Contact == null)
        {
            return NotFound();
        }

        var isAuthorized = await AuthorizationService.AuthorizeAsync(
                                                 User, Contact,
                                                 ContactOperations.Delete);
        if (!isAuthorized.Succeeded)
        {
            return new ChallengeResult();
        }

        return Page();
    }

    public async Task<IActionResult> OnPostAsync(int id)
    {
        Contact = await Context.Contact.FindAsync(id);

        var contact = await Context
            .Contact.AsNoTracking()
            .FirstOrDefaultAsync(m => m.ContactId == id);

        if (contact == null)
        {
            return NotFound();
        }

        var isAuthorized = await AuthorizationService.AuthorizeAsync(
                                                 User, contact,
                                                 ContactOperations.Delete);
        if (!isAuthorized.Succeeded)
        {
            return new ChallengeResult();
        }

        Context.Contact.Remove(Contact);
        await Context.SaveChangesAsync();

        return RedirectToPage("./Index");
    }
}

보기에 권한 부여 서비스 삽입

현재 UI에는 사용자가 수정할 수 없는 연락처에 대한 편집 및 삭제 링크가 표시됩니다.

모든 보기에서 사용할 수 있도록 Views/_ViewImports.cshtml 파일에 권한 부여 서비스를 삽입합니다.

@using Microsoft.AspNetCore.Identity
@using ContactManager
@using ContactManager.Data
@namespace ContactManager.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@using ContactManager.Authorization;
@using Microsoft.AspNetCore.Authorization
@using ContactManager.Models
@inject IAuthorizationService AuthorizationService

앞의 태그는 여러 using 문을 추가합니다.

Pages/Contacts/Index.cshtml에서 편집삭제 링크를 업데이트하여 적절한 권한이 있는 사용자만 렌더링되도록 합니다.

@page
@model ContactManager.Pages.Contacts.IndexModel

@{
    ViewData["Title"] = "Index";
}

<h2>Index</h2>

<p>
    <a asp-page="Create">Create New</a>
</p>
<table class="table">
    <thead>
        <tr>
            <th>
                @Html.DisplayNameFor(model => model.Contact[0].Name)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Contact[0].Address)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Contact[0].City)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Contact[0].State)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Contact[0].Zip)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Contact[0].Email)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Contact[0].Status)
            </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
        @foreach (var item in Model.Contact)
        {
            <tr>
                <td>
                    @Html.DisplayFor(modelItem => item.Name)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Address)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.City)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.State)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Zip)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Email)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Status)
                </td>
                <td>
                    @if ((await AuthorizationService.AuthorizeAsync(
                     User, item,
                     ContactOperations.Update)).Succeeded)
                    {
                        <a asp-page="./Edit" asp-route-id="@item.ContactId">Edit</a>
                        <text> | </text>
                    }

                    <a asp-page="./Details" asp-route-id="@item.ContactId">Details</a>

                    @if ((await AuthorizationService.AuthorizeAsync(
                     User, item,
                     ContactOperations.Delete)).Succeeded)
                    {
                        <text> | </text>
                        <a asp-page="./Delete" asp-route-id="@item.ContactId">Delete</a>
                    }
                </td>
            </tr>
        }
    </tbody>
</table>

경고

데이터를 변경할 수 있는 권한이 없는 사용자의 링크를 숨기면 앱이 보호되지 않습니다. 링크를 숨기면 유효한 링크만 표시하여 앱의 사용자에게 더 친숙해집니다. 사용자는 생성된 URL을 해킹하여 소유하지 않은 데이터에 대한 편집 및 삭제 작업을 호출할 수 있습니다. Razor페이지 또는 컨트롤러는 액세스 검사를 적용하여 데이터를 보호해야 합니다.

세부 정보 업데이트

관리자가 연락처를 승인하거나 거부할 수 있도록 세부 정보 보기를 업데이트합니다.

        @*Precedng markup omitted for brevity.*@
        <dt>
            @Html.DisplayNameFor(model => model.Contact.Email)
        </dt>
        <dd>
            @Html.DisplayFor(model => model.Contact.Email)
        </dd>
        <dt>
            @Html.DisplayNameFor(model => model.Contact.Status)
        </dt>
        <dd>
            @Html.DisplayFor(model => model.Contact.Status)
        </dd>
    </dl>
</div>

@if (Model.Contact.Status != ContactStatus.Approved)
{
    @if ((await AuthorizationService.AuthorizeAsync(
     User, Model.Contact, ContactOperations.Approve)).Succeeded)
    {
        <form style="display:inline;" method="post">
            <input type="hidden" name="id" value="@Model.Contact.ContactId" />
            <input type="hidden" name="status" value="@ContactStatus.Approved" />
            <button type="submit" class="btn btn-xs btn-success">Approve</button>
        </form>
    }
}

@if (Model.Contact.Status != ContactStatus.Rejected)
{
    @if ((await AuthorizationService.AuthorizeAsync(
     User, Model.Contact, ContactOperations.Reject)).Succeeded)
    {
        <form style="display:inline;" method="post">
            <input type="hidden" name="id" value="@Model.Contact.ContactId" />
            <input type="hidden" name="status" value="@ContactStatus.Rejected" />
            <button type="submit" class="btn btn-xs btn-success">Reject</button>
        </form>
    }
}

<div>
    @if ((await AuthorizationService.AuthorizeAsync(
         User, Model.Contact,
         ContactOperations.Update)).Succeeded)
    {
        <a asp-page="./Edit" asp-route-id="@Model.Contact.ContactId">Edit</a>
        <text> | </text>
    }
    <a asp-page="./Index">Back to List</a>
</div>

세부 정보 페이지 모델을 업데이트합니다.

public class DetailsModel : DI_BasePageModel
{
    public DetailsModel(
        ApplicationDbContext context,
        IAuthorizationService authorizationService,
        UserManager<IdentityUser> userManager)
        : base(context, authorizationService, userManager)
    {
    }

    public Contact Contact { get; set; }

    public async Task<IActionResult> OnGetAsync(int id)
    {
        Contact = await Context.Contact.FirstOrDefaultAsync(m => m.ContactId == id);

        if (Contact == null)
        {
            return NotFound();
        }

        var isAuthorized = User.IsInRole(Constants.ContactManagersRole) ||
                           User.IsInRole(Constants.ContactAdministratorsRole);

        var currentUserId = UserManager.GetUserId(User);

        if (!isAuthorized 
            &&  currentUserId != Contact.OwnerID
            && Contact.Status != ContactStatus.Approved) 
        {
            return new ChallengeResult();
        }

        return Page();
    }

    public async Task<IActionResult> OnPostAsync(int id, ContactStatus status)
    {
        var contact = await Context.Contact.FirstOrDefaultAsync(
                                                  m => m.ContactId == id);

        if (contact == null)
        {
            return NotFound();
        }

        var contactOperation = (status == ContactStatus.Approved)
                                                   ? ContactOperations.Approve
                                                   : ContactOperations.Reject;

        var isAuthorized = await AuthorizationService.AuthorizeAsync(User, contact,
                                    contactOperation);
        if (!isAuthorized.Succeeded)
        {
            return new ChallengeResult();
        }
        contact.Status = status;
        Context.Contact.Update(contact);
        await Context.SaveChangesAsync();

        return RedirectToPage("./Index");
    }
}

역할에 사용자 추가 또는 제거

자세한 내용은 이 문제를 참조하세요.

  • 사용자로부터 권한 제거 예를 들어 채팅 앱에서 사용자를 음소거합니다.
  • 사용자에게 권한 추가

완료된 앱 테스트

시드된 사용자 계정에 대한 암호를 아직 설정하지 않은 경우 비밀 관리자 도구를 사용하여 암호를 설정합니다.

  • 강력한 암호 선택: 8자 이상의 문자와 하나 이상의 대문자, 숫자 및 기호를 사용합니다. 예를 들어 는 Passw0rd! 강력한 암호 요구 사항을 충족합니다.

  • 프로젝트의 폴더에서 다음 명령을 실행합니다. 여기서 <PW> 는 암호입니다.

    dotnet user-secrets set SeedUserPW <PW>
    
  • 데이터베이스 삭제 및 업데이트

    dotnet ef database drop -f
    dotnet ef database update  
    
  • 앱을 다시 시작하여 데이터베이스를 시드합니다.

완료된 앱을 테스트하는 쉬운 방법은 세 가지 브라우저(또는 incognito/InPrivate 세션)를 시작하는 것입니다. 한 브라우저에서 새 사용자(예: test@example.com )를 등록합니다. 다른 사용자로 각 브라우저에 로그인합니다. 다음 작업을 확인합니다.

  • 등록된 사용자는 승인된 연락처 데이터를 모두 볼 수 있습니다.
  • 등록된 사용자는 자신의 데이터를 편집/삭제할 수 있습니다.
  • 관리자는 연락처 데이터를 승인/거부할 수 있습니다. Details보기에 승인거부 단추가 표시됩니다.
  • 관리자는 모든 데이터를 승인/거부 및 편집/삭제할 수 있습니다.
사용자 앱에서 시드 옵션
test@example.com 아니요 자체 데이터를 편집/삭제합니다.
manager@contoso.com 자체 데이터를 승인/거부 및 편집/삭제합니다.
admin@contoso.com 모든 데이터를 승인/거부 및 편집/삭제합니다.

관리자의 브라우저에서 연락처를 만듭니다. 관리자 연락처에서 삭제 및 편집을 위한 URL을 복사합니다. 테스트 사용자의 브라우저에 이러한 링크를 붙여넣어 테스트 사용자가 이러한 작업을 수행할 수 없는지 확인합니다.

시작 앱 만들기

  • Razor"ContactManager"라는 Pages 앱 만들기

    • 개별 사용자 계정으로 앱을 만듭니다.
    • 네임스페이스가 샘플에 사용된 네임스페이스와 일치할 수 있도록 이름을 "ContactManager"로 지정합니다.
    • -uld는 SQLite 대신 LocalDB 지정합니다.
    dotnet new webapp -o ContactManager -au Individual -uld
    
  • Models/Contact.cs를 추가합니다.

    public class Contact
    {
        public int ContactId { get; set; }
        public string Name { get; set; }
        public string Address { get; set; }
        public string City { get; set; }
        public string State { get; set; }
        public string Zip { get; set; }
        [DataType(DataType.EmailAddress)]
        public string Email { get; set; }
    }
    
  • 모델을 스캐폴드합니다. Contact

  • 초기 마이그레이션을 만들고 데이터베이스를 업데이트합니다.

    dotnet aspnet-codegenerator razorpage -m Contact -udl -dc ApplicationDbContext -outDir Pages\Contacts --referenceScriptLibraries
    dotnet ef database drop -f
    dotnet ef migrations add initial
    dotnet ef database update
    
  • Pages/_Layout.cshtml 파일에서 ContactManager 앵커를 업데이트합니다.

    <a asp-page="/Contacts/Index" class="navbar-brand">ContactManager</a>
    
  • 연락처를 만들고 편집하고 삭제하여 앱 테스트

데이터베이스 시드

Data 폴더에 SeedData 클래스를 추가합니다.

에서 를 SeedData.Initialize Main 호출합니다.

public class Program
{
    public static void Main(string[] args)
    {
        var host = CreateWebHostBuilder(args).Build();

        using (var scope = host.Services.CreateScope())
        {
            var services = scope.ServiceProvider;

            try
            {
                var context = services.GetRequiredService<ApplicationDbContext>();
                context.Database.Migrate();
                SeedData.Initialize(services, "not used");
            }
            catch (Exception ex)
            {
                var logger = services.GetRequiredService<ILogger<Program>>();
                logger.LogError(ex, "An error occurred seeding the DB.");
            }
        }

        host.Run();
    }

    public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
        WebHost.CreateDefaultBuilder(args)
            .UseStartup<Startup>();
}

앱이 데이터베이스를 시드했는지 테스트합니다. 연락처 DB에 행이 있으면 시드 메서드가 실행되지 않습니다.

추가 리소스