前言
之前一直在找工作中,過程也是令人着實的心塞,最后還是穩定了下來,博客也停止更新快一個月了,學如逆水行舟,不進則退,之前學的東西沒怎么用,也忘記了一點,不過至少由於是切身研究,本質以及原理上的脈絡還是知其所以然,所以也無關緊要,停止學習以及分享是一件很痛苦的事情,心情很忐忑也很擔憂,那么多牛逼的人都在無時無刻的學習更何況是略懂皮毛的我呢?好了,廢話說了不少,我們接下來進入主題。
話題
看到博客也有對於我最近有關Web APi中認證這篇文章的評論和疑問,【其中就有一個是何時清除用戶的信息呢】,我當時也就僅僅想想的是認證,所以對於這個問題也不知如何解答,后來還是想了想在這個地方還是略有不足,認證成功之后其信息會一直存在,我們怎樣去靈活的控制呢?關於用戶的信息的清除或者將問題抽離出來可以這樣說:【在Web APi中如何維護Session呢?】於是乎,就誕生了這篇文章的出現。這篇文章應該值得一看,將用我淺薄的理解加上一些其他的知識,而不是僅僅停留在認證以及授權這塊上。
【溫馨提示】:此文你將學習到UnitOfWork、MEF、IOC之Unity、Log4Net、WebApiTestOnHelpPage、基於WebAPi認證后續。。。。。。
如何維護Session呢?
我們知道RESETful是基於Http無狀態的協議,我們在Web APi中實現維護Session可以用基於我寫過的授權的票據,一個用戶當已經被認證后可以在某一個階段時間內訪問服務器上的資源,當再次發出請求時可以通過增加Session的時間來訪問相同的資源或者說其他的資源,在Web應用中如果我們使用Web APi作為服務對於用戶的登陸和退出時,我們需要實現【基於認證和授權的基礎驗證或者摘要認證】 。至於這二者驗證前面文章也已經介紹,更多詳細內容請參考前面內容,不再敘述。下面我們慢慢來搭建整個應用程序架構。
實體層(ApplicationEntity)
既然是維持Session,那么必然是涉及到兩個實體類了,即用戶實體類 UserEntity 和票據類 TokenEntity 。
UserEntity
public class UserEntity { public int UserId { get; set; } public string UserName { get; set; } public string UserPassword { get; set; } public virtual ICollection<TokenEntity> Tokens { get; set; } }
TokenEntity
public class TokenEntity { public int TokenId { get; set; } public string AuthToken { get; set; } public System.DateTime IssuedOn { get; set; } public System.DateTime ExpiresOn { get; set; } public int UserId { get; set; } public virtual UserEntity User { get; set; } }
實體模型層(UnitOfWork 即ApplicationDataModel)
在之前系列介紹過關於在WebAPi中利用EF中的倉儲模式來實現其增、刪等,並未涉及到這一塊知識,關於UnitOfWork(工作單元)應該是屬於領域驅動設計中的一種解決方案,對於領域驅動設計的詳細介紹以及學習我也是只是處於了解的層次,只是看了工作單元這一部分,有關領域驅動設計可以參考園友(dax.net)的文章,至於關於對於UnitOfWork的最佳實踐以及幾種實現方式,請參考園友(田園里的蟋蟀)的文章,對於一些基礎知識就不再廢話,接下來我們繼續往下走。在討論這個之前我們需要首先得看以下實現。關於以下有些內容可能之前系列文章中已經介紹,請耐心點,至於為什么還是貼上代碼,是為了更好的讓大家理解整個業務邏輯(已經介紹過的也已經折疊),當然你覺得你知道了,那就跳過向下看,能夠吸收到知識是我的榮幸,覺得是廢話一篇,就當我是對自己的一次學習。
(第一步)IDbContext
在這個接口中,我們封裝了通過EF上下文來進行對數據操作常用的幾個方法,具體實現如下:
public interface IDbContext
{
/// <summary>
/// 獲得實體集合
/// </summary>
/// <typeparam name="TEntity"></typeparam>
/// <returns></returns>
DbSet<TEntity> Set<TEntity>() where TEntity : class;
/// <summary>
/// 執行存儲過程
/// </summary>
/// <typeparam name="TEntity"></typeparam>
/// <param name="commandText"></param>
/// <param name="parameters"></param>
/// <returns></returns>
IList<TEntity> ExecuteStoredProcedureList<TEntity>(string commandText, params object[] parameters)
where TEntity : class;
/// <summary>
/// 執行SQL語句查詢
/// </summary>
/// <typeparam name="TElement"></typeparam>
/// <param name="sql"></param>
/// <param name="parameters"></param>
/// <returns></returns>
IEnumerable<TElement> SqlQuery<TElement>(string sql, params object[] parameters);
DbEntityEntry Entry<TEntity>(TEntity entity) where TEntity : class;
/// <summary>
/// 變更追蹤代碼
/// </summary>
bool ProxyCreationEnabled { get; set; }
/// <summary>
/// DetectChanges方法自動調用
/// </summary>
bool AutoDetectChangesEnabled { get; set; }
/// <summary>
/// 調用Dispose方法
/// </summary>
void Dispose();
}
(第二步)EFDbContext(EF上下文對該接口的具體實現)
public class EFDbContext : DbContext, IDbContext { public EFDbContext(string connectionString) : base(connectionString) { } static EFDbContext() { Database.SetInitializer<EFDbContext>(new DropCreateDatabaseIfModelChanges<EFDbContext>()); } /// <summary> /// 一次性加載所有映射 /// </summary> /// <param name="modelBuilder"></param> protected override void OnModelCreating(DbModelBuilder modelBuilder) { var typesToRegister = Assembly.GetExecutingAssembly().GetTypes() .Where(type => !String.IsNullOrEmpty(type.Namespace)) .Where(type => type.BaseType != null && type.BaseType.IsGenericType && type.BaseType.GetGenericTypeDefinition() == typeof(EntityTypeConfiguration<>)); foreach (var type in typesToRegister) { dynamic configurationInstance = Activator.CreateInstance(type); modelBuilder.Configurations.Add(configurationInstance); } base.OnModelCreating(modelBuilder); } /// <summary> /// 獲得實體集合 /// </summary> /// <typeparam name="TEntity"></typeparam> /// <returns></returns> public new DbSet<TEntity> Set<TEntity>() where TEntity : class { return base.Set<TEntity>(); } /// <summary> /// 實體狀態 /// </summary> /// <typeparam name="TEntity"></typeparam> /// <param name="entity"></param> /// <returns></returns> public new DbEntityEntry Entry<TEntity>(TEntity entity) where TEntity : class { return base.Entry<TEntity>(entity); } /// <summary> /// 執行存儲過程 /// </summary> /// <typeparam name="TEntity"></typeparam> /// <param name="commandText"></param> /// <param name="parameters"></param> /// <returns></returns> public IList<TEntity> ExecuteStoredProcedureList<TEntity>(string commandText, params object[] parameters) where TEntity : class { if (parameters != null && parameters.Length > 0) { for (int i = 0; i <= parameters.Length - 1; i++) { var p = parameters[i] as DbParameter; if (p == null) throw new Exception("Not support parameter type"); commandText += i == 0 ? " " : ", "; commandText += "@" + p.ParameterName; if (p.Direction == ParameterDirection.InputOutput || p.Direction == ParameterDirection.Output) { commandText += " output"; } } } var result = this.Database.SqlQuery<TEntity>(commandText, parameters).ToList(); bool acd = this.Configuration.AutoDetectChangesEnabled; try { this.Configuration.AutoDetectChangesEnabled = false; for (int i = 0; i < result.Count; i++) result[i] = this.Set<TEntity>().Attach(result[i]); } finally { this.Configuration.AutoDetectChangesEnabled = acd; } return result; } /// <summary> /// SQL語句查詢 /// </summary> /// <typeparam name="TElement"></typeparam> /// <param name="sql"></param> /// <param name="parameters"></param> /// <returns></returns> public IEnumerable<TElement> SqlQuery<TElement>(string sql, params object[] parameters) { return this.Database.SqlQuery<TElement>(sql, parameters); } /// <summary> /// 當查詢或者獲取值時是否啟動創建代理 /// </summary> public virtual bool ProxyCreationEnabled { get { return this.Configuration.ProxyCreationEnabled; } set { this.Configuration.ProxyCreationEnabled = value; } } /// <summary> /// 當查詢或者獲取值時指定是否開啟自動調用DetectChanges方法 /// </summary> public virtual bool AutoDetectChangesEnabled { get { return this.Configuration.AutoDetectChangesEnabled; } set { this.Configuration.AutoDetectChangesEnabled = value; } } }
(第三步) 封裝BaseRepository倉儲實現通過EFDbContext上下文對數據的基本操作
public class BaseRepository<TEntity> where TEntity : class { internal EFDbContext Context; internal DbSet<TEntity> DbSet; public BaseRepository(EFDbContext context) { this.DbSet = context.Set<TEntity>(); } public virtual IEnumerable<TEntity> Get() { IQueryable<TEntity> query = DbSet; return query.ToList(); } public virtual TEntity GetByID(object id) { return DbSet.Find(id); } public virtual void Insert(TEntity entity) { DbSet.Add(entity); } public virtual void Delete(object id) { TEntity entityToDelete = DbSet.Find(id); Delete(entityToDelete); } public virtual void Delete(TEntity entityToDelete) { if (Context.Entry(entityToDelete).State == EntityState.Detached) { DbSet.Attach(entityToDelete); } DbSet.Remove(entityToDelete); } public virtual void Update(TEntity entityToUpdate) { DbSet.Attach(entityToUpdate); Context.Entry(entityToUpdate).State = EntityState.Modified; } public virtual IEnumerable<TEntity> GetMany(Func<TEntity, bool> where) { return DbSet.Where(where).ToList(); } public virtual IQueryable<TEntity> GetManyQueryable(Func<TEntity, bool> where) { return DbSet.Where(where).AsQueryable(); } public TEntity Get(Func<TEntity, Boolean> where) { return DbSet.Where(where).FirstOrDefault<TEntity>(); } public void Delete(Func<TEntity, Boolean> where) { IQueryable<TEntity> objects = DbSet.Where<TEntity>(where).AsQueryable(); foreach (TEntity obj in objects) DbSet.Remove(obj); } public virtual IEnumerable<TEntity> GetAll() { return DbSet.ToList(); } public IQueryable<TEntity> GetWithInclude(System.Linq.Expressions.Expression<Func<TEntity, bool>> predicate, params string[] include) { IQueryable<TEntity> query = this.DbSet; query = include.Aggregate(query, (current, inc) => current.Include(inc)); return query.Where(predicate); } public bool Exists(object primaryKey) { return DbSet.Find(primaryKey) != null; } public TEntity GetSingle(Func<TEntity, bool> predicate) { return DbSet.Single<TEntity>(predicate); } public TEntity GetFirst(Func<TEntity, bool> predicate) { return DbSet.First<TEntity>(predicate); } }
(第四步)對於UnitOfWork,當然就需要IUnitOfWork接口了
interface IUnitOfWork { void Commit(); void RollBack(); }
(個人比較傾向於該接口只有數據的提交,至於增、刪等操作則是放在服務層)
(第五步)UnitOfWork最終登上舞台
public class UnitOfWork : IUnitOfWork, IDisposable { private bool disposed = false; private readonly EFDbContext _context = null; private BaseRepository<UserEntity> _userRepository; private BaseRepository<TokenEntity> _tokenRepository; public UnitOfWork() { _context = new EFDbContext("basicAuthenticate"); } /**獲得用戶倉儲**/ public BaseRepository<UserEntity> UserRepository { get { if (_userRepository == null) _userRepository = new BaseRepository<UserEntity>(_context); return _userRepository; } } /**獲得票據倉儲**/ public BaseRepository<TokenEntity> TokenRepository { get { if (_tokenRepository == null) _tokenRepository = new BaseRepository<TokenEntity>(_context); return _tokenRepository; } }
/**進行數據提交持久化到數據庫中**/ public void Commit() { try { _context.SaveChanges(); } catch (Exception ex) { logger.LogError("--------EF提交數據,出現異常:" + ex.Message); logger.LogError("--------EF提交數據,堆棧消息:" + ex.StackTrace); } } public void RollBack() { throw new Exception(); } protected virtual void Dispose(bool disposing) { if (!this.disposed) { if (disposing) { logger.LogInfo("釋放UnitOfWork資源"); _context.Dispose(); } } this.disposed = true; } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } }
服務層 (ApplicationService)
用戶服務接口(IUserService)
public interface IUserService { int Authenticate(string userName, string userPassword); }
用戶服務接口實現(UserService)
public class UserService : IUserService { private readonly UnitOfWork _unitOfWork; public UserService(UnitOfWork unitOfWork) { _unitOfWork = unitOfWork; }
/**認證用戶名和密碼**/ public int Authenticate(string userName, string password) { var user = _unitOfWork.UserRepository.Get(u => u.UserName == userName && u.UserPassword == password); if (user != null && user.UserId > 0) { return user.UserId; } return 0; } }
票據服務接口(ITokenService)
public interface ITokenService { TokenEntity GenerateToken(int userId); /**認證通過自動生成票據**/ bool ValidateToken(string tokenId);/**下次登錄認證添加到cookie中的票據是否過期**/ bool Kill(string tokenId);/*刪除票據**/ bool DeleteByUserId(int userId);/**通過用戶Id刪除該用戶所有票據**/ }
票據服務接口實現(TokenService)
public class TokenService : ITokenService { private readonly UnitOfWork _unitOfWork; public TokenService(UnitOfWork unitOfWork) { _unitOfWork = unitOfWork; } public TokenEntity GenerateToken(int userId) { string token = Guid.NewGuid().ToString(); DateTime issuedOn = DateTime.Now; DateTime expiredOn = DateTime.Now.AddSeconds( Convert.ToDouble(ConfigurationManager.AppSettings["AuthTokenExpiry"])); var tokendomain = new TokenEntity { UserId = userId, AuthToken = token, IssuedOn = issuedOn, ExpiresOn = expiredOn }; _unitOfWork.TokenRepository.Insert(tokendomain); _unitOfWork.Commit(); var tokenModel = new TokenEntity() { UserId = userId, IssuedOn = issuedOn, ExpiresOn = expiredOn, AuthToken = token }; return tokenModel; }
/**驗證票據和失效時間,若未過期則繼續追加失效時間,並更新並提交到數據庫中**/ public bool ValidateToken(string tokenId) { var token = _unitOfWork.TokenRepository.Get(t => t.AuthToken == tokenId && t.ExpiresOn > DateTime.Now); if (token != null && !(DateTime.Now > token.ExpiresOn)) { token.ExpiresOn = token.ExpiresOn.AddSeconds( Convert.ToDouble(ConfigurationManager.AppSettings["TokenExpiry"])); _unitOfWork.TokenRepository.Update(token); _unitOfWork.Commit(); return true; } return false; } public bool Kill(string tokenId) { _unitOfWork.TokenRepository.Delete(x => x.AuthToken == tokenId); _unitOfWork.Commit(); var isNotDeleted = _unitOfWork.TokenRepository.GetMany(x => x.AuthToken == tokenId).Any(); if (isNotDeleted) { return false; } return true; } public bool DeleteByUserId(int userId) { _unitOfWork.TokenRepository.Delete(x => x.UserId == userId); _unitOfWork.Commit(); var isNotDeleted = _unitOfWork.TokenRepository.GetMany(x => x.UserId == userId).Any(); return !isNotDeleted; } }
到此為止,基本的實現都已經完成,我們就差表現層,在表現層我們就需要實現自定義認證。下面我們一起來看看表現層。
表現層(WebApiFllowUp)
認證實體類(BasicAuthenticationIdentity),多添加一個字段,就是用戶Id(UserId)
public class BasicAuthenticationIdentity : GenericIdentity { public int UserId { get; set; } public string UserName { get; set; } public string UserPassword { get; set; } public BasicAuthenticationIdentity(string name, string password) : base(name, "Basic") { this.UserName = name; this.UserPassword = password; } }
自定義基礎認證特性
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false)] public class BasicAuthenticationFilter : AuthorizationFilterAttribute { public override void OnAuthorization(HttpActionContext actionContext) { var userIdentity = ParseHeader(actionContext); if (userIdentity == null) { Challenge(actionContext); return; } var principal = new GenericPrincipal(userIdentity, null); Thread.CurrentPrincipal = principal; if (!OnAuthorizeUser(userIdentity.Name, userIdentity.UserPassword, actionContext)) { Challenge(actionContext); return; } base.OnAuthorization(actionContext); } protected virtual bool OnAuthorizeUser(string userName, string userPassword, HttpActionContext actionContext) { if (string.IsNullOrEmpty(userName) || string.IsNullOrEmpty(userPassword)) return false; else return true; } public virtual BasicAuthenticationIdentity ParseHeader(HttpActionContext actionContext) { string authParameter = null; var authValue = actionContext.Request.Headers.Authorization; if (authValue != null && authValue.Scheme == "Basic") authParameter = authValue.Parameter; if (string.IsNullOrEmpty(authParameter)) return null; authParameter = Encoding.Default.GetString(Convert.FromBase64String(authParameter)); var authToken = authParameter.Split(':'); if (authToken.Length < 2) return null; return new BasicAuthenticationIdentity(authToken[0], authToken[1]); } private void Challenge(HttpActionContext actionContext) { var host = actionContext.Request.RequestUri.DnsSafeHost; actionContext.Response = actionContext.Request.CreateResponse(HttpStatusCode.Unauthorized); actionContext.Response.Headers.Add("WWW-Authenticate", string.Format("Basic realm=\"{0}\"", host)); } }
自定義針對於APi認證特性,重載基礎認證特性中方法
public class ApiAuthenticationFilter : BasicAuthenticationFilter { public ApiAuthenticationFilter() { } protected override bool OnAuthorizeUser(string username, string password, HttpActionContext actionContext) { var provider = actionContext.ControllerContext.Configuration .DependencyResolver.GetService(typeof(IUserService)) as IUserService; if (provider != null) { var userId = provider.Authenticate(username, password); if (userId > 0) { var basicAuthenticationIdentity = Thread.CurrentPrincipal.Identity as BasicAuthenticationIdentity; if (basicAuthenticationIdentity != null) basicAuthenticationIdentity.UserId = userId; return true; } } return false; } }
想必都看到上述標記的那句代碼,那么問題就來了,我們是如何通過action上下文獲得其服務的呢?准確的說就是我們是如何注入這些服務的呢?這就是本文的第一大重點,依賴注入服務。
在文章開頭也有說過就是利用微軟的Unity來實現,那么具體是怎么實現的呢?接下來我們開始一起來看看。
Unity
安裝程序包:

接下來我們通過代碼實現所有需要注入的服務,無需通過配置文件來麻煩進行注冊。
定義注冊服務組件接口
public interface IRegisterComponent { void RegisterType<TFrom, TTo>() where TTo : TFrom; }
實現該服務接口並通過Untity容器進行注入服務
internal class RegisterComponent : IRegisterComponent { private readonly IUnityContainer _container; public RegisterComponent(IUnityContainer container) { this._container = container; } public void RegisterType<TFrom, TTo>() where TTo : TFrom { _container.RegisterType<TFrom, TTo>(); } }
定義組件導入該組件接口來注冊服務
public interface IComponent { void SetUp(IRegisterComponent registerTypeComponent); }
導出IComponent組件,通過MEF中組件容器獲得導出組件(有關MEF【擴展性管理框架】詳情請參考園友(Bēniaǒ)的文章)
加載組件【加載實現了IComponent接口的程序集】
public static class ComponentLoader { public static void LoadContainer(IUnityContainer container, string path, string pattern) {
/**獲取指定路徑下匹配的程序集目錄**/ var dirCat = new DirectoryCatalog(path, pattern);
/**導入IComponent的完整名稱**/ var importDef = BuildImportDefinition(); try { using (var aggregateCatalog = new AggregateCatalog()) {
/**將指定目錄添加到聚合目錄**/ aggregateCatalog.Catalogs.Add(dirCat); /**將組件目錄添加到組件容器中**/ using (var componsitionContainer = new CompositionContainer(aggregateCatalog)) { /**從組件容器中獲得指定類型的導入定義**/ IEnumerable<Export> exports = componsitionContainer.GetExports(importDef); /**獲取其值為IComponent並且不為空**/ IEnumerable<IComponent> modules = exports.Select(export => export.Value as IComponent).Where(m => m != null); /**實例化的注冊組件類構造函數組件容器來注入類型**/ var registerComponent = new RegisterComponent(container); foreach (IComponent module in modules) { module.SetUp(registerComponent); } } } } catch (ReflectionTypeLoadException typeLoadException) { var builder = new StringBuilder(); foreach (Exception loaderException in typeLoadException.LoaderExceptions) { builder.AppendFormat("{0}\n", loaderException.Message); } logger.LogError(string.Format("--------通過反射注冊服務,出現異常:{0},異常信息{1}", typeLoadException, builder)); } } private static ImportDefinition BuildImportDefinition() { return new ImportDefinition( def => true, typeof(IComponent).FullName, ImportCardinality.ZeroOrMore, false, false); } }
初始化加載組件並注入實現了IComponent接口的類型並設置到注冊點上
public class Bootstrapper { public static void Initial() { var container = BuildUnityContainer(); /**注意不要添加此句,否則會報錯**/ //DependencyResolver.SetResolver(new UnityDependencyResolver(container)); GlobalConfiguration.Configuration.DependencyResolver = new UnityDependencyResolver(container); } /**建立Unity容器**/ private static IUnityContainer BuildUnityContainer() { var container = new UnityContainer(); RegisterTypes(container); return container; } /**通過加載如下兩個程序集來注冊其類型到Unity容器中**/ public static void RegisterTypes(IUnityContainer container) { ComponentLoader.LoadContainer(container, ".\\bin", "WebAPiFllowUp.dll"); ComponentLoader.LoadContainer(container, ".\\bin", "AppicationServices.dll"); } }
最后一步在全局配置中進行初始化加載並注入
Bootstrapper.Initial();
至此關於如何通過MEF和Unity來依賴注入類型就已經結束,但是到這里我們還有一個問題未解決,那就是我們在應用層寫了相應的接口服務以及其實現,那么我們怎么如何去注入呢?因為我們上面說過只要實現了IComponent接口的類型都將會進行注入,接下來通過MEF中的導入接口以及其實現即可。
在服務層,我們注入IUserService以及ITokenService。
[Export(typeof(IComponent))] public class DependencyResolver : IComponent { public void SetUp(IRegisterComponent registerComponent) { registerComponent.RegisterType<IUserService, UserService>(); registerComponent.RegisterType<ITokenService, TokenService>(); } }
在實體模型層注入IUnitOfWork。
[Export(typeof(IComponent))] public class DependencyResolver : Resolver.IComponent { public void SetUp(IRegisterComponent registerComponent) { registerComponent.RegisterType<IUnitOfWork, UnitOfWork>(); } }
到這里關於依賴注入類型就完美結束,無需通過配置文件來進行繁瑣配置,同理如果需要注入其他類型只需要如上導入IComponent並注冊其類型即可,一勞永逸。接下來一切准備就緒,我們准備開始利用WebAPiTestOnHelpPage。
WebAPiTestOnHelpPage
關於測試WebAPi的工具也有園友給出了相應的工具,但是我找的這個無論是界面還是效果都是非常好的,所以推薦給這個測試工具給大家,這個測試WebAPi的程序包是在WebAPiTest的基礎上進行了更新,WebAPiTest只能運行在MVC3或者4,在MVC5上會報錯,並且版主也未及時進行更新,后有人對其進行了更新使其能在高版本上能愉快的玩耍,我也是通過看評論才發現進一步的解決方案。下載如下程序包

我是挺愛折騰的人,查資料過程中又發現居然有一個可以綁定路由特性(AttributeRouting)的程序包,於是乎我又嘗試了一把,當然你也不必這么做,可以一起看看,配置起來還是非常簡單的。 此時代碼生成成功,我們運行下程序來試試看,媽的,竟然出錯了,如下:

很明顯了,我們只需在Web.Config中下的WebServer節點添加如下即可,結果運行成功:
<validation validateIntegratedModeConfiguration="false" />
AttributeRouting
我們通過程序包繼續下載AttributeRouting.WebApi程序包即可,因為是以WebHost為宿主,所以SelfHost就不用下載。

安裝完成后會在App_Start中自動生成一個AttributeRoutingHttpConfig文件來進行它所給出的路由請求配置。
接下來我們定義一個關於WebAPi的認證請求控制器類即AuthenticationController,具體實現如下:
[ApiAuthenticationFilter] public class AuthenticationController : ApiController { /// <summary> ///注意: WebAPi默認必須要有無參函數,否則報錯 /// </summary> public AuthenticationController() { } private readonly ITokenService _tokenService; private readonly IUserService _userService; public AuthenticationController(ITokenService tokenService,IUserService userService) { _tokenService = tokenService; _userService = userService; } /**此請求方法特性就是利用上述我們添加的程序包來實現的**/ [POST("Authenticate")] public HttpResponseMessage Authenticate() { if (System.Threading.Thread.CurrentPrincipal != null && System.Threading.Thread.CurrentPrincipal.Identity.IsAuthenticated) { var basicAuthenticationIdentity = Thread.CurrentPrincipal.Identity as BasicAuthenticationIdentity; if (basicAuthenticationIdentity != null) { var userId = basicAuthenticationIdentity.UserId; return GetAuthToken(userId); } } return null; } private HttpResponseMessage GetAuthToken(int userId) { var token = _tokenService.GenerateToken(userId); var response = Request.CreateResponse(HttpStatusCode.OK, "Authorized"); response.Headers.Add("Token", token.AuthToken); response.Headers.Add("TokenExpiry", ConfigurationManager.AppSettings["TokenExpiry"]); response.Headers.Add("Access-Control-Expose-Headers", "Token,TokenExpiry"); return response; } }
最后我們來通過WebAPiTestOnHelpPage來測試下。通過locahost:xxx/help,得到如下界面

看到沒,該測試工具會自動識別出你所添加的所有控制器類以及所有的方法,還不強大嗎,我們繼續看。

它會自動給出相應的請求信息以及響應信息,我們點擊TestAPi,即可進入測試界面,如下:

我們還可以在此基礎上添加請求頭,點擊Add header即可,當然也可以刪除,如下:

我們發送點擊發送Send來瞧瞧其結果,我了個去,出錯了,如下

為什么會出現這樣的錯誤呢?因為是我們使用程序包AttributeRoutingWebAPi出現的錯誤,主要錯誤出現在App_Start中其配置文件中,添加默認的會出錯,我們需要將配置文件中的如下:
routes.MapHttpAttributeRoutes();
替換成如下即可:
routes.MapHttpAttributeRoutes(cfg => { cfg.InMemory = true; cfg.AutoGenerateRouteNames = true; cfg.AddRoutesFromAssemblyOf<AuthenticationController>(); });
至此,關於錯誤的解決以及測試工具都已經處理完成,因為文章開頭說的是關於如何保持Session的問題,我們最終回到這個問題上來,在認證控制器中我們添加了一個Authenticate方法來取得當前認證身份是否通過認證,沒有則發起質詢,有的話則通過獲取到UserId來自動生成一個AuthToken並將其添加到請求報文頭中以及還有失效時間。接下來我們來完整的測試整個流程。
假設我們通過登錄界面在數據庫中注冊了如下賬號,如下:

現在我們用上述賬號和密碼進行基礎認證發出的質詢進行登錄試試看,此時會返回如下信息

說明授權成功,我們再來看看數據庫,應該是有數據的,如我所期望的那樣

這僅僅是告訴了我們授權成功,如果我們要想訪問某一個方法時,可能是需要驗證是否有這個權限,所以我們接下來要做的就是驗證我們上述在請求報文頭中是否包含這個Token即可,接下來我們就需要自定義一個實現ActionFilterAttribute特性的特性,我們取名為ActionFilterRequiredAttribute,下面是具體實現
public class ActionFilterRequiredAttribute : ActionFilterAttribute { private const string Token = "Token"; public override void OnActionExecuting(HttpActionContext filterContext) { var provider = filterContext.ControllerContext.Configuration .DependencyResolver.GetService(typeof(ITokenService)) as ITokenService; if (filterContext.Request.Headers.Contains(Token)) { var tokenValue = filterContext.Request.Headers.GetValues(Token).First(); if (provider != null && !provider.ValidateToken(tokenValue)) { var responseMessage = new HttpResponseMessage(HttpStatusCode.Unauthorized) { ReasonPhrase = "Invalid Request" }; filterContext.Response = responseMessage; } } else { filterContext.Response = new HttpResponseMessage(HttpStatusCode.Unauthorized); } base.OnActionExecuting(filterContext); } }
當驗證某個用戶是否有這個權限訪問方法時,只需驗證該請求報文頭中是否包含其Token即可,也就是在方法上添加ActionFilterRequiredAttribute特性即可。如果其失效時間未過期則繼續添加其實現時間,這樣就達到了所謂的保持Session的目的,這樣做也是一個不錯的方案,在WebAPi中默認是關閉Session的,當然你想去用的話只需啟動它即可
Log4net
關於log4net的文章是數不勝數,我只是對其進行了封裝並起名為logger,能滿足絕大多數應用需求。關於要添加什么log4net.dll這些基礎就不廢話了,直接上代碼:
第一步
public class logger { private static ILog Info; private static ILog Error; private static ILog Warn; private static ILog Debug; private static object objectLock = new object(); private static logger _instance; private logger() { Error = LogManager.GetLogger("logerror"); Info = LogManager.GetLogger("loginfo"); Warn = LogManager.GetLogger("logwarn"); Debug = LogManager.GetLogger("logdebug"); } private static logger Instance() { if (_instance == null) { lock (objectLock) { if (_instance == null) { _instance = new logger(); } } } return _instance; } public static void LogInfo(string info) { logger.Instance(); //需要添加操作人id var method = Method(2); var infoLog = string.Format("{0}{1}", method, info); Info.Info(infoLog); } public static void LogInfo(string info,Exception ex) { logger.Instance(); //需要添加操作人id var method = Method(2); var infoLog = string.Format("{0}{1}{2}", method,info, ex); Info.Info(infoLog); } public static void LogWarn(string warn) { logger.Instance(); //需要添加操作人id var method = Method(2); var warnLog = string.Format("{0}{1}", method, warn); Warn.Warn(warnLog); } public static void LogWarn(string warn,Exception ex) { logger.Instance(); //需要添加操作人id var method = Method(2); var warnLog = string.Format("{0}{1}{2}", method,warn, ex); Warn.Warn(warnLog); } public static void LogError(string error) { logger.Instance(); //需要添加操作人id var method = Method(2); var errorLog = string.Format("{0}{1}", method, error); Error.Error(errorLog); } public static void LogError(string error,Exception ex) { logger.Instance(); //需求添加操作人id var method = Method(2); var errorLog = string.Format("{0}{1}{2}", method, error,ex); Error.Error(errorLog); } private static string Method(int i) { var trace = new StackTrace(); var methodInfo = trace.GetFrame(i).GetMethod(); var methodName = methodInfo.Name; var className = methodInfo.DeclaringType.FullName; return string.Format("{0}.{1}", className, methodName).Trim('.'); } }
第二步
將logger該類的屬性中的復制到輸出目錄設置為始終復制
第三步
在該類所在的類庫中的Properties文件夾下的AssemblyInfo類文件添加如下一句
[assembly: XmlConfigurator(ConfigFile = "log4net.config", Watch = true)]
第四步
單獨建立一個log4net.config關於日志的配置文件,添加如下內容
<configuration>
<configSections>
<section name="log4net" type="log4net.Config.Log4NetConfigurationSectionHandler,log4net"/>
</configSections>
<log4net>
<logger name="loginfo">
<level value="INFO"/>
<appender-ref ref="InfoAppender"/>
</logger>
<logger name="logdebug">
<level value="DEBUG"/>
<appender-ref ref="DebugAppender"/>
</logger>
<logger name="logerror">
<level value="ERROR"/>
<appender-ref ref="ErrorAppender"/>
</logger>
<logger name="logwarn">
<level value="WARN"/>
<appender-ref ref="WarningAppender"/>
</logger>
<appender name="ErrorAppender" type="log4net.Appender.RollingFileAppender">
<param name="File" value="D:/log/Error/"/>
<param name="AppendToFile" value="true"/>
<param name="MaxSizeRollBackups" value="10000"/>
<param name="MaxFileSize" value="10240"/>
<param name="StaticLogFileName" value="false"/>
<param name="DatePattern" value="yyyyMMdd".log""/>
<param name="RollingStyle" value="Composite"/>
<layout type="log4net.Layout.PatternLayout">
<conversionPattern value="%date [%thread] %-5level %logger [%property{NDC}] - %message%newline"/>
</layout>
<filter type="log4net.Filter.LevelRangeFilter">
<param name="LevelMin" value="ERROR"/>
<param name="LevelMax" value="ERROR"/>
</filter>
</appender>
<appender name="InfoAppender" type="log4net.Appender.RollingFileAppender">
<param name="File" value="D:/log/Info/"/>
<param name="AppendToFile" value="true"/>
<param name="MaxFileSize" value="10000"/>
<param name="MaxSizeRollBackups" value="200"/>
<param name="StaticLogFileName" value="false"/>
<param name="DatePattern" value="yyyyMMdd".log""/>
<param name="RollingStyle" value="Composite"/>
<layout type="log4net.Layout.PatternLayout">
<conversionPattern value="%date [%thread] %-5level %logger [%property{NDC}] - %message%newline"/>
</layout>
</appender>
<appender name="WarningAppender" type="log4net.Appender.RollingFileAppender">
<param name="File" value="D:/log/Warn/"/>
<param name="AppendToFile" value="true"/>
<param name="MaxFileSize" value="10000"/>
<param name="MaxSizeRollBackups" value="200"/>
<param name="StaticLogFileName" value="false"/>
<param name="DatePattern" value="yyyyMMdd".log""/>
<param name="RollingStyle" value="Composite"/>
<layout type="log4net.Layout.PatternLayout">
<conversionPattern value="%date [%thread] %-5level %logger [%property{NDC}] - %message%newline"/>
</layout>
</appender>
<appender name="DebugAppender" type="log4net.Appender.RollingFileAppender">
<param name="File" value="D:/log/Debug/"/>
<param name="AppendToFile" value="true"/>
<param name="MaxFileSize" value="10000"/>
<param name="MaxSizeRollBackups" value="200"/>
<param name="StaticLogFileName" value="false"/>
<param name="DatePattern" value="yyyyMMdd".log""/>
<param name="RollingStyle" value="Composite"/>
<layout type="log4net.Layout.PatternLayout">
<conversionPattern value="%date [%thread] %-5level %logger [%property{NDC}] - %message%newline"/>
</layout>
</appender>
</log4net>
</configuration>
至於以上log4net各個參數的含義自行查資料了解。最后生成如下文件夾

在配置文件中是按照日期來進行日志的記錄,如下:

總結
最后的最后還是依然來個總結,本文比較詳細的介紹如何去維護和保持Session,同時也涉及到了一些知識就如已經提過的Unity、Log4net、MEF、WebAPiTestOnHelpPage等,在WebAPi默認是關閉Session,如果我們想去利用Session的話還得手動去啟動它,但是在本文中並未如此實現,用建立票據表的形式來管理其所謂的Session也是一種不錯的解決方案。不知不覺寫博客已經到一點了,終於Over,休息。
