自定义分布式RESTful API鉴权机制
微软利用OAuth2为RESTful API提供了完整的鉴权机制,但是可能微软保姆做的太完整了,在这个机制中指定了数据持久化的方法是用EF,而且对于用户、权限等已经进行了封装,对于系统中已经有了自己的用户表,和不是采用EF做持久化的系统来说限制太大,不自由,而且现实中很多情况下,授权服务器和资源服务器并不是同一个服务器,甚至数据库用的都不是同一个数据库,这种情况下直接利用微软的授权机制会有一定的麻烦,基于这种情况不如自己实现一套完整的分布式的鉴权机制。
自定义的鉴权机制中利用redis来缓存授权令牌信息,结构大体如下:
从图上可以看出,用户登录成功后的用户信息缓存在redis,用户带着令牌访问资源服务器时再对令牌进行解密,获取到key,然后就可以获取到用户信息,可以根据判断redis中是否有这个key来校验用户是否经过授权,大体思路就是这些,下边就是具体的代码实现
redis的操作帮助类,帮助类中实现了redis的读写分离
public class RedisBase { #if DEBUG private static string[] ReadWriteHosts = { "127.0.0.1:6379" }; private static string[] ReadOnlyHosts = { "127.0.0.1:6379" }; #endif #if !DEBUG private static string[] ReadWriteHosts = System.Configuration.ConfigurationSettings.AppSettings["RedisWriteHosts"].Split(new char[] { ';' }); private static string[] ReadOnlyHosts = System.Configuration.ConfigurationSettings.AppSettings["RedisReadOnlyHosts"].Split(new char[] { ';' }); #endif #region -- 连接信息 -- public static PooledRedisClientManager prcm = CreateManager(ReadWriteHosts, ReadOnlyHosts); private static PooledRedisClientManager CreateManager(string[] readWriteHosts, string[] readOnlyHosts) { // 支持读写分离,均衡负载 return new PooledRedisClientManager(readWriteHosts, readOnlyHosts, new RedisClientManagerConfig { MaxWritePoolSize = 5, // “写”链接池链接数 MaxReadPoolSize = 5, // “读”链接池链接数 AutoStart = true, }); } #endregion #region KEY #endregion #region -- Item String -- /// <summary> /// 设置单体 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="key"></param> /// <param name="t"></param> /// <param name="timeSpan"></param> /// <returns></returns> public static bool Item_Set<T>(string key, T t) { try { using (IRedisClient redis = prcm.GetClient()) { return redis.Set<T>(key, t, new TimeSpan(1, 0, 0)); } } catch (Exception ex) { // LogInfo } return false; } public static int String_Append(string key, string value) { try { using (IRedisClient redis = prcm.GetClient()) { return redis.AppendToValue(key, value); } } catch (Exception ex) { } return 0; } /// <summary> /// 获取单体 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="key"></param> /// <returns></returns> public static T Item_Get<T>(string key) where T : class { using (IRedisClient redis = prcm.GetReadOnlyClient()) { return redis.Get<T>(key); } } /// <summary> /// 移除单体 /// </summary> /// <param name="key"></param> public static bool Item_Remove(string key) { using (IRedisClient redis = prcm.GetClient()) { return redis.Remove(key); } } /// <summary> /// 根据指定的Key,将值加1(仅整型有效) /// </summary> /// <param name="key"></param> /// <returns></returns> public static long IncrementValue(string key) { using (IRedisClient redis = prcm.GetClient()) { return redis.IncrementValue(key); } } /// <summary> /// 根据指定的Key,将值加上指定值(仅整型有效) /// </summary> /// <param name="key"></param> /// <param name="count"></param> /// <returns></returns> public static long IncrementValueBy(string key,int count) { using (IRedisClient redis = prcm.GetClient()) { return redis.IncrementValueBy(key,count); } } /// <summary> /// 设置过期时间 /// </summary> /// <param name="key"></param> /// <param name="time"></param> /// <returns></returns> public static bool ExpireEntryIn(string key, TimeSpan time) { using (IRedisClient redis = prcm.GetClient()) { return redis.ExpireEntryIn(key, time); } } /// <summary> /// 设置过期时间 /// </summary> /// <param name="key"></param> /// <param name="expireAt"></param> /// <returns></returns> public static bool ExpireEntryAt(string key, DateTime expireAt) { using (IRedisClient redis = prcm.GetClient()) { return redis.ExpireEntryAt(key, expireAt); } } /// <summary> /// 判断key是否已存在 /// </summary> /// <param name="key"></param> /// <returns></returns> public static bool ContainsKey(string key) { using (IRedisClient redis = prcm.GetReadOnlyClient()) { return redis.ContainsKey(key); } } #endregion #region -- List -- public static void List_Add<T>(string key, T t) { using (IRedisClient redis = prcm.GetClient()) { var redisTypedClient = redis.As<T>(); redisTypedClient.AddItemToList(redisTypedClient.Lists[key], t); } } public static bool List_Remove<T>(string key, T t) { using (IRedisClient redis = prcm.GetClient()) { var redisTypedClient = redis.As<T>(); return redisTypedClient.RemoveItemFromList(redisTypedClient.Lists[key], t) > 0; } } public static void List_RemoveAll<T>(string key) { using (IRedisClient redis = prcm.GetClient()) { var redisTypedClient = redis.As<T>(); redisTypedClient.Lists[key].RemoveAll(); } } public static long List_Count(string key) { using (IRedisClient redis = prcm.GetReadOnlyClient()) { return redis.GetListCount(key); } } public static List<T> List_GetRange<T>(string key, int start, int count) { using (IRedisClient redis = prcm.GetReadOnlyClient()) { var c = redis.As<T>(); return c.Lists[key].GetRange(start, start + count - 1); } } public static List<T> List_GetList<T>(string key) { using (IRedisClient redis = prcm.GetReadOnlyClient()) { var c = redis.As<T>(); return c.Lists[key].GetRange(0, c.Lists[key].Count); } } public static List<T> List_GetList<T>(string key, int pageIndex, int pageSize) { int start = pageSize * (pageIndex - 1); return List_GetRange<T>(key, start, pageSize); } /// <summary> /// 设置缓存过期 /// </summary> /// <param name="key"></param> /// <param name="datetime"></param> public static void List_SetExpire(string key, DateTime datetime) { using (IRedisClient redis = prcm.GetClient()) { redis.ExpireEntryAt(key, datetime); } } #endregion #region -- Set -- public static void Set_Add<T>(string key, T t) { using (IRedisClient redis = prcm.GetClient()) { var redisTypedClient = redis.As<T>(); redisTypedClient.Sets[key].Add(t); } } public static bool Set_Contains<T>(string key, T t) { using (IRedisClient redis = prcm.GetClient()) { var redisTypedClient = redis.As<T>(); return redisTypedClient.Sets[key].Contains(t); } } public static bool Set_Remove<T>(string key, T t) { using (IRedisClient redis = prcm.GetClient()) { var redisTypedClient = redis.As<T>(); return redisTypedClient.Sets[key].Remove(t); } } #endregion #region -- Hash -- /// <summary> /// 判断某个数据是否已经被缓存 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="key"></param> /// <param name="dataKey"></param> /// <returns></returns> public static bool Hash_Exist<T>(string key, string dataKey) { using (IRedisClient redis = prcm.GetReadOnlyClient()) { return redis.HashContainsEntry(key, dataKey); } } /// <summary> /// 存储数据到hash表 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="key"></param> /// <param name="dataKey"></param> /// <returns></returns> public static bool Hash_Set<T>(string key, string dataKey, T t) { using (IRedisClient redis = prcm.GetClient()) { string value = ServiceStack.Text.JsonSerializer.SerializeToString<T>(t); return redis.SetEntryInHash(key, dataKey, value); } } /// <summary> /// 移除hash中的某值 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="key"></param> /// <param name="dataKey"></param> /// <returns></returns> public static bool Hash_Remove(string key, string dataKey) { using (IRedisClient redis = prcm.GetClient()) { return redis.RemoveEntryFromHash(key, dataKey); } } /// <summary> /// 移除整个hash /// </summary> /// <typeparam name="T"></typeparam> /// <param name="key"></param> /// <param name="dataKey"></param> /// <returns></returns> public static bool Hash_Remove(string key) { using (IRedisClient redis = prcm.GetClient()) { return redis.Remove(key); } } /// <summary> /// 从hash表获取数据 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="key"></param> /// <param name="dataKey"></param> /// <returns></returns> public static T Hash_Get<T>(string key, string dataKey) { using (IRedisClient redis = prcm.GetReadOnlyClient()) { string value = redis.GetValueFromHash(key, dataKey); return ServiceStack.Text.JsonSerializer.DeserializeFromString<T>(value); } } /// <summary> /// 获取整个hash的数据 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="key"></param> /// <returns></returns> public static List<T> Hash_GetAll<T>(string key) { using (IRedisClient redis = prcm.GetReadOnlyClient()) { var list = redis.GetHashValues(key); if (list != null && list.Count > 0) { List<T> result = new List<T>(); foreach (var item in list) { var value = ServiceStack.Text.JsonSerializer.DeserializeFromString<T>(item); result.Add(value); } return result; } return null; } } /// <summary> /// 设置缓存过期 /// </summary> /// <param name="key"></param> /// <param name="datetime"></param> public static void Hash_SetExpire(string key, DateTime datetime) { using (IRedisClient redis = prcm.GetClient()) { redis.ExpireEntryAt(key, datetime); } } #endregion #region -- SortedSet -- /// <summary> /// 添加数据到 SortedSet /// </summary> /// <typeparam name="T"></typeparam> /// <param name="key"></param> /// <param name="t"></param> /// <param name="score"></param> public static bool SortedSet_Add<T>(string key, T t, double score) { using (IRedisClient redis = prcm.GetClient()) { string value = ServiceStack.Text.JsonSerializer.SerializeToString<T>(t); return redis.AddItemToSortedSet(key, value, score); } } /// <summary> /// 为有序集 key 的成员 member 的 score 值加上增量 increment /// 可以通过传递一个负数值 increment ,让 score 减去相应的值 /// 当 key 不存在,或 member 不是 key 的成员时, ZINCRBY key increment member 等同于 ZADD key increment member /// </summary> /// <typeparam name="T"></typeparam> /// <param name="key"></param> /// <param name="t"></param> /// <param name="incrementBy"></param> /// <returns></returns> public static double SortedSet_Zincrby<T>(string key,T t,double incrementBy) { using (IRedisClient redis = prcm.GetClient()) { string value = ServiceStack.Text.JsonSerializer.SerializeToString<T>(t); return redis.IncrementItemInSortedSet(key, value, incrementBy); } } /// <summary> /// 返回有序集 key 中,成员 member 的 score 值。 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="key"></param> /// <param name="t">member</param> /// <returns></returns> public static double SortedSet_ZSCORE<T>(string key,T t) { using (IRedisClient redis = prcm.GetReadOnlyClient()) { string value = ServiceStack.Text.JsonSerializer.SerializeToString<T>(t); return redis.GetItemScoreInSortedSet(key, value); } } /// <summary> /// 移除数据从SortedSet /// </summary> /// <typeparam name="T"></typeparam> /// <param name="key"></param> /// <param name="t"></param> /// <returns></returns> public static bool SortedSet_Remove<T>(string key, T t) { using (IRedisClient redis = prcm.GetClient()) { string value = ServiceStack.Text.JsonSerializer.SerializeToString<T>(t); return redis.RemoveItemFromSortedSet(key, value); } } /// <summary> /// 修剪SortedSet /// </summary> /// <param name="key"></param> /// <param name="size">保留的条数</param> /// <returns></returns> public static long SortedSet_Trim(string key, int size) { using (IRedisClient redis = prcm.GetClient()) { return redis.RemoveRangeFromSortedSet(key, size, 9999999); } } /// <summary> /// 获取SortedSet的长度 /// </summary> /// <param name="key"></param> /// <returns></returns> public static long SortedSet_Count(string key) { using (IRedisClient redis = prcm.GetReadOnlyClient()) { return redis.GetSortedSetCount(key); } } /// <summary> /// 按照由小到大排序获取SortedSet的分页数据 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="key"></param> /// <param name="pageIndex"></param> /// <param name="pageSize"></param> /// <returns></returns> public static List<T> SortedSet_GetList<T>(string key, int pageIndex, int pageSize) { using (IRedisClient redis = prcm.GetReadOnlyClient()) { var list = redis.GetRangeFromSortedSet(key, (pageIndex - 1) * pageSize, pageIndex * pageSize - 1); if (list != null && list.Count > 0) { List<T> result = new List<T>(); foreach (var item in list) { var data = ServiceStack.Text.JsonSerializer.DeserializeFromString<T>(item); result.Add(data); } return result; } } return null; } /// <summary> /// 按照由大到小顺序获取SortedSet的分页数据 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="key"></param> /// <param name="pageIndex"></param> /// <param name="pageSize"></param> /// <returns></returns> public static List<T> SortedSet_GetListDesc<T>(string key, int pageIndex, int pageSize) { using (IRedisClient redis = prcm.GetReadOnlyClient()) { var list = redis.GetRangeFromSortedSetDesc(key, (pageIndex - 1) * pageSize, pageIndex * pageSize - 1); if (list != null && list.Count > 0) { List<T> result = new List<T>(); foreach (var item in list) { var data = ServiceStack.Text.JsonSerializer.DeserializeFromString<T>(item); result.Add(data); } return result; } } return null; } /// <summary> /// 按照由大到小的顺序获取SortedSet包含分数的分页数据 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="key"></param> /// <param name="pageIndex"></param> /// <param name="pageSize"></param> /// <returns></returns> public static IDictionary<T,double> SortedSet_GetListWidthScoreDesc<T>(string key, int pageIndex, int pageSize) { using (IRedisClient redis = prcm.GetReadOnlyClient()) { var list = redis.GetRangeWithScoresFromSortedSetDesc(key, (pageIndex - 1) * pageSize, pageIndex * pageSize - 1); if (list != null && list.Count > 0) { IDictionary<T,double> result = new Dictionary<T,double>(); foreach (var item in list) { var data = ServiceStack.Text.JsonSerializer.DeserializeFromString<T>(item.Key); result[data]=item.Value; } return result; } } return null; } /// <summary> /// 获取SortedSet的全部数据 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="key"></param> /// <param name="pageIndex"></param> /// <param name="pageSize"></param> /// <returns></returns> public static List<T> SortedSet_GetListALL<T>(string key) { using (IRedisClient redis = prcm.GetReadOnlyClient()) { var list = redis.GetRangeFromSortedSet(key, 0, 9999999); if (list != null && list.Count > 0) { List<T> result = new List<T>(); foreach (var item in list) { var data = ServiceStack.Text.JsonSerializer.DeserializeFromString<T>(item); result.Add(data); } return result; } } return null; } /// <summary> /// 设置缓存过期 /// </summary> /// <param name="key"></param> /// <param name="datetime"></param> public static void SortedSet_SetExpire(string key, DateTime datetime) { using (IRedisClient redis = prcm.GetClient()) { redis.ExpireEntryAt(key, datetime); } } #endregion }
用户类UserInfo
public class UserInfo { }
用户管理类,在asp.net web api中添加一个用户管理类,用来管理用户信息
public class ApiUserManager { private HttpActionContext actionContext; public ApiUserManager(HttpActionContext actionContext) { this.actionContext = actionContext; } private UserInfo _User; /// <summary> /// 当前用户 /// </summary> public UserInfo User { get { if (_User==null) { string key = GetKey(); if (!string.IsNullOrEmpty(key) && RedisBase.ContainsKey(key)) { _User = RedisBase.Item_Get<UserInfo>(key); } } return _User; } } string GetKey() { if (actionContext.Request.Headers.Contains("Authorization")) { string base64Code = actionContext.Request.Headers.GetValues("Authorization").FirstOrDefault(); //code结构为:userid-UserAgent.MD5()-随机数-时间戳 string code = EncryptUtil.UnBase64(base64Code); string[] para = code.Split(new[] { "-" }, StringSplitOptions.RemoveEmptyEntries); string key = (para[0] + para[1] + para[3]).MD5(); return key; } return string.Empty; } /// <summary> /// 用户是否已经登录 /// </summary> /// <returns></returns> public bool ExistsLogin() { string base64Code = string.Empty; if (actionContext.Request.Headers.Contains("Authorization")) { base64Code = actionContext.Request.Headers.GetValues("Authorization").FirstOrDefault(); } if (base64Code.IsNull()) { return false; } //code结构为:userid-UserAgent.MD5()-随机数-时间戳 string code = EncryptUtil.UnBase64(base64Code); string[] para = code.Split(new[] { "-" }, StringSplitOptions.RemoveEmptyEntries); if (para.Length != 4) { return false; } string key = (para[0] + para[1] + para[3]).MD5(); if (!RedisBase.ContainsKey(key)) { return false; } return true; } /// <summary> /// 用户登录返回令牌 /// </summary> /// <param name="user"></param> /// <returns></returns> public string GetUserToken(UserInfo user) { string uagin = actionContext.Request.Headers.UserAgent.TryToString().MD5(); string rm = Utils.GenPsw(11,11); long time = Utils.GetUnixTime(); string code = string.Format("{0}-{1}-{2}-{3}", user.ID, uagin, rm, time); string token = EncryptUtil.Base64(code); string key = (user.ID + uagin + time).MD5(); RedisBase.Item_Set(key,user); RedisBase.ExpireEntryAt(key,DateTime.Now.AddDays(2)); return token; } /// <summary> /// 刷新当前用户信息【修改用户信息后刷新用户信息到缓存中】 /// </summary> public void RefreshUser() { string key = GetKey(); if (RedisBase.ContainsKey(key)) { RedisBase.Item_Set(key,User); } } }
获取授权接口【用户登录接口】,在授权接口中校验用户名密码,成功后将用户信息存入redis,产生令牌,并返回
public class AccountController : ApiController { UserInfoBll ubll = new UserInfoBll(); /// <summary> /// 登录、根据用户名密码获取令牌 /// </summary> /// <param name="username"></param> /// <param name="password"></param> /// <returns></returns> [HttpGet] [Route("token")] public JsonResult<string> Token(string username, string password) { JsonResult<string> result = new JsonResult<string>(); result.code = 0; result.msg = "OK"; UserInfo user = ubll.UserLogin(username, password); if (user == null) { result.Result = "用户名或者密码错误"; } else { ApiUserManager userManager = new ApiUserManager(ActionContext); result.Result = userManager.GetUserToken(user); result.code = 1; result.msg = "OK"; } return result; } }
自定义webAPI的过滤器,在过滤器中校验用户是否已经登录,过滤器中还可以设置只能指定的UserAgent可以访问接口,这样就可以让不同的客户端在请求的时候自定义不同的UserAgent,并且可以限制浏览器访问,做到一定的安全性,针对客户端的限制除了利用UserAgent之外还可以在http请求头自定义一个ClientID,为每个客户端配备一个ID,在过滤器中校验ClientID是否合法即可
public class ApiActionFilterAttribute: ActionFilterAttribute { /// <summary> /// 签名参数 /// </summary> public string[] Signpara { get; set; } public string[] Cachepara { get; set; } private bool _IsCache = false; public bool IsCache { get { return _IsCache; } set { _IsCache = value; } } private bool _IsSigna = false; public bool IsSigna { get { return _IsSigna; } set { _IsSigna = value; } } private bool _IsUrlDecode = false; /// <summary> /// 是否解码 /// </summary> public bool IsUrlDecode { get { return _IsUrlDecode; } set { _IsUrlDecode = value; } } private ClientEnum _CheckClient = ClientEnum.NoCheck; public ClientEnum CheckClient { get { return _CheckClient; } set { _CheckClient = value; } } private bool _IsLogin; /// <summary> /// 是否登录 /// </summary> public bool IsLogin { get { return _IsLogin; } set { _IsLogin = value; } } /// <summary> /// 缓存超时时间 /// </summary> public int TimeOut { get; set; } public override void OnActionExecuting(HttpActionContext actionContext) { JsonResult<string> result = new JsonResult<string>(); if (CheckClient == ClientEnum.WindowsClient && !actionContext.Request.Headers.UserAgent.TryToString().Equals(SystemSet.WindowsClientUserAgent)) { result.code = -21; result.msg = "illegal client"; //filterContext.HttpContext.Response.Status = HttpStatusCode.OK; actionContext.Response = actionContext.Request.CreateResponse(HttpStatusCode.OK, result); return; } if (CheckClient == ClientEnum.WebClient && !actionContext.Request.Headers.UserAgent.TryToString().Equals(SystemSet.WebClientUserAgent)) { result.code = -21; result.msg = "illegal client"; //filterContext.HttpContext.Response.Status = HttpStatusCode.OK; actionContext.Response = actionContext.Request.CreateResponse(HttpStatusCode.OK, result); return; } if (IsLogin) { ApiUserManager userManager = new ApiUserManager(actionContext); if (!userManager.ExistsLogin()) { result.code = -22; result.msg = "illegal user"; actionContext.Response = actionContext.Request.CreateResponse(HttpStatusCode.OK, result); return; } } base.OnActionExecuting(actionContext); } }
过滤器的使用可以针对单独一个接口进行限制,也可以针对一个控制器中的所有接口进行限制。
针对单独一个接口限制
/// <summary> /// 获取当前登录用户信息 /// </summary> /// <returns></returns> [HttpGet] [Route("userinfo")] [ApiActionFilterAttribute(IsLogin = true)] public async Task<IHttpActionResult> Userinfo() { JsonResult<UserInfo> result = new JsonResult<UserInfo>(); result.code = 1; result.msg = "OK"; ApiUserManager userManager = new ApiUserManager(ActionContext); result.Result = userManager.User; return Ok(result); }
针对整个控制器进行限制的话可以自定义一个控制器基类BaseApiController,所有需要验证用户登录的控制器继承它就可以
[DExceptionFilterAttribute] [ApiActionFilterAttribute(IsLogin = true)] public class BaseApiController: ApiController { private UserInfo _User; /// <summary> /// 当前登录用户 /// </summary> public UserInfo User { get { if (_User==null) { ApiUserManager userManager = new ApiUserManager(this.ActionContext); _User = userManager.User; } return _User; } } }
至此授权和鉴权的机制基本已经完成,单独的资源服务器只需要使用公用的redis服务即可,而且redis也可以进行读写分离,就可以进行分布式部署。
项目源码地址:
https://github.com/liemei/asp.netOpenService.git
作者: 刘帅超
出处: http://www.cnblogs.com/liemei
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出, 原文链接
如有问题, 可邮件(liushuaichao159@163.com)咨询.