WebAPI授权认证注意事项
今天搞了一天的webapi授权,刚开始想用微软的Authorization去做,虽然强大的微软提供了多种验证方式,但是我还是没用。
原因是要实现微软定义的认证方式,服务端和客户端都需要实现太多了。如下图
后来自己定了一个流程,即调用webapi的接口前,客户端登录一次,如下图
只是Token创建后,除了更新数据库对应的字段,同时也将Token存入缓存。后台有一个Token对象,这个对象包含了生成的Token值以及该Token对应的
客户端信息,。
然后实现的话也比较简单,继承AuthorizationFilterAttribute刷新器,重写OnAuthorization方法,在该方法中验证Token,失败则通知客户端。
本来这个思路真的很美好啊,但是我的webapi和mvc在同一个站点!!
同一个站点有个坑爹地方,就是我mvc已经实现了微软的认证方式,如下图
然后webapi只要认证未通过就会重定向到登录页面。。。。
我特么为了这个问题,困扰好久,将继承刷选器更新为继承DelegatingHandler,然并卵。。。
知道原因么?就是我在给response的StatusCode赋值不应该赋值HttpStatusCode.Unauthorized,因为给StatusCode赋值为HttpStatusCode.Unauthorized后,系统会走mvc的认证失败流程。。。
好了,唠叨完了,直接看代码,下面是登录的Webapi
1 [HttpPost] 2 [SwaggerActionFilter] 3 [NoAuthentication] 4 public IHttpActionResult Validate(dynamic post) 5 { 6 try 7 { 8 9 string orgCode = post.orgCode; 10 string userName = post.userName; 11 string password = post.password; 12 if (string.IsNullOrEmpty(orgCode)) 13 return Json(new BaseApiRelust(ApiErrCodes.必传参数为空, $"参数名[{nameof(orgCode)}]")); 14 15 if (string.IsNullOrEmpty(userName)) 16 return Json(new BaseApiRelust(ApiErrCodes.必传参数为空, $"参数名[{nameof(userName)}]")); 17 18 if (string.IsNullOrEmpty(password)) 19 return Json(new BaseApiRelust(ApiErrCodes.必传参数为空, $"参数名[{nameof(password)}]")); 20 21 string key = "test2017"; 22 //解密 23 string pwd = EncryUtils.DESDecrypt(password, key); 24 25 ResponseBase<bool> funRes = InvoiceProcessor.ValidateOrgUser(orgCode, userName, pwd); 26 if (funRes.Error != null) 27 { 28 return Json(new BaseApiRelust(ApiErrCodes.数据处理异常, funRes.Msg)); 29 } 30 if (!funRes.Result) 31 { 32 return Json(new BaseApiRelust(ApiErrCodes.数据处理失败, funRes.Msg)); 33 } 34 /*先搜索当前业务的Token缓存集合是否有对应的Token, 35 如果有先清除再新增,这里将加密后的pwd存储到服务器,将他设为token*/ 36 SiUser user = (SiUser)funRes.OutData; 37 WebApiToken token = CreateToken(user.Id, user.Name, user.PassWord, WebApiBusType.InvoiceWeixin); 38 InvoiceProcessor.AddWebApiToken(user.Id, token); 39 return Json(new BaseApiRelust(ApiErrCodes.成功, token.Token)); 40 } 41 catch (Exception ex) 42 { 43 return Json(new BaseApiRelust(ApiErrCodes.数据处理异常, ex.Message)); 44 } 45 }
下面是WebApiToken的对象
1 /// <summary> 2 /// webapi授权token 3 /// </summary> 4 public class WebApiToken 5 { 6 /// <summary> 7 /// 用户id 8 /// </summary> 9 public Guid UserId { get; set; } 10 11 /// <summary> 12 /// 用户姓名 13 /// </summary> 14 public string UserName { get; set; } 15 16 /// <summary> 17 /// 业务类型 18 /// </summary> 19 public WebApiBusType Type { get; set; } 20 21 /// <summary> 22 /// Token值 23 /// </summary> 24 public string Token { get; set; } 25 }
下面是创建token的方法
/// <summary> /// 生成Token /// </summary> /// <typeparam name="T">账户id类型</typeparam> /// <param name="id">账户id</param> /// <param name="name">账户名</param> /// <param name="pwd">密码</param> /// <param name="busType">webapi业务类型</param> /// <returns></returns> public WebApiToken CreateToken(Guid id, string name, string pwd, WebApiBusType busType) { string note = $"id={id}&name={name}&pwd={pwd}&busType={busType.ToString()}"; string key = RandomHelper.CreateRandomCode(8); string encry = EncryUtils.DESEncrypt(note, key); WebApiToken token = new WebApiToken { Token = encry, Type = busType, UserId = id, UserName = name }; return token; }
这里创建Token我传了一个WebApiBusType的枚举,这个枚举用来区分webapi的模块,用于进一步的权限控制;
接下来,是更新db的用户token以及缓存的token
1 /// <summary> 2 /// 新增webapi的授权token 3 /// </summary> 4 /// <param name="userId">用户id</param> 5 /// <param name="token">token值</param> 6 public static void AddWebApiToken(Guid userId, WebApiToken token) 7 { 8 string key = CacheKeys.WebApiToken.ToString(); 9 //移除该用户过期Token 10 List<WebApiToken> AuthentTokens = Air.Infrastructure.Cache.CacheManager.Current.Get<List<WebApiToken>>(key); 11 //根据用户id在数据库中获取Token ,然后再拼接成对应的缓存值到缓存集合中检索是否存在 12 SiUser user = SiUserManager.Current.FindByIdAsync(userId).Result; 13 WebApiToken oldValue = new WebApiToken 14 { 15 UserId = user.Id, 16 Token = user.Token, 17 Type = WebApiBusType.InvoiceWeixin 18 }; 19 if (AuthentTokens != null) 20 AuthentTokens.Remove(oldValue); 21 else 22 AuthentTokens = new List<WebApiToken>(); 23 AuthentTokens.Add(token); 24 Infrastructure.Cache.CacheManager.Current.Remove<List<string>>(key); 25 Infrastructure.Cache.CacheManager.Current.AddOrUpdate(key, AuthentTokens); 26 //更新数据库Token值 27 user.Token = token.Token; 28 SiUserManager.Current.UpdateWebApiToken(user); 29 }
这里可以看出来Token是放到了缓存,从缓存获取到token后我们就可以知道是哪个用户在调用,根据WebApiBusType我们可以新增一个刷选器限制访问级别;
接下来看认证刷选器是如何实现的:
1 using Air.Data.Enums; 2 using Air.Data.Transfer; 3 using System; 4 using System.Collections.Generic; 5 using System.Linq; 6 using System.Threading.Tasks; 7 using System.Web.Http; 8 using System.Web.Http.Controllers; 9 using System.Web.Http.Filters; 10 using System.Net.Http; 11 using Newtonsoft.Json; 12 using System.Text; 13 using Newtonsoft.Json.Linq; 14 15 namespace Air.Api.Filters 16 { 17 /// <summary> 18 /// 认证刷选器 :System.Web.Http.AuthorizeAttribute System.Web.Http.AuthorizeAttribute// AuthorizationFilterAttribute 19 /// :ActionFilterAttribute 20 /// </summary> 21 [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, Inherited = true)] 22 public class WebApiAuthenticationAttribute : AuthorizationFilterAttribute 23 { 24 const string TOKEN_NAME = "X-Token"; 25 /// <summary> 26 /// 重写请求授权 27 /// </summary> 28 /// <param name="actionContext"></param> 29 public override void OnAuthorization(HttpActionContext actionContext) 30 { 31 if (!IsAnonymous(actionContext)) 32 { 33 HttpContent content = actionContext.Request.Content; 34 string encryptedToken = actionContext.Request.Headers.GetValues(TOKEN_NAME).First(); 35 ResponseBase<bool> response = ValidateToken(encryptedToken); 36 if (!response.Result) 37 { 38 SetUnauthorizedResponse(actionContext); 39 WebApiToken token = (WebApiToken)response.OutData; 40 return; 41 } 42 //是否有此action权限 43 if (!ValidateAuth(actionContext, encryptedToken)) 44 { 45 SetUnauthorizedResponse(actionContext); 46 return; 47 } 48 } 49 } 50 private void SetUnauthorizedResponse(HttpActionContext actionContext) 51 { 52 var response = actionContext.Response = actionContext.Response ?? new HttpResponseMessage(); 53 var content = new BaseApiRelust(ApiErrCodes.未授权, "当前访问身份无效!"); 54 response.Content = new StringContent(JsonConvert.SerializeObject(content), Encoding.UTF8, "application/json"); 55 } 56 57 58 /// <summary> 59 /// 是否跳过授权验证 60 /// </summary> 61 /// <param name="actionContext"></param> 62 /// <returns></returns> 63 private bool IsAnonymous(HttpActionContext actionContext) 64 { 65 if (actionContext.ActionDescriptor.GetCustomAttributes<NoAuthenticationAttribute>().Any()|| 66 actionContext.ActionDescriptor.GetCustomAttributes<AllowAnonymousAttribute>().Any()) 67 return true; 68 return false; 69 } 70 /// <summary> 71 /// 验证Token是否存在 72 /// </summary> 73 /// <param name="value"></param> 74 /// <returns></returns> 75 private ResponseBase<bool> ValidateToken(string value) 76 { 77 /*从缓存中获取*/ 78 ResponseBase<bool> responese = new ResponseBase<bool>(); 79 string key = Air.Data.Enums.CacheKeys.WebApiToken.ToString(); 80 List<WebApiToken> tokenLst = Air.Infrastructure.Cache.CacheManager.Current.Get<List<WebApiToken>>(key); 81 WebApiToken token = tokenLst?.SingleOrDefault(p => p.Token == value); 82 responese.Result = token == null ? false : true; 83 responese.OutData = token; 84 return responese; 85 } 86 87 /// <summary> 88 /// 验证是否有权限访问Action 89 /// </summary> 90 /// <param name="actionContext"></param> 91 /// <param name="value"></param> 92 /// <returns></returns> 93 private bool ValidateAuth(HttpActionContext actionContext, string value) 94 { 95 string key = Air.Data.Enums.CacheKeys.WebApiToken.ToString(); 96 List<WebApiToken> tokenLst = Air.Infrastructure.Cache.CacheManager.Current.Get<List<WebApiToken>>(key); 97 WebApiToken token = tokenLst?.FirstOrDefault(p => p.Token == value); 98 if (token == null) return false; 99 100 var moduleAuthorizationAction = actionContext.ActionDescriptor.GetCustomAttributes<ModuleAuthorizationAttribute>(); 101 if (moduleAuthorizationAction.Any()) 102 { 103 WebApiBusType busType = token.Type; 104 if (!moduleAuthorizationAction[0].Authorizations.Contains(busType)) 105 return false; 106 } 107 return true; 108 } 109 110 } 111 }
还有一个特性就是ModuleAuthorizationAttribute,这个特性就是注明webapi能够给哪几个WebApiBusType枚举值调用,代码如下:
1 /// <summary> 2 /// 权限控制标记 3 /// </summary> 4 [AttributeUsage(AttributeTargets.Method, Inherited = true, AllowMultiple = true)] 5 public class ModuleAuthorizationAttribute : Attribute 6 { 7 /// <summary> 8 /// 权限控制构造函数 9 /// </summary> 10 /// <param name="authorization">允许操作的模块</param> 11 public ModuleAuthorizationAttribute(params WebApiBusType[] authorization) 12 { 13 this.Authorizations = authorization; 14 } 15 16 /// <summary> 17 /// 允许访问的模块 18 /// </summary> 19 public WebApiBusType[] Authorizations { get; set; } 20 }
好了,整个完成了。客户端试下就是这结果: