2014年11月5日 星期三

自定AuthenticationFilter實現Basic認證

HTTP基本認證
當沒有訪問需要認證(還沒有認證通過)的網站時,瀏覽器自動跳出登入對話框



自定一個 Filter:
public class MyAutheticationAttribute : FilterAttributeIAuthenticationFilter
    {
        /* basic 驗證的 http header
         *     401 Unauthorizted
         *         WWW-Authenticate: Basic realm="Secure Area"
         *     請求登入
         *         Authorization: Basic {base64編碼過的登錄信息}
         */
        public const string AuthoriztionHeaderName = "Authorization";
        public const string WwwAuthenticationHeaderName = "WWW-Authenticate";
        public const string BasicAuthenticationScheme = "Basic";
        private static Dictionary<stringstring> userAccounters;
 
        static MyAutheticationAttribute()
        {
            // 為了簡單測試起見,將一些帳號密碼存放到一個靜態欄位中
            userAccounters = new Dictionary<stringstring>(StringComparer.OrdinalIgnoreCase);
            userAccounters.Add("Ian""1234");
            userAccounters.Add("Peter""1111");
            userAccounters.Add("Jay""2222");
        }
 
        // 具體的認證實現
        public void OnAuthentication(AuthenticationContext filterContext)
        {
            IPrincipal user;
            if (this.IsAuthenticated(filterContext, out user))
            {
                filterContext.Principal = user;
            }
            else
            {
                this.ProcessUnauthenticatedRequest(filterContext);
            }
        }
 
        //
        protected virtual AuthenticationHeaderValue GetAuthenticationHeaderValue(AuthenticationContext filterContext)
        {
            // Authorization 內容
            string rawValue = filterContext.RequestContext.HttpContext.Request.Headers[AuthoriztionHeaderName];
            if (string.IsNullOrEmpty(rawValue))
            {
                return null;
            }
            string[] split = rawValue.Split(' ');
            if (split.Length != 2)
            {
                return null;
            }
            return new AuthenticationHeaderValue(split[0], split[1]);
        }
 
        // 判斷是否通過驗證
        protected virtual bool IsAuthenticated(AuthenticationContext filterContext, out IPrincipal user)
        {
            user = filterContext.Principal;
            if (null != user && user.Identity.IsAuthenticated)
            {
                return true;
            }
            AuthenticationHeaderValue token = this.GetAuthenticationHeaderValue(filterContext);
            if (null != token && token.Scheme == BasicAuthenticationScheme) // "Basic"
            {
                string credential = Encoding.Default.GetString(Convert.FromBase64String(token.Parameter));
                string[] split = credential.Split(':'); // {UserNamr}:{Password}
                if (split.Length == 2)
                {
                    string userName = split[0];
                    string password;
                    if (userAccounters.TryGetValue(userName, out password))
                    {
                        if (password == split[1])
                        {
                            // 認證成功的情況下得到代表請求用戶的 Principal 對象
                            GenericIdentity identiry = new GenericIdentity(userName);
                            user = new GenericPrincipal(identiry, new string[0]);
                            return true;
                        }
                    }
                }
            }
 
            return false;
        }
 
        // 沒有通過請求
        protected virtual void ProcessUnauthenticatedRequest(AuthenticationContext filterContext)
        {
            string parameter = string.Format("realm=\"{0}\"", filterContext.RequestContext.HttpContext.Request.Url.DnsSafeHost);
            AuthenticationHeaderValue challenge = new AuthenticationHeaderValue(BasicAuthenticationScheme, parameter);
            filterContext.HttpContext.Response.Headers[WwwAuthenticationHeaderName] = challenge.ToString();
            filterContext.Result = new HttpUnauthorizedResult();
        }
 
        public void OnAuthenticationChallenge(AuthenticationChallengeContext filterContext)
        {
            // throw new NotImplementedException();
        }
    }

應用在Controller:
    [MyAuthetication]
public class HomeController : Controller
{
    public void Index()
    {
        Response.Write(string.Format("Controller.User: {0}<br/>"this.User.Identity.Name));
        Response.Write(string.Format("HttpContext.User: {0}<br/>"this.ControllerContext.HttpContext.User.Identity.Name));
        Response.Write(string.Format("Thread.CurrentPrincipal.Identity.Name: {0}<br/>"Thread.CurrentPrincipal.Identity.Name));
    }
}



2014年11月4日 星期二

非同步Action

非同步Action方法有兩種定義方式
1:XxxAsync/XxxCompleted
public class HomeController : AsyncController
{
    // GET : /Home/Article (在繼承自AsyncController裡的Controller才能使用Article當Action名稱)
    // 非同步操作
    public void ArticleAsync()
    {
        // 開始非同步的時候,先使用 AsyncManager.OutstandingOperations 取得一個 OperationCounter 對象,
        // 然後調用 Increment() 方法向系統發一個非同步操作開始的通知,OperationCounter 會 +1
        // 結束的時候調用 Decrement(),OperationCounter 會 -1,在發法結束時,當OperationCounter 為 0 才會自動調用 ArticleCompleted() 方法

        AsyncManager.OutstandingOperations.Increment();
        string path = ControllerContext.HttpContext.Server.MapPath(string.Format(@"\Project_Readme.html"));
        StreamReader reader = new StreamReader(path);
        reader.ReadToEndAsync().ContinueWith(Task =>
        {
            AsyncManager.Parameters["content"= Task.Result; // 保存要給 ArticleCompleted() 方法的參數,必須與參數同名("content")才能自動匹配
            AsyncManager.OutstandingOperations.Decrement(); // 自動調用 ArticleCompleted() 方法
            reader.Close();
        });
    }
 
    // 非同步最終請求的響應,ArticleAsync 方法的回調
    // content 參數來自 AsyncManager.Parameters["content"]
    public ActionResult ArticleCompleted(string content)
    {
        return Content(content);
    }
}
以 XxxAsync/XxxCompleted 方式定義非同步 Action 方法,表示說必需得為一個 Action 定義兩個方法(實際上可以通過一個方法完成 Action 非同步的定義,那就是讓 Action 方法返回一個代表非同步操作的 Task 對象)。

這方式只能出現在繼承自 AsyncController 的 Controller 中,但是 Task 方式沒有限制(保留 AsyncController 這個抽象類主要是為了實現對 ASP.NET MVC 3 的向後兼容)。

2:Task
public Task<ActionResult> Article()
{
    string path = ControllerContext.HttpContext.Server.MapPath(string.Format(@"\Project_Readme.html"));
    StreamReader reader = new StreamReader(path);
    return reader.ReadToEndAsync().ContinueWith<ActionResult>(task =>
    {
        reader.Close();
        return Content(task.Result);
    });
}

由於 Action 返回一個 Task<ActionResult> 對象,所以可以利用 async/await 關鍵字 (C# 5.0) 直接將其標示為非同步


public async Task<ActionResult> Article()
{
    string path = ControllerContext.HttpContext.Server.MapPath(string.Format(@"\Project_Readme.html"));
    using (StreamReader reader = new StreamReader(path))
    {
        return Content(await reader.ReadToEndAsync());
    }
}



2014年11月2日 星期日

IController、ControllerBase 和 Controller

當在 ASP.NET MVC 專案建立一個 Controller 時,Visual Studio 會產生一個以 "Controller" 為結尾並繼承 Controller 抽象類別的新類別,例如:
HomeController : Controller

以下為繼承關係
Controller 抽象類別 --> ControllerBase 抽象類別 --> IController 介面

IController
唯一的目的在 Controller 接收請求時,Execute() 方法會執行一些code
// 摘要: 
//     定義控制器所需的方法。
public interface IController
{
    // 摘要: 
    //     執行指定的要求內容。
    //
    // 參數: 
    //   requestContext:
    //     要求內容。
    void Execute(RequestContext requestContext);
}

Execute() 方法在 MvcHandler 的 ProcessRequest() 方法中被調用;Execute() 方法接收 RequestContext 類別參數,RequestContext 類別封裝了 RouteData(在UrlRoutingModule中解析Http產生的路由數據)與 HttpContext 用來表示當前請求的上下文。


ControllerBase
定義了 Controller 使用到的部分公共屬性,比如:用來保存臨時數據的 TempData,用來返回到 View 中的 Model 數據對象 ViewBag、ViewData;並且初始化了ControllerContext 對象,用來作為後續Controller使用的數據容器和操作上下文


public abstract class ControllerBase : IController
{
    //省略其他成員
 
    protected virtual void Execute(RequestContext requestContext)
    {
        if (requestContext == null)
        {
            throw new ArgumentNullException("requestContext");
        }
        if (requestContext.HttpContext == null)
        {
            throw new ArgumentException(MvcResources.ControllerBase_CannotExecuteWithNullHttpContext, "requestContext");
        }
 
        VerifyExecuteCalledOnce();
        Initialize(requestContext);
 
        using (ScopeStorage.CreateTransientScope())
        {
            //執行ExecuteCore方法
            ExecuteCore();
        }
    }
    // 該方法在衍生的Controller類中實現
    protected abstract void ExecuteCore();
 
    protected virtual void Initialize(RequestContext requestContext)
    {
        ControllerContext = new ControllerContext(requestContext, this);
    }
    internal void VerifyExecuteCalledOnce()
    {
        if (!_executeWasCalledGate.TryEnter())
        {
            string message = String.Format(CultureInfo.CurrentCulture,
              MvcResources.ControllerBase_CannotHandleMultipleRequests, GetType());
            throw new InvalidOperationException(message);
        }
    }
    void IController.Execute(RequestContext requestContext)
    {
        Execute(requestContext);
    }
}

ControllerBase 實作了 Execute() 方法,負責建立 ControllerContext(提供MVC當前請求的具體上下文),ControllerBase 以“顯式介面實現”的方式定義了Execute() 方法,該方法在內部直接調用受保護的 Execute() 虛方法,而後者最終會調用抽象方法 ExecuteCore() 方法。

Controller 繼承自 ControllerBase 並實現抽象方法ExecuteCore()

2014年10月20日 星期一

利用範例理解 OWIN


自定web server的範例中,可看出接收客服請求可不用依賴於IIS,,本文中會使用OWIN來接管IIS請求和自行託管(self-hosting)

OWIN(Open Web Interface for .Net)是一套定義,它本身不具備任何代碼,目的是為了讓網站應用程式和網站伺服器解除依賴關係

在一個 Http 請求在進入 IIS (7.0 集成模式管道模型)後,一直到返回response之間會經歷許多事件,會有很多的Http Module專門處理這些事件,例如FormsAuthenticationModule就是註冊了AuthenticateRequest事件,也可以開發自己的 Http Module 處理一些事件,通過 web.config 把我們自定義的 Http Module 註冊進 IIS

ASP.NET 多數Modules默認全部開啟,如果不使用它們,這些Module是需要手動在config文件裡面移除的。但是大多數情況下並不會想到去移除他們,這其實是一個性能上的損失。

有了OWin之後,就不再是與ASP.NET 管道打交道了,而是OWin的管道,但是這個管道相對於ASP.NET 管道而言更靈活,更開放


Demo 1 : 讓OWin接管IIS的請求

1. 建立一個空的MVC網站
2. 從Nuget中添加Microsoft.Owin.Host.SystemWeb
這個dll可以讓OWin接管IIS的請求,雖然同樣是託管在IIS,但是所有的請求都會被OWin來處理。在OWin的4層結構中(Applicaton->Middleware->Server->Host),Microsoft.Owin.Host.SystemWeb屬於Server層,還有一個同樣也在Server層的是Microsoft.Owin.Host.HttpListener,這個可以實現利用控制台程序現實自託管,就可以完全擺脫IIS了
3. 添加Startup類
要使用Owin的應用程序都要有一個叫Startup的類,在這個類裡面有一個Configuration的方法,這兩個名字是默認約定,必須用同樣的名字才會被Owin找到。
public class Startup
{
    /// <summary>
    /// 自行定義管線
    /// </summary>
    /// <param name="app"></param>
    public void Configuration(IAppBuilder app) // IAppBuilder using "Owin"
    {
        // 通過 Use 方法來添加自己的管線的處理步驟
        app.Use(async (context, next) => {
            await context.Response.WriteAsync("Authentication... ");
            await next();
        });
        app.Use(async (context, next) =>
        {
            await context.Response.WriteAsync("Authorization... ");
            await next();
        });
        app.Use(async (context, next) =>
        {
            context.Response.ContentType = "text/plain";
            await context.Response.WriteAsync("Hello World!");
        });
    }
}

在web.config中加入一個配置,讓OWin處理所有的請求
<?xml version="1.0" encoding="utf-8"?>
<!--
  如需如何設定 ASP.NET 應用程式的詳細資訊,請造訪
  http://go.microsoft.com/fwlink/?LinkId=169433
  -->
<configuration>
  <system.web>
    <compilation debug="true" targetFramework="4.5" />
    <httpRuntime targetFramework="4.5" />
  </system.web>
  <appSettings>
    <!--讓 OWin 處理所有請求 -->
    <add key="owin:HandleAllRequests" value="true" />
  </appSettings>
</configuration>

不管我們輸入什麼URL,都會返回同樣的結果,因為不管哪個URL,對應的都是我們上面所寫的代碼。

上面的網站我們依舊是託管在IIS中的,現在想要擺脫IIS,所以接下來我們就來利用Owin的自託管功能。


Demo 2 : 利用OWIN的自託管功能


1. 建立 Console 應用程式
2. 用Nuget安裝Microsoft.Owin.Hosting 和Mi​​crosoft.Owin.HttpListener
利用Microsoft.Owin.Host.HttpListener來實現自寄宿(self-hosting)
3. 添加Startup類 (將上面的 Startup 範例 copy 過來用)
4. 在Main方法中加入下面的一段代碼去啟動我們的網站。
static void Main(string[] args)
{
    using (WebApp.Start<Startup>(new StartOptions(url: "http://localhost:8080/"))) // WebApp using "Microsoft.Owin.Hosting"
    {
        Console.ReadLine();
    }
    Console.ReadLine();
}
啟動我們的控制台程序之後,通過瀏覽​​器訪問http://localhost:8080/就可以看到結果。

用控制台程序自寄宿的時候,就沒有使用IIS,也就沒有System.Web,而response 和 request 訊息被封裝在 Katana (微軟對於Owin的一套實現,用VS2013新建一個MVC5的項目都會自動引用相關的dll(Owin.dll, Microsoft.Owin.dll) ,也會自動添加Startup的配置類)


用Middleware來串成一個完整的管道

以上的 Startup 類裡面使用了Use方法來構成一個完整的管道,現在要將Use中的代碼轉換成Middleware,打包成dll供其它項目使用

IAppBuilder 提供了一個Use的重載可以把一個Middleware作為泛型參數傳進去來實現將這個Middleware註冊進Owin的管道。下面模擬一下AuthenticationMiddleware和AuthorizationMiddleware的實現,我們可以直接從OwinMiddleware繼承


class AuthenticationMiddleware : OwinMiddleware
{
    /*
         * 所有的Middleware構造函數都接收一個OwinMiddleware作為參數傳給基類,
         * 基類會把它作為下一下Middleware,和我們上面用到的Next一樣都是為了確定管道繼續進行下去。
         */
 
    public AuthenticationMiddleware(OwinMiddleware next)
        : base(next)
    {
    }
 
    // 主要邏輯入口
    public override async Task Invoke(IOwinContext context)
    {
        await context.Response.WriteAsync("Authentication....");
        // 如果想在這裡中斷整個管道,下面的程式可以刪除。
        await Next.Invoke(context);
    }
}
 
class AuthorizationMiddleware : OwinMiddleware
{
    public AuthorizationMiddleware(OwinMiddleware next)
        : base(next)
    {
    }
 
    // 主要邏輯入口
    public override async Task Invoke(IOwinContext context)
    {
        await context.Response.WriteAsync("Authorization....");
        await Next.Invoke(context);
    }
}

修改Startup類來註冊我們的Middleware


class Startup
{
    public void Configuration(IAppBuilder app)
    {
        app.Use<AuthenticationMiddleware>();
        app.Use<AuthorizationMiddleware>();
 
        app.Use(async (context, next) =>
        {
            context.Response.ContentType = "text/plain";
            await context.Response.WriteAsync("Hello World!");
        });
    }
}


用Microsoft.Owin.StaticFiles來實現靜態站點的託管

1. 利用上面 console 的範例
2. 用Nuget下載Microsoft.Owin.StaticFiles
3. 在 \bin\Debug 下添加一個 index.html
4. 修改 Startup 類
class Startup
{
    public void Configuration(IAppBuilder app)
    {
        // var root = @"C:\工作\練習\OwinConsoleApplication\OwinConsoleApplication\bin\Debug"; // 直接寫死根目錄位置
        var root = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location);
        var fileSystem = new PhysicalFileSystem(root); // using Microsoft.Owin.FileSystems;
        var options = new FileServerOptions // using Microsoft.Owin.StaticFiles;
        {
            EnableDefaultFiles = true,
            FileSystem = fileSystem
        };
        app.UseFileServer(options: options);
    }
}
啟動 console 應用程式後,利用瀏覽器訪問 http://localhost:8080/ (自行設定的網址) 後,就能看到 index.html

參考:
Getting Started with ASP NET MVC5 6 2 Introduction to OWIN New to MVC5

2014年10月19日 星期日

自定一個 web server 來理解 HttpListener

建立一個 console 應用程式

namespace SampleWebServerApplication
{
    class Program
    {
        public static HttpListener listener = new HttpListener();
        // 把放這個應用程式所存放的路徑當成網站的根目錄 (Path using "System.IO")
        public static string startUpPath = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location);
        static void Main(string[] args)
        {
            try
            {
                // 要監聽的URL範圍, 使用本機IP位置監聽
                listener.Prefixes.Add("http://localhost:8080/");
                // 開始監聽端口,接收客户端請求
                listener.Start();
                Console.WriteLine("Web Server Listening..");
 
                while (true)
                {
                    // 獲取一個客户端請求為止
                    HttpListenerContext context = listener.GetContext();
                    // 從執行緒池開一個新的執行緒去出處理客户端請求
                    System.Threading.ThreadPool.QueueUserWorkItem(ProcessRequest, context);
                }
            }
            catch (Exception e)
            {
 
                Console.WriteLine(e.Message);
            }
            finally
            {
                listener.Stop(); //關閉 HttpListener
                Console.WriteLine("Web Server Stop!");
            }
 
        }
        /// <summary>
        /// 客户請求處理
        /// </summary>
        /// <param name="listenerContext"></param>
        public static void ProcessRequest(object listenerContext)
        {
            try
            {
                var context = listenerContext as HttpListenerContext;
                string fileName = context.Request.RawUrl.Remove(01);
                string path = Path.Combine(startUpPath, fileName);
                byte[] msg;
                if (File.Exists(path)) // 目錄下有該檔案 (File using "System.IO")
                {
                    context.Response.StatusCode = (int)HttpStatusCode.OK;
                    // 開啟二進位檔案, 將檔案內容讀入位元組陣列, 然後關閉檔案
                    msg = File.ReadAllBytes(path);
                }
                else
                {
                    Console.WriteLine("找不到檔案! {0}", path);
                    context.Response.StatusCode = (int)HttpStatusCode.NotFound;
                    msg = File.ReadAllBytes(startUpPath + "\\error.html");
                }
                context.Response.ContentLength64 = msg.Length;
                using (Stream outputStream = context.Response.OutputStream)
                {
                    // 將檔案寫入到 response
                    outputStream.Write(msg, 0, msg.Length);
                    // outputStream.Close(); // 在 using 裡會自動 close 釋放資源
                }
            }
            catch (Exception e)
            {
                Console.WriteLine(e.Message);
            }
        }
    }
}

2014年10月15日 星期三

JQuery 的 on() 原理


在JavaScript中,大多數事件會在DOM裡往上一層一層的泡浮,也就是說,當一個元素觸發一個事件時,她會在DOM一直往上泡浮到 document 層級

舉例,現在有個基本的html:
<div>
   <span>1</span>
   <span>2</span>
</div>
現在,我們採用事件委託:
$('div').on('click', 'span', fn);
該事件處理程序僅僅連接到 div 元素,由於 span 是在 div 裡面,對 span click 會往上泡浮到 div,並觸發 div 的 click 事件處理器,同時,剩下要做的就是檢查 event.target 是否符合我們指定的目標(範例中為 span)

看看更複雜點的範例:
<div id="parent">
    <span>1</span>
    <span>2 <b>another nested element inside span</b></span>
    <p>paragraph won't fire delegated handler</p>
</div>
下面是一些基礎邏輯:

// 對祖先附加處理程序
document.getElementById('parent').addEventListener('click', function(e) {
    // 過濾 event target
    if (e.target.tagName.toLowerCase() === 'span') {
        console.log('span inside parent clicked');
    }
});
上面的程式中,當 event.target 是被嵌套在過濾器裡面時就無法匹配(例如第二個span裡面有b,click她時,會被上面的程式過濾掉),所以我們需要一些迭代邏輯
document.getElementById('parent').addEventListener('click', function(e) {
    var failsFilter = true,
        el = e.target;
    while (el !== this && (failsFilter = el.tagName.toLowerCase() !== 'span') && (el = el.parentNode));
    if (!failsFilter) {
        console.log('span inside parent clicked');
    }
});