使用windows驗證(Using Windows Authentication)
在軟件術語里面,驗證的意思是測定身份。這個跟授權是完全分開的,授權是讓合適的人做合適的事情,授權通常在驗證之后發生。ASP.NET的驗證功能也緊緊圍繞識別訪問者的身份並且設置決定實際的訪問者能夠做什么的安全上下文(security context)。最簡單的驗證方式就是把這個任務委托給IIS(這通常適合內部系統)。在配置文件里啟用Windows身份驗證,如果我們使用的Intranet應用程序模版,默認會使用這個配置。如下:
<configuration>
<system.web>
<authentication mode="Windows" />
</system.web>
</configuration>
使用windows驗證,ASP.NET依賴於IIS來驗證用戶的請求,並且有如下模式:
①匿名驗證:允許任何用戶訪問,IIS7默認啟用
②基本驗證:需要用戶提供一個經過驗證的用戶名和密碼,驗證標識作為文本會從瀏覽器發送到服務器,這種模式僅僅應該在SSL連接的基礎上使用
③摘要驗證:需要用戶名和密碼,驗證標識會被加密為hash代碼發送到服務器。安全性比基本驗證更高,並且需要服務器是一個域控制器。
④windows驗證:通過windows域透明地建立用戶的身份,不需要提供其他的認證。在企業內部局域網使用廣泛,不適合面向Internet的應用。
可以在IIS管理器里面設置驗證模式,如下:
如果我們想讓所有的請求都經過驗證,需要我們禁用匿名驗證功能。如果只是部分限制,那么可以啟用匿名驗證並對actions和controllers使用驗證過濾器。在局域網部署程序,那么使用windows驗證是非常有用的。如果用戶是來自Internet,應用程序傾向於依賴表單驗證。
使用表單驗證(Using Forms Authentication)
表單驗證非常適合面向Internet網的程序,當然建立表單驗證也會比windows驗證更加復雜,一旦設置都完畢,那么比windows驗證將會更加靈活。表單驗證的安全性依賴於一加密的cookie——.ASPXAUTH。這個cookie內容類似於:
9CC50274C662470986ADD690704BF652F4DFFC3035FC19013726A22F794B3558778B12F799852B2E84
D34D79C0A09DA258000762779AF9FCA3AD4B78661800B4119DD72A8A7000935AAF7E309CD81F28
用FormsAuthentication.Decrypt對其進行解密,可以得到一個FormsAuthenticationTicket對象並且具有如下屬性:
其中里面一個關鍵的屬性就是Name,這個跟用戶請求的相關聯的身份標識。系統的安全性的保證來自於cookie數據的機密和使用服務器的machine keys的簽名。這些是由IIS自動生成的,並且如果沒有這些keys,包含在cookie里的驗證信息是不能被讀取和修改的。
Tip:當我們把使用了表單驗證的程序部署到服務器集群時,必須保證請求總是返回生成cookie的服務器或保證所有的服務器具有相同的machine keys。這些Keys可以使用IIS管理器里面的machine keys選項來生成和配置。
建立表單驗證(Setting Up Forms Authentication)
當我們使用互聯網應用程序模版創建一個MVC程序時,默認啟用了表單身份驗證,相關的配置如下:
<authentication mode="Forms">
<forms loginUrl="~/Account/LogOn" timeout="2880" />
</authentication>
這個配置能夠適合大多數程序,當然也可以定義更多的屬性來控制,這些屬性如下:
屬性名 | 默認值 | 描述 |
name | .ASPXAUTH | cookie名 |
timeout | 30分鍾 | 超時時間 |
slidingExpiration | true | 滑動過去時間 |
domain | None | 跨子域共享(www.example.com和a.example.com) |
path | / | 設置驗證cookie發送到指定的URL,這個讓我們可以在同一個域中寄宿多個應用程序而不會暴露彼此的驗證cookie |
loginUrl | /login.aspx | 如果表單需要用戶登錄,重定向到指定的登錄頁面 |
cookieless | UserDeviceProfile | 啟用無cookie驗證 |
requireSSL | false | 設置為true會建議瀏覽器僅僅在使用SSL加密的請求中傳遞cookie |
在web.config文件啟用表單驗證,當沒有經過驗證的用戶訪問到任何標記了[Authorize]的controller和action時會跳轉到登錄頁。
使用無cookie的表單驗證(Using Cookieless Forms Authentication)
表單驗證支持無cookie模式,這種情況下驗證的票據存放在URL里,數據仍然是簽名和加密的,但是會作為URL的一部分發送到服務器。只要每一個請求包含了驗證數據,那么用戶接收到同樣應用程序的體驗跟啟用了cookie是一樣的。啟用無cookie驗證的配置如下:
<authentication mode="Forms">
<forms loginUrl="~/Account/LogOn" timeout="2880" cookieless="UseUri">
</forms>
</authentication>
當用戶登錄時,他們會被重定向到一個像如下的URL:
/(F(nMD9DiT464AxL7nlQITYUTT05ECNIJ1EGwN4CaAKKze-9ZJq1QTOK0vhXTx0fWRjAJdgSYojOYyhDil
HN4SRb4fgGVcn_fnZU0x55I3_Jes1))/Home/ShowPrivateInformation
仔細觀察會發現,URL遵循這樣的模式:/(F(authenticationData))/normalUrl
我們不用特別的步驟和配置,路由系統會仔細轉換這些URL以至於我們的路由配置能夠適應任何改變並且HTML輔助方法會自動生成這種類型的URL。不推薦使用無cookie的驗證,因為這種驗證非常脆弱,如果有一個鏈接沒有包含驗證信息,那么用戶馬上就被注銷了。無cookie驗證也是不安全的,任何一個人復制你的URL並共享給其他人,那么第一個用戶的session將會被劫持。而且,如果我們依賴了從第三方服務器獲取的內容,那么我們的驗證數據會通過瀏覽器的引用頭(Referer header)發送給第三方。最后,這個URL看起非常丑陋,沒有可讀性和吸引力。
使用Membership,Roles和Profiles
在SportsStroe項目里面,我們在Web.config文件里面存儲了用戶的證書信息,對於小的程序和用戶不會經常改變並且用戶數量很小的情況下可以這樣使用。ASP.NET提供了一套標准的用戶賬戶系統來支持常用的用戶賬戶管理任務,包含注冊,密碼管理,個性設置。具有三個關鍵的功能區域:
Membership:注冊用戶賬戶並訪問賬戶詳情和授權證書
Roles:把用戶放入組里面,對身份驗證的典型應用
Profiles:存儲每一個用戶的基礎數據
ASP.NET對上面的三個區域提供了標准的實現,但是我們也可以跟自定義的實現混合使用,通過一個providers系統就可以實現。內置的providers能夠以不同的方式存儲數據,包括SQL Server和活動目錄。
建立和使用Membership
ASP.NET里面已經有了SqlMembershipProvider和ActiveDirectoryMembershipProvider兩個提供者。下面會介紹如何最常規的使用:
建立SqlMembershipProvider
當使用互聯網應用程序模版創建一個MVC程序時,默認配置了SqlMembershipProvider。配置的Web.config如下:

<configuration> <connectionStrings> <add name="ApplicationServices" connectionString="data source=.\SQLEXPRESS;Integrated Security=SSPI; AttachDBFilename=|DataDirectory|aspnetdb.mdf;User Instance=true" providerName="System.Data.SqlClient" /> </connectionStrings> ... <system.web> <membership> <providers> <clear/> <add name="AspNetSqlMembershipProvider" type="System.Web.Security.SqlMembershipProvider" connectionStringName="ApplicationServices" enablePasswordRetrieval="false" enablePasswordReset="true" requiresQuestionAndAnswer="false" requiresUniqueEmail="false" maxInvalidPasswordAttempts="5" minRequiredPasswordLength="6" minRequiredNonalphanumericCharacters="0"passwordAttemptWindow="10" applicationName="/" /> </providers> </membership> </system.web> </configuration>
SQL Server Express版本的數據庫支持一個user實例的數據庫,這些數據庫是不用在之前配置就可以使用的。付費版的SQL Server不支持用戶實例數據庫,需要在使用之前准備好該數據庫。安裝方法:運行aspnet_regsql.exe。安裝完了可以去SQL Server management studio里面查看下。
管理Membership
Membership API包含對注冊用戶的管理方法:添加和移除賬戶,重置密碼等等。對於一些簡單的情形,完全可以使用站點管理工具(VS里面有對應的按鈕)。如下:
一旦部署了應用程序,我們能夠通過IIS .NET用戶選項來管理應用程序的用戶。使用.NET User選項時,IIS管理工具讀取Web.config文件並且視圖確保membership提供者是值得信任的。不太走運的是,IIS管理工具是基於.NET2.0並且尚未更新到支持ASP.NET4.當前的IIS版本,微軟對使用.NET4的程序禁用了.NET Users選項。下面看看示例的配置文件:

<?xml version="1.0" encoding="UTF-8"?> <configuration> <connectionStrings> <add name="ApplicationServices" connectionString="data Source=TITAN\SQLEXPRESS; Initial Catalog=aspnetdb; Persist Security Info=True; User ID=adam;Password=adam" providerName="System.Data.SqlClient" /> </connectionStrings> <system.web> <authentication mode="Forms"> <forms loginUrl="~/Account/LogOn" timeout="2880" /> </authentication> <membership> <providers> <remove name="AspNetSqlMembershipProvider"/> <add name="AspNetSqlMembershipProvider" type="System.Web.Security.SqlMembershipProvider, System.Web, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" connectionStringName="ApplicationServices" applicationName="/" /> </providers> </membership> </system.web> </configuration>
這是一個精簡過的Web.config文件,只包含兩個配置節:connection string和membership database,使用時編輯適合自己的環境。
創建一個自定義的Membership提供者
通過從抽象類MembershipProvider派生可以創建自定義的membership提供者,如下所示:

using System; using System.Web.Security; using System.Collections.Generic; namespace MvcApp.Infrastructure { public class SiteMember { public string UserName { get; set; } public string Password { get; set; } } public class CustomMembershipProvider : MembershipProvider { private static List<SiteMember> Members = new List<SiteMember> { new SiteMember { UserName = "adam", Password = "secret" }, new SiteMember { UserName = "steve", Password = "shhhh" } }; public override bool ValidateUser(string username, string password) { return Members.Exists(m => m.UserName == username && m.Password == password); } ... ... } }
上面的Provider使用一個靜態的用戶和密碼列表來執行驗證,為了簡單說明原理,這里忽略了除ValidateUser之外的其他的方法。接着在配置文件注冊自定義的Provider:

<configuration> <system.web> <authentication mode="Forms"> <forms loginUrl="~/Account/LogOn" timeout="2880" /> </authentication> <membership defaultProvider="MyMembershipProvider"> <providers> <clear/> <add name="MyMembershipProvider" type="MvcApp.Infrastructure.CustomMembershipProvider"/> </providers> </membership> </system.web> </configuration>
下面截取一個在SportsStore里面使用的例子:

using System.Web.Security; using SportsStore.WebUI.Infrastructure.Abstract; namespace SportsStore.WebUI.Infrastructure.Concrete { public class FormsAuthProvider : IAuthProvider { public bool Authenticate(string username, string password) { bool result = Membership.ValidateUser(username, password); if (result) { FormsAuthentication.SetAuthCookie(username, false); } return result; } } }
建立並使用角色
前面介紹了驗證,還有另外一個常見的安全需求就是授權——決定用戶在驗證之后能夠做什么。ASP.NET使用基於角色的授權機制,這意味着actions是限制在角色里面的,屬於角色的用戶能夠執行相應的action方法。角色通過唯一的字符串值來表示,例如可以定義如下三個角色:ApprovedMember CommentsModerator SiteAdministrator。每一個角色都是完全獨立的,沒有層級關系。ASP.NET平台期望我們通過provider模型來使用角色,提供了常用的API。當然也可以自定義:
建立SqlRoleProvider
SqlRoleProvider類是對SqlMembershipProvider的補足,使用了同樣的數據庫。使用互聯網應用程序模版創建MVC程序,VS會自動添加相應的元素來建立SqlRoleProvider,如下:

<configuration> <system.web> <roleManager enabled="false"> <providers> <clear/> <add name="AspNetSqlRoleProvider" type="System.Web.Security.SqlRoleProvider" connectionStringName="ApplicationServices" applicationName="/" /> <add name="AspNetWindowsTokenRoleProvider" type="System.Web.Security.WindowsTokenRoleProvider" applicationName="/" /> </providers> </roleManager> </configuration>
兩個Role Provider注冊了,默認都沒有啟用。要建立SqlRoleProvider必須修改roleManager元素
如:<roleManager enabled="true" defaultProvider="AspNetSqlRoleProvider">
管理Roles
可以使用管理members的方法管理roles,可以使用VS里面的ASP.NET圖形化的配置工具來操作。下面介紹創建自定義的Roles Provider:

using System; using System.Web.Security; namespace MvcApp.Infrastructure { public class CustomRoleProvider : RoleProvider { public override string[] GetRolesForUser(string username) { if (username == "adam") { return new string[] { "CommentsModerator", "SiteAdministrator" }; } else if (username == "steve") { return new string[] { "ApprovedUser", "CommentsModerator" }; } else { return new string[] { }; } } ...... } }
通過從RoleProvider派生來定義自己的提供者,我只需要實現GetRolesForUser方法就可以使用了。同樣,創建以后需要注冊:

<configuration> <system.web> <authentication mode="Forms"> <forms loginUrl="~/Account/LogOn" timeout="2880" /> </authentication> <membership defaultProvider="MyMembershipProvider"> <providers> <clear/> <add name="MyMembershipProvider" type="MvcApp.Infrastructure.CustomMembershipProvider"/> </providers> </membership> <roleManager enabled="true" defaultProvider="MyRoleProvider"> <providers> <clear/> <add name="MyRoleProvider" type="MvcApp.Infrastructure.CustomRoleProvider"/> </providers> </roleManager> </system.web> </configuration>
建立並使用Profiles
Membership記錄我們的用戶,roles記錄允許用戶做什么操作。如果我們想記錄用戶個性化的一些數據,如會員積分等等信息。那么通常可以使用Profiles,這對於使用SqlMembershipProvider的小應用程序來說是一個非常有吸引力的功能。
建立SqlProfileProvider
使用互聯網應用程序模版創建一個新的MVC程序,包含在Web.config里面的元素創建SqlProfileProvider。如下:

<configuration> <system.web> <profile> <providers> <clear/> <add name="AspNetSqlProfileProvider" type="System.Web.Profile.SqlProfileProvider" connectionStringName="ApplicationServices" applicationName="/" /> </providers> </profile> </system.web> </configuration>
配置/讀/寫Profile數據
在使用Profile之前,我們必須定義Profile的數據結構,可以在Web.config的profile節里面添加屬性元素如下:

<profile> <providers> <clear/> <add name="AspNetSqlProfileProvider" type="System.Web.Profile.SqlProfileProvider" connectionStringName="ApplicationServices" applicationName="/" /> </providers> <properties> <add name="Name" type="String"/> <group name="Address"> <add name="Street" type="String"/> <add name="City" type="String"/> <add name="ZipCode" type="String"/> <add name="State" type="String"/> </group> </properties> </profile>
上面定義的屬性都是String類型的,但是Profile支持任何能夠序列化的.NET類型。在我們使用Web Forms的時候,通過跟profiles屬性一致的代理對象來訪問profile數據。這個在MVC里是不可行的,但是我們可以使用HttpContext.Profile屬性來訪問。如:

public ActionResult Index() { ViewBag.Name = HttpContext.Profile["Name"]; ViewBag.City = HttpContext.Profile.GetProfileGroup("Address")["City"]; return View(); } [HttpPost] public ViewResult Index(string name, string city) { HttpContext.Profile["Name"] = name; HttpContext.Profile.GetProfileGroup("Address")["City"] = city; return View(); }
ASP.NET框架在我們第一次訪問profile數據時,使用profile提供者載入用戶的profile屬性並且在請求結束時通過profile提供者寫回,我們不用顯示的保存,這個是自動完成的。
啟用匿名的Profiles
默認情況下,profile數據只對經過驗證的用戶可用,沒有登錄的用戶訪問時會拋異常。可以通過啟用對你們profile的支持來解決:

<configuration> <system.web> <anonymousIdentification enabled="true"/> <profile> <providers> <clear/> <add name="AspNetSqlProfileProvider" type="System.Web.Profile.SqlProfileProvider" connectionStringName="ApplicationServices" applicationName="/" /> </providers> <properties> <add name="Name" type="String" allowAnonymous="true"/> <group name="Address"> <add name="Street" type="String"/> <add name="City" type="String" allowAnonymous="true"/> <add name="ZipCode" type="String"/> <add name="State" type="String"/> </group> </properties> </profile> </system.web> </configuration>
當匿名的身份標識啟用時,ASP.NET框架將會通過一個.ASPXANONYMOUS的cookie來記錄你們用戶,並且cookie的過期時間是10000分鍾(70天左右)。啟用了匿名profile以后,沒有驗證的用戶也可以讀寫profile數據,沒一個沒有驗證的用戶會自動為他們創建一個賬戶並保存在profile數據庫里面。
創建自定義的Profile提供者(Profile Provider)
通過從ProfileProvider派生來創建自定義的profile provider。如下:

using System; using System.Collections.Generic; using System.Configuration; using System.Linq; using System.Web.Profile; namespace MvcApp.Infrastructure { public class CustomProfileProvider : ProfileProvider { private IDictionary<string, IDictionary<string, object>> data = new Dictionary<string, IDictionary<string, object>>(); public override SettingsPropertyValueCollection GetPropertyValues( SettingsContext context, SettingsPropertyCollection collection) { SettingsPropertyValueCollection result = new SettingsPropertyValueCollection(); IDictionary<string, object> userData; bool userDataExists = data.TryGetValue((string)context["UserName"], out userData); foreach (SettingsProperty prop in collection) { SettingsPropertyValue spv = new SettingsPropertyValue(prop); if (userDataExists) { spv.PropertyValue = userData[prop.Name]; } result.Add(spv); } return result; } public override void SetPropertyValues(SettingsContext context, SettingsPropertyValueCollection collection) { string userName = (string)context["UserName"]; if (!string.IsNullOrEmpty(userName)) { data[userName] = collection .Cast<SettingsPropertyValue>() .ToDictionary(x => x.Name, x => x.PropertyValue); } } ...... } }
上面的provider非常簡單,僅僅將數據存儲在內存。接着需要注冊:

<configuration> <connectionStrings> <add name="ApplicationServices" connectionString="data Source=TITAN\SQLEXPRESS; Initial Catalog=aspnetdb; Persist Security Info=True; User ID=adam;Password=adam" providerName="System.Data.SqlClient" /> </connectionStrings> <system.web> <authentication mode="Forms"> <forms loginUrl="~/Account/LogOn" timeout="2880" /> </authentication> <membership defaultProvider="MyMembershipProvider"> <providers> <clear/> <add name="MyMembershipProvider" type="MvcApp.Infrastructure.CustomMembershipProvider"/> </providers> </membership> <roleManager enabled="true" defaultProvider="MyRoleProvider"> <providers> <clear/> <add name="MyRoleProvider" type="MvcApp.Infrastructure.CustomRoleProvider"/> </providers> </roleManager> <profile enabled="true" defaultProvider="MyProfileProvider"> <providers> <clear/> <add name="MyProfileProvider" type="MvcApp.Infrastructure.CustomProfileProvider" /> </providers> <properties> <add name="Name" type="String"/> <group name="Address"> <add name="Street" type="String"/> <add name="City" type="String"/> <add name="ZipCode" type="String"/> <add name="State" type="String"/> </group> </properties> </profile> </system.web> </configuration>
為什么不應該使用基於URL的授權(Why You Shouldn’t Use URL-Based Authorization)
ASP.NET曾經依靠匹配URL到應用程序的目錄結構來實現授權,這樣的做法非常多以至於在URL模式中定義授權規則有非常重要的意義。很多Webform的程序,例如把所有管理員的aspx頁面放在一個Admin的文件夾下,這意味着我們可以使用基於URL的授權來限制登錄的用戶對/Admin/*的訪問。這種方式對MVC框架是沒有用的,因為路由系統的存在打破了URL跟文件系統之間的聯系。正確做法是在controller上使用Authorize過濾器(前面的章節有介紹)
使用IP地址和域名限制訪問(Restricting Access Using IP Addresses and Domains)
首先介紹下這么做的風險:
這種方式應該謹慎使用,有很多潛在的風險。首先這是基於URL的授權,並且不能很完善的跟MVC的路由系統相適應。其次,用這種方式限制訪問沒有考慮用戶怎樣跟我們應用程序的可能的交互。舉一個例子,亞當今天早上在家第一次上網,后來他在咖啡店連接上網,在朋友家上網,在另一個朋友的辦公室上網,最后回到家里上網。在上面的情形里有些是通過VPN連接的,有些是直接連的網。基於用戶從哪連接網絡來限制訪問很容易建立,但是要想得到正確的判斷很困難。下面的Web.config使用了IP地址和域名限制來建立:

<configuration> <location path="Home"> <system.webServer> <security> <ipSecurity enableReverseDns="true"> <clear/> <add ipAddress="192.188.100.1"/> <add ipAddress="169.254.0.0" subnetMask="255.255.0.0"/> <add domainName="mydomain.com"/> <remove domainName="otherdomain.com"/> </ipSecurity> </security> </system.webServer> </location> </configuration>
我們在ipSecurity元素里面定義策略,可以使用IP地址和域名來限制訪問,但是需要顯示的啟用enableReverseDns=true。建議在啟用域名支持時需要謹慎,因為它需要為每一個請求反向查找DNS,這是非常耗時的並且會嚴重限制服務器的吞吐量。使用clear元素移除任何存在的策略,然后使用add和remove定義我們的新策略。add添加新的限制,如:
<add ipAddress="192.188.100.1"/> 這樣阻止處理來自該IP地址的請求,再如:<remove domainName="otherdomain.com"/> ,這樣就允許來自該域名的請求。
好了,本章的筆記就到這里,還剩下最后一章的筆記,謝謝大家持續支持。希望路過的朋友留下你們的各種建議或意見:-)