Web APi之认证(Authentication)两种实现方式后续【三】(十五)
前言
之前一直在找工作中,过程也是令人着实的心塞,最后还是稳定了下来,博客也停止更新快一个月了,学如逆水行舟,不进则退,之前学的东西没怎么用,也忘记了一点,不过至少由于是切身研究,本质以及原理上的脉络还是知其所以然,所以也无关紧要,停止学习以及分享是一件很痛苦的事情,心情很忐忑也很担忧,那么多牛逼的人都在无时无刻的学习更何况是略懂皮毛的我呢?好了,废话说了不少,我们接下来进入主题。
话题
看到博客也有对于我最近有关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,休息。