직장에서 ASP.NET MVC를 처음 만지면서 개발하는 사람에 대해 그 사람이 만든 코드를 보면서 코드 리뷰와 ASP.NET MVC의 기능에 대해 조언할 기회가 있었기 때문에 그 내용을 간단히 정리해 보았습니다.
상사의 지시였던지라 [MSDN을 참고 하세요.] 로 끝낼 수는 없는 상황이었기에ㅎㅎ....
1. 어플 설정은 배포 환경별로 분리시킨다
개발환경과 스테이징 환경, 운영환경이 각자 같은 키에 값이 다를 경우, 아래와 같이 설정하면 배포 환경별로 각자 맞는 값을 반영시킵니다.
xdt:Transform="Replace" xdt:Locator="Match(key)"
【Web.config】
배포 환경별로 다른 값을 설정합니다(아래 샘플 참고)
Web.config (예) 로컬PC)
<appSettings>
<add key="env" value="Local"/>
</appSettings>
Web.Debug.config (예) 개발환경)
<appSettings>
<add key="env" value="Test" xdt:Transform="Replace" xdt:Locator="Match(key)" />
</appSettings>
Web.Release.config (예) 운영환경)
<appSettings>
<add key="env" value="Live" xdt:Transform="Replace" xdt:Locator="Match(key)" />
</appSettings>
2. Log4net의 설정
Log4net의 설정은 Web.config의 Configuration(Debug/Release) 별로 나눠서 설정 합니다.
Properties\AssemblyInfo.cs
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]
[assembly: log4net.Config.XmlConfigurator(Watch = true)]★추가
Web.config (예) 개발환경)
<log4net debug="true">
<appender name="RollingLogFileAppender" type="log4net.Appender.RollingFileAppender">
<param name="File" value="C:\logs\" />
<param name="AppendToFile" value="true" />
<param name="MaxSizeRollBackups" value="10" />
<param name="RollingStyle" value="date" />
<param name="StaticLogFileName" value="false" />
<param name="DatePattern" value='yyyy-MM-dd".log"' />
<lockingModel type="log4net.Appender.FileAppender+MinimalLock" />
<layout type="log4net.Layout.PatternLayout">
<param name="ConversionPattern" value="%d [%t] %-5p - %m%n" />
</layout>
</appender>
<root>
<level value="DEBUG" />
<appender-ref ref="RollingLogFileAppender" />
</root>
</log4net>
Web.Debug.config/Web.Release.config ※환경에 맞게 설정 합니다.
<log4net debug="true" xdt:Transform="Replace">
<appender name="RollingLogFileAppender" type="log4net.Appender.RollingFileAppender">
<param name="File" value="C:\logs\Test\" />
<param name="AppendToFile" value="true" />
<param name="MaxSizeRollBackups" value="10" />
<param name="RollingStyle" value="date" />
<param name="StaticLogFileName" value="false" />
<param name="DatePattern" value='yyyy-MM-dd".log"' />
<lockingModel type="log4net.Appender.FileAppender+MinimalLock" />
<layout type="log4net.Layout.PatternLayout">
<param name="ConversionPattern" value="%d [%t] %-5p - %m%n" />
</layout>
</appender>
<root>
<level value="DEBUG" />
<appender-ref ref="RollingLogFileAppender" />
</root>
</log4net>
3. Action 호출 전후의 공통 로직과 공통 인증
MVC에선 ActionFilterAttribute를 계승한 커스텀 Attribute의 내부에서 Action이 호출되는 전후의 타이밍에 공통처리를 행하는 것이 가능합니다.
3-1. Action 호출 전후의 공통 로직의 참고
/Filters/KariiInitAttribute.cs (예시)
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
namespace Pj.Kari.Filters
{
// 커스텀 액션 필터의 작성
// https://msdn.microsoft.com/ja-jp/library/dd381609(v=vs.98).aspx
public class KariInitAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
}
public override void OnActionExecuted(ActionExecutedContext filterContext)
{
base.OnActionExecuted(filterContext);
}
}
}
KariInitAttribute 적용 예시
※Controller/Action 단위로 적용 됩니다.
[KariInit]
public class HonyararaController : BaseController
만약 특정의 Controller/Action의 경우엔 필터의 적용을 하고 싶지 않은 경우(Controller단위로 커스텀 Attribute를 적용시킨 경우 등), 아래의 방법으로 현재의 Controller/Action를 판별하여 대상외 처리를 하는 것도 가능합니다.
var currentController = httpContext.Request.RequestContext.RouteData.Values["controller"].ToString();
var currentAction = httpContext.Request.RequestContext.RouteData.Values["action"].ToString();
3-2. 공통 인증의 참고
Action이 호출되기 전에 인증 처리를 시키고 싶을 경우(로그인 유저가 권한을 가지고 있는가의 체크 등)엔 AuthorizeAttribute를 계승한 커스텀 Attribute를 사용하는 것이 바람직 합니다. 적용 예시는 「3-1. Action 호출 전후의 공통 로직의 참고」와 동일합니다.
Override Authorize Attribute in ASP.NET MVC
MyAuthorizeAttribute
public class MyAuthorizeAttribute: AuthorizeAttribute
{
protected override bool AuthorizeCore(HttpContextBase httpContext)
{
var authorized = base.AuthorizeCore(httpContext);
if (!authorized)
{
// The user is not authorized => no need to go any further
return false;
}
// We have an authenticated user, let's get his username
string authenticatedUser = httpContext.User.Identity.Name;
// and check if he has completed his profile
if (!this.IsProfileCompleted(authenticatedUser))
{
// we store some key into the current HttpContext so that
// the HandleUnauthorizedRequest method would know whether it
// should redirect to the Login or CompleteProfile page
httpContext.Items["redirectToCompleteProfile"] = true;
return false;
}
return true;
}
protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
{
if (filterContext.HttpContext.Items.Contains("redirectToCompleteProfile"))
{
var routeValues = new RouteValueDictionary(new
{
controller = "someController",
action = "someAction",
});
filterContext.Result = new RedirectToRouteResult(routeValues);
}
else
{
base.HandleUnauthorizedRequest(filterContext);
}
}
private bool IsProfileCompleted(string user)
{
// You know what to do here => go hit your database to verify if the
// current user has already completed his profile by checking
// the corresponding field
throw new NotImplementedException();
}
}
4. 예외의 집약
ASP.NET MVC에서는 예외를 집약시켜서 처리하는 것이 가능합니다. 또한 유저의 동향을 매번 로그로 출력하는 것도 가능합니다.
아래는 샘플 내용입니다.
Global.asax.cs
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);//active★
RouteConfig.RegisterRoutes(RouteTable.Routes);
}
// Application_Error를 사용할 경우엔 아래 내용을 코멘트 아웃
//protected void Application_Error(object sender, EventArgs e)
//{
// var exception = Server.GetLastError();
// if (exception == null)
// {
// return;
// }
// Log.Err(exception.Message, exception);
//}
App_Start/FilterConfig.cs
public static void RegisterGlobalFilters(GlobalFilterCollection filters)
{
//filters.Add(new HandleErrorAttribute());//MVC Default는 코멘트 아웃
filters.Add(new LogAttribute()); //유저의 움직임을 로그출력
filters.Add(new ExceptionHandleAttribute()); //집약 예외 처리
}
Filters/ExceptionHandleAttribute.cs ※새로 파일 추가
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;
namespace Pj.Kari.Filters
{
public class ExceptionHandleAttribute : HandleErrorAttribute
{
readonly log4net.ILog logger = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
public override void OnException(ExceptionContext exceptionContext)
{
var controllerName = exceptionContext.RouteData.Values["controller"].ToString();
var actionName = exceptionContext.RouteData.Values["action"].ToString();
var exceptionMsg = exceptionContext.Exception.Message;
var exMessage = string.Format("{0}", exceptionMsg);
// 리퍼런스 에러
if (exceptionContext.Exception.GetType() == typeof(NullReferenceException))
{
//
}
// 인증 에러
if (exceptionContext.Exception.GetType() == typeof(HttpAntiForgeryException))
{
exceptionContext.ExceptionHandled = true;
exceptionContext.Result = new RedirectToRouteResult(
new RouteValueDictionary(
new
{
controller = "Home",
action = "Index"
})
);
return;
}
if (exceptionContext.ExceptionHandled)
{
return;
}
// Ajax리퀘스트 에러
if (exceptionContext.HttpContext.Request.IsAjaxRequest())
{
var text = string.Empty;
text = HttpUtility.JavaScriptStringEncode(exceptionMsg);
exceptionContext.Result = new JavaScriptResult()
{
// js를 return시키는 것이 가능
//Script = "alert('에러입니다.');"
};
}
else
{
exceptionContext.Result = new ViewResult()
{
ViewName = "../Error/Error", // ★Ajax리퀘스트 에러시에 표시하고 싶은 공통 에러 페이지
ViewData = new ViewDataDictionary
{
Model = new HandleErrorInfo(exceptionContext.Exception, controllerName, actionName)
}
};
}
var errorMsgList = new List();
var innerExection = exceptionContext.Exception;
while (innerExection != null)
{
errorMsgList.Add(innerExection.Message);
innerExection = innerExection.InnerException;
}
logger.Fatal(string.Format("{1}{0}{2}", Environment.NewLine, exMessage, string.Join(Environment.NewLine, errorMsgList)), exceptionContext.Exception);
exceptionContext.ExceptionHandled = true;
exceptionContext.HttpContext.Response.StatusCode = 500;
HttpContext.Current.Response.TrySkipIisCustomErrors = true;
}
}
}
Filters/LogAttribute.cs ※새로 파일 추가
using log4net;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Security.Claims;
using System.Web;
using System.Web.Mvc;
namespace Pj.Kari.Filters
{
public class LogAttribute : ActionFilterAttribute
{
private ILog log = LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
private Stopwatch stopwatch = new Stopwatch();
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
stopwatch.Reset();
stopwatch.Start();
// 유저 정보를 취득하여 현재 있는 유저가 어떤 조작을 하고 있는지 로그로 기록하는 것이 가능
log.Debug(string.Format("▼Start...{0}.{1}",
// 유저ID
// 유저 이름
filterContext.RouteData.Values["controller"],
filterContext.RouteData.Values["action"]));
}
public override void OnActionExecuted(ActionExecutedContext filterContext)
{
//filterContext.Exception
// 리스폰스 헤더에 캐시 무효화를 추가
var response = filterContext.HttpContext.Response;
response.Cache.SetCacheability(HttpCacheability.NoCache);
base.OnActionExecuted(filterContext);
}
public override void OnResultExecuting(ResultExecutingContext filterContext)
{
base.OnResultExecuting(filterContext);
}
public override void OnResultExecuted(ResultExecutedContext filterContext)
{
// 유저 정보를 취득하여 현재 있는 유저가 어떤 조작을 하고 있는지 로그로 기록하는 것이 가능
stopwatch.Stop();
log.Debug(string.Format("▲End.....{0}.{1}..........took {2}ms",
// 유저ID
// 유저 이름
filterContext.RouteData.Values["controller"],
filterContext.RouteData.Values["action"],
stopwatch.ElapsedMilliseconds));
}
}
}
5. 시큐리티 대책
ASP.NET MVC : 어플 단위에서 설정 가능한 security 대책 방법
이상 입니다.
'개발 > ASP.NET' 카테고리의 다른 글
C# private key로 SSH 접속 후 리모트 내의 MySQL에 접속해서 DB 조작 하기 (0) | 2019.01.28 |
---|---|
C# EPPlus에서 수식설정 변경&값 검색 (0) | 2019.01.28 |
C# : API 호출 방법 (0) | 2019.01.28 |
SQL과 JSON의 이스케이프(escape) 처리 (0) | 2019.01.28 |
ASP.NET MVC : 다국어 대응에 대한 메모 (0) | 2019.01.28 |
댓글