센로그

13. Authorization 본문

게임/ASP.NET Core

13. Authorization

seeyoun 2024. 4. 15. 23:18

◆ Authorization

authorization은 권한과 관련된 문제이다.

인증을 통과한 사용자라고 하더라도 사용자마다 허용된 권한은 다를 수 있다.

 

ASP.NET Core에서 권한 관련 작업은 필터를 통해 처리된다. 

 

요청 처리 과정 속에서 보면, 다음 시점(4.1)에서 처리된다.

  1. Request
  2. Routing
  3. Authentication 미들웨어 (인증)
  4. MVC 미들웨어
    1. Authorize 필터 적용
    2. Action
    3. View

 

 

Authorization시 사용하는 애트리뷰트는 다음과 같다.

  • [Authorize]
    • 권한이 있는 경우에만 해당 액션 또는 컨트롤러를 사용 가능하도록 함
  • [AllowAnonymous]
    • 필터 특성상, 적용 범위를 세부적으로 설정할 수 있다. (Global, Controller, Action 등)
    • [Authorize] 필터가 붙은 컨트롤러 내부에서, 예외적으로 권한 없이도 접근할 수 있는 액션을 설정하고 싶을 때 사용

 


Authorization Fail

그러면 권한이 없으면 어떻게 될까?

 

권한이 없는 경우, MVC에서는 다음과 같은 IActionResult를 생성하고, 이에 따라 특정 View를 보여준다.

  • ChallengeResult
    • 로그인하지 않은 상태
  • ForbidResult
    • 로그인은 했는데, 권한은 없는 상태

 

WebAPI의 경우, 다음과 같은 Status Code를 반환한다.

  • 401
  • 403

 


Authorization Policy

권한에 관한 정책(Policy)도 설정할 수 있다.

 

 

Authorization Policy

  • Request가 Authorize(권한 부여)되기 위해 필요한 정책
  • [Authorize("정책이름")]을 통해 정책 사용 선언 가능

 

그럼 정책은 어떻게 만들까?

 

Program.cs에다 등록해주면 된다.

builder.Services.AddAuthorization(options =>
{
    //정책 등록
});

 

 

정책을 만드는 몇가지 예제 코드를 살펴보자.

builder.Services.AddAuthorization(options =>
{
    //정책: IsAdmin이라는 클레임이 있어야 한다. (DB의 AspNetUserClaims)
    options.AddPolicy("AdminPolicy", policy => policy.RequireClaim("IsAdmin"));

    //정책: Email 주소가 "grace7040@naver.com"이어야 한다.
    options.AddPolicy("TestPolicy", policy => policy.RequireClaim(ClaimTypes.Email, "grace7040@naver.com"));

    //정책: 인증받은 유저여야 한다. (기본값이긴 함)
    options.AddPolicy("TestPolicy", policy => policy.RequireAuthenticatedUser());

    //여러가지를 복합적으로 체크하고 싶을 때 사용. p는 authorization handler context
    //정책: Email 타입의 클레임을 가지고 있어야 한다.
    options.AddPolicy("TestPolicy", policy => policy.RequireAssertion(
        p => p.User.HasClaim(c => c.Type == ClaimTypes.Email)));
});
  • 만드는 방법이나 기능은 되게 많으니, 필요할 때 구글링해서 사용 ㄱㄱ

 

1. Program.cs에 정책 등록

builder.Services.AddAuthorization(options =>
{
    //정책: IsAdmin이라는 클레임이 있어야 한다. (DB의 AspNetUserClaims)
    options.AddPolicy("AdminPolicy", policy => policy.RequireClaim("IsAdmin"));
});

 

 

2. 클레임 추가

AdminPolicy라는 정책을 만드려면, 실제로 DB에 IsAdmin이라는 클레임을 추가해줘야 할 것이다.

  • Register.cshtml.cs에서 using System.Security.Claims; 를 하고, 새 클레임 추가
//Test
var claim = new Claim("IsAdmin", Input.Email.StartsWith("admin").ToString());
await _userManager.AddClaimAsync(user, claim);  //DB에 클레임 추가
  • 이메일이 admin으로 시작하는 경우 IsAdmin 클레임을 주도록 설정했다.

 

3. 권한을 필요로하는 컨트롤러나 액션에다 Authorize 애트리뷰트를 붙여준다.

  • [Authorize("AdminPolicy")]

테스트를 위해 HomeController의 Privacy 액션에다 AdminPolicy를 추가해줬다.

[Authorize("AdminPolicy")]
public IActionResult Privacy()
{
    return View();
}

 

 

4. 이제 실행을 해보자.

  • Privacy를 누르면, 권한이 필요한 페이지이므로 우선 로그인하라고 뜬다.
  • grace7040@khu.ac.kr 계정으로 로그인 하면, 아직 따로 설정해준 권한이 없기 때문에 Dennied 된다.

 

 

그럼 아까 IsAdmin 클레임을 주는 조건이었던, admin으로 시작하는 계정을 만들어보자.

  • adminSeeyoun@naver.com 이라는 계정을 등록하고, 로그인한다.
  • 이 경우 Privacy 페이지에 접근할 수 있는 것을 확인할 수 있다.

 

 

 

실제로 AspNetUserClaims에 들어가보면, 다음과 같이 IsAdmin권한이 True인 것을 확인할 수 있다.

  • 5~8번까지가 adminSeeyoun@naver.com에 대한 클레임 정보임. (UserId로 식별)

 


CustomPolicy Class

앞에서는 AddAuthorization을 통해 하나씩 AddPolicy를 했었다.

그러나 정책이 복잡해지고 많아질수록 이 방법은 보기 힘들어질 것임

 

따라서, 단순하지 않은 복잡한 정책에 대해서는 CustomPolicy를 클래스로 만들어 관리할 수 있다.

 

 

 

예를들어 클럽 입장 정책을 구성한다고 생각해보자.

다음과 같은 정책에 따라 입장 권한을 부여한다고 하자.

  • 20~30세 or VIP
  • 블랙리스트에 없어야 함 (!IsBanned)

이 클럽 입장 정책을 CustomPolicy 클래스로 구현해보자.

 

 

우선 알아둬야할 개념:

Policy는 쉽게 말해 Requirement(AND) + Handler(OR) 이다.

  • Policy는 하나 이상의 Requirement로 구성
  • Requirement는 하나 이상의 Handler로 구성

 

 

 

이 개념을 기반으로, 클럽 입장 정책은 다음과 같이 구현할 수 있다

  • [CanEnterRequirement]
    • AgeHandler : 20-30세인지 판단
    • IsVipHandler : VIP여부 판단
  • [IsNotBlackListRequirement]
    • IsUnbannedHandler : IsBanned == false인지 판단

 

 

위 명세를 실제로 구현해보자.

1. 우선 Policy 를 위한 Requirement 클래스 및 Handler 클래스를 만들고, 연결해주자.

public class CanEnterRequirement : IAuthorizationRequirement
{
    public int MinAge { get; }
    public int MaxAge { get; }

    public CanEnterRequirement(int minAge, int maxAge)
    {
        MinAge = minAge;
        MaxAge = maxAge;
    }
}

//Ok, Pass, Fail 중 하나를 반환함.
//순서대로 통과, 중립, 실패 를 의미함
public class AgeHandler : AuthorizationHandler<CanEnterRequirement>	//핸들러와 관련된 requirement
{
    //구현해야 하는 메서드
    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, CanEnterRequirement requirement)
    {
        //Age타입을 추출해서 클레임을 가져옴
        Claim claim = context.User.Claims.FirstOrDefault(c => c.Type == "Age");
        if(claim != null)
        {
            int age = int.Parse(claim.Value);

            //MinAge <= age <= MaxAge이면 Succeed
            if(requirement.MinAge <= age && requirement.MaxAge >= age)  
            {
                context.Succeed(requirement);   //Ok
            }
        }

        //Requirement가 만족되지 않았으면 아무것도 안함. (만족된 경우 Succeed()에서 성공 플래그 꽂음)
        return Task.CompletedTask;
    }
}

public class IsVipHandler : AuthorizationHandler<CanEnterRequirement>
{
    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, CanEnterRequirement requirement)
    {
        Claim claim = context.User.Claims.FirstOrDefault(c => c.Type == "IsVip");
        if (claim != null)
        {
            bool vip = bool.Parse(claim.Value);

            if (vip)
            {
                context.Succeed(requirement);   
            }
        }
       
        return Task.CompletedTask;
    }
}

 

public class IsNotBlackListRequirement : IAuthorizationRequirement
{

}

public class IsUnbannedHandler : AuthorizationHandler<IsNotBlackListRequirement>
{
    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, IsNotBlackListRequirement requirement)
    {
        Claim claim = context.User.Claims.FirstOrDefault(c => c.Type == "IsBanned");
        if (claim != null)
        {
            bool ban = bool.Parse(claim.Value);

            if (ban)
            {
                context.Fail(); //Fail : 다른 거 볼 것도 없이 실패
            }
            else
            {
                context.Succeed(requirement);
            }
        }

        return Task.CompletedTask;
    }
}

 

 

2. 그리고 Program.cs에다 Policy 등록 및 DI 등록을 해준다.

builder.Services.AddAuthorization(options =>
{
    //정책 추가
    options.AddPolicy("ClubEnterPolicy", policy =>
    {
    	//Requirement 추가
        policy.AddRequirements(
            new CanEnterRequirement(20, 30),
            new IsNotBlackListRequirement()
            );
    });
});

//DI 추가
builder.Services.AddSingleton<IAuthorizationHandler, AgeHandler>();
builder.Services.AddSingleton<IAuthorizationHandler, IsVipHandler>();
builder.Services.AddSingleton<IAuthorizationHandler, IsUnbannedHandler>();
  • ClubEnterPolicy 라는 이름의 정책을 추가해줌
    • 두가지 Requirement를 추가해줌
  • 각종 핸들러를 DI 해줌

 

3. 마지막으로 해당 정책을 사용할 컨트롤러나 액션에다 [Authorize("정책이름")]을 붙여주면 된다.

[Authorize("ClubEnterPolicy")]
public IActionResult Privacy()
{
    return View();
}

 

 

4. 여러 계정을 사용한 실행 결과는 다음과 같다.

  • 우측 상단에 있는 계정 명을 보자. 나이/vip여부/(밴여부) 로 구성했다.

 

 

 


Authorization 수동 처리

마지막으로 , Authorization 수동 처리에 대해서 알아보자.

 

지금까지는 필터를 사용해서 아예 거부하거나, 아예 통과시키거나 두개 중 하나로 처리를 했었다.

그러지 않고, Action 내부에서 조건에 따라 유동적으로 특정 처리를 하고 싶다면?

  • IAuthorizationService 이용하면 됨

 

다음과 같이 작성하면 앞의 예제와 동일하게 동작한다.

public class HomeController : Controller
{
    private IAuthorizationService _auth;

    public HomeController(IAuthorizationService auth)
    {
        _auth = auth;   
    }

    public async Task<IActionResult> Privacy()
    {
        var result = await _auth.AuthorizeAsync(User, "ClubEnterPolicy");
        if(!result.Succeeded) {
            return new ForbidResult();
        }
        return View();
    }
}

 

 

'게임 > ASP.NET Core' 카테고리의 다른 글

15. Custom Middleware  (0) 2024.04.15
14. Logging  (0) 2024.04.15
12. Authentication  (0) 2024.04.15
11. Filter  (0) 2024.04.15
10. Configuration  (0) 2024.04.15
Comments