微軟利用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