ASP.NET MVC에서는 참 편리한 많은 필터들을 내장하고 있습니다.
그중에 권한에 관련한 AuthorizeAttribute 라는 놈이 있습니다.
메서드나 클래스에 이 어트리뷰트를 선언하면 Role과 UserName에 따라서 권한제어를 할 수 있습니다.
웹응용프로그램에서 이와 같은 일을 하기 위해서는 web.config 파일에 설정을 해 주어야 했습니다만..계층 구조도 맞게끔 설계해야 하고..여간 복잡하지 않았죠.
그러나 ASP.NET MVC 에서는 컨트롤러의 액션 메서드에서 위 어트리뷰트와 권한을 줄 UserName 혹은 Role을 주기만 하면 됩니다.
그러나..이 어트리뷰트 뭔가 문제가 있습니다.
내가 원하는 프로세스는 권한이 없으면 권한 없음이라는 페이지로 리다이렉트 시키는 것입니다.
그러나 이 어트리뷰트는 권한이 없는 사용자의 경우 로그인 페이지로 리다이렉트 시켜버립니다.
마치 인증이 안된것 처럼 말이죠..
만약 Login 액션 메서드에서 인증된 사용자이며 ReturnUrl이라는 파라메터에 url이 있으면 바로 해당 url로 리다이렉트 시키는 코드가 삽입되어 있다면..무한 루프에 빠지게 됩니다..ㅡ.ㅠ;
Authentication과 Authorization이 혼동을 일으키는 듯 합니다..^^;
MSDN을 살펴 보면.
권한이 없는 사용자가 Authorize 특성으로 표시된 메서드에서 액세스하려고 하면 MVC 프레임워크는 401 HTTP 상태 코드를 반환합니다.사이트가 ASP.NET 폼 인증을 사용하도록 구성되어 있으면 401 상태 코드는 브라우저가 사용자를 로그인 페이지로 리디렉션하도록 합니다.
라고 되어 있습니다.
그럼 이 401 에러 코드는 어디서 받을 수 있을까요??? @.@
먼저 AuthorizeAttribute를 reflector로 분해해 보면 아래와 같습니다.
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, Inherited=true, AllowMultiple=true)]
public class AuthorizeAttribute : FilterAttribute, IAuthorizationFilter
{
// Fields
private string _roles;
private string[] _rolesSplit;
private readonly object _typeId;
private string _users;
private string[] _usersSplit;
// Methods
public AuthorizeAttribute();
protected virtual bool AuthorizeCore(HttpContextBase httpContext);
private void CacheValidateHandler(HttpContext context, object data, ref HttpValidationStatus validationStatus);
protected virtual void HandleUnauthorizedRequest(AuthorizationContext filterContext);
public virtual void OnAuthorization(AuthorizationContext filterContext);
protected virtual HttpValidationStatus OnCacheAuthorization(HttpContextBase httpContext);
internal static string[] SplitString(string original);
// Properties
public string Roles { get; set; }
public override object TypeId { get; }
public string Users { get; set; }
}
이 중에서 관심 있게 봐야 하는 메서드는 AuthorizeCore 와 HandleUnauthoziedRequest 와 OnAuthorization 입니다.
하나씩 살펴 보도록 하죠.
protected virtual bool AuthorizeCore(HttpContextBase httpContext)
{
if (httpContext == null)
{
throw new ArgumentNullException("httpContext");
}
IPrincipal user = httpContext.User;
if (!user.Identity.IsAuthenticated)
{
return false;
}
if ((this._usersSplit.Length > 0) && !this._usersSplit.Contains<string>(user.Identity.Name, StringComparer.OrdinalIgnoreCase))
{
return false;
}
if ((this._rolesSplit.Length > 0) && !this._rolesSplit.Any<string>(new Func<string, bool>(user.IsInRole)))
{
return false;
}
return true;
}
먼저 AutorizeCore 메서드는 위와 같습니다.
8행은 인증 여부, 12행은 UserName이 어트리뷰트 선언시에 포함되는지 16행은 Role이 포함되었는지를 판단하여 true/false를 리턴합니다.
이 메서드가 주어진 값으로 true/false를 판단함을 알수 있습니다.
이제 OnAuthorization 메서드를 살펴봅니다.
public virtual void OnAuthorization(AuthorizationContext filterContext)
{
if (filterContext == null)
{
throw new ArgumentNullException("filterContext");
}
if (this.AuthorizeCore(filterContext.HttpContext))
{
HttpCachePolicyBase cache = filterContext.HttpContext.Response.Cache;
cache.SetProxyMaxAge(new TimeSpan(0L));
cache.AddValidationCallback(new HttpCacheValidateHandler(this.CacheValidateHandler), null);
}
else
{
this.HandleUnauthorizedRequest(filterContext);
}
}
이 메서드는 위의 AutorizeCore메서드가 true이면 어떤 작업을 하고..false 라면 HandleUnauthorizedRequest 메서드를 호출함을 알 수 있습니다.
이제 마지막으로 HandleUnauthorizedRequest 메서드를 살펴 봅니다.
protected virtual void HandleUnauthorizedRequest(AuthorizationContext filterContext)
{
filterContext.Result = new HttpUnauthorizedResult();
}
위 메서드는 HttpUnauthorizedResult 라는 클래스를 선언합니다. 헉헉..
그럼 이 놈은 또 뭘까요..
public override void ExecuteResult(ControllerContext context)
{
if (context == null)
{
throw new ArgumentNullException("context");
}
context.HttpContext.Response.StatusCode = 0x191;
}
위와 같이 이 클래스는 Excute에 위처럼 정의해 놓은 ActionResult 클래스입니다.
0x191은 10진수로 401 입니다..
즉 상태코드를 401로 바꾸고 그냥 그대로 리턴합니다.
코드는 여기 까지 입니다.
즉 MSDN에서 말한 것처럼 401을 던지는 것은 맞는 듯 합니다.
그러나 실제로 HttpModule의 EndRequest레서 디버깅을 해보면..302로 떨어집니다.
(현재 Fiddler를 사용할 수 없는 환경이라 Fiddler는 못 보았네요..>.<)
302는 리다이렉트 입니다.
이미 302로 StatusCode가 변환 되어 내려 집니다..허곡..
그럼 401은 어디서 가로 챌수 있을까요...ㅡ.ㅠ;
여기서 부터 구글링을 열심히 해 보았습니다..
역시 리소스는 많은데..결국 근본적인 해결책(401을 가로채는 것)은 없어 보입니다.
많으 블로거들이 CustomerAuthorizeAttribute를 구현 하는 방법으로 우회 하더군요...
아..그런 것인가...그 거밖에 없는 건가...
그런가 봅니다..
public class CustomAuthorizeAttribute : AuthorizeAttribute
{
public override void OnAuthorization(AuthorizationContext filterContext)
{
if (filterContext == null)
{
throw new ArgumentNullException("filterContext");
}
if (this.AuthorizeCore(filterContext.HttpContext))
{
HttpCachePolicyBase cache = filterContext.HttpContext.Response.Cache;
cache.SetProxyMaxAge(new TimeSpan(0L));
cache.AddValidationCallback(new HttpCacheValidateHandler(this.CacheValidateHandler), null);
}
else
{
String[] arr = filterContext.HttpContext.Request.AcceptTypes;
switch (arr[0])
{
case "text/html":
filterContext.Result = new RedirectResult("/Error/HasNotPermission");
break;
case "application/json":
filterContext.Result = JsonCreator.CreateJson((Controller)filterContext.Controller, false, "권한이 없습니다.","알림");
break;
default:
this.HandleUnauthorizedRequest(filterContext);
break;
}
}
}
private void CacheValidateHandler(HttpContext context, object data, ref HttpValidationStatus validationStatus)
{
validationStatus = this.OnCacheAuthorization(new HttpContextWrapper(context));
}
}
위 코드는 그러한 예가 되겠습니다.
17행의 경우 요청에 따라.. 그 결과를 달리 보여 주는 루틴입니다.
저는 ajax를 자주 써서 말이죠..ajax로 서버 콜 하고 json 형식으로 리턴 해주고..클라이언트에서 이 json를 받아 화면에 뿌려주고..뭐 이런 방식 말입니다.
그러한 경우에느 메시지로 보여야 겠지요..
그렇지 않은 경우에는 /Error/HasNotPermission ( 해당 컨트롤러에 정의되어 있어야 합니다..>.<) 로 이동 시킵니다.
이 상입니다..