asp.net 用JWT來實現token以此取代Session

先說一下爲何要寫這一篇博客吧,其實我的有關asp.net 會話管理的瞭解也就通常,這裏寫出來主要是請你們幫我分析分析這個思路是否正確。我之前有些有關Session的也整理以下:html

你的項目真的須要Session嗎? redis保存session性能怎麼樣?前端

asp.net mvc Session RedisSessionStateProvider鎖的實現redis

用redis來實現Session保存的一個簡單Demo數據庫

大型Web 網站 Asp.net Session過時你怎麼辦api

Asp.net Session認識增強-Session到底是如何存儲你知道嗎?跨域

 JsonWebToken Demo瀏覽器

先簡單的說一下問題所在,目前項目是用RedisSessionStateProvider來管理咱們的會話,同時咱們的業務數據有一部分也存放在redis裏面,而且在一個redis實例裏面。 項目採用asp.net api +單頁面程序。就我我的而言我是很討厭Session 過時的時候給你彈一個提示讓你從新登陸這種狀況。京東和淘寶都不幹這事。。。。,系統還須要有 單點登陸和在線統計功能,如下說說個人思路:服務器

1.若是用純淨的JWT來實現,客戶端必須改code,由於jwt實現能夠放在http請求的body或者header,不管整麼放都須要修改前端js的code,因此我決定用cookie在存放對應的數據,由於cookie是瀏覽器管理的;注意一下jwt放在header在跨域的時候會走複雜跨域請求哦。cookie

2.再用統計計劃用redis的key來作,經過查找能夠的個數確認在線的人數,key的value將存放用戶名和級別session

3.單點登陸仍是用redis再作,把當前用戶的session id存起來,和當前http請求的session id對比,不一樣就踢出去。

運行結果以下:

進入login頁面,我會清空當前域的全部cookie,而後分配一個session id

輸入用戶名和密碼進入到一個 協議頁面,會增長一個TempMemberId的cookie

注意這裏的TempMemberId是jwt的格式,接受協議後會刪除該cookie,並把當前session Id做爲cookie 的key把真正的值賦給它

設置該cookie的code以下:

  public static void SetLogin(int memberId, MemberInfo member)
        {
            HttpCookie sessionCookie = HttpContext.Current.Request.Cookies[CookieName];
            if (sessionCookie != null && !string.IsNullOrEmpty(sessionCookie.Value))
            {
                string token = Encode(member);
                HttpCookie membercookie = new HttpCookie(sessionCookie.Value, token) { HttpOnly = true };
                HttpContext.Current.Response.SetCookie(membercookie);

                string loginKey = ConfigUtil.ApplicationName + "-" + member.MemberId.ToString();
                string memberKey = $"{member.MemberLevel.ToString()}-{member.Account}";
                redis.StringSet(loginKey, memberKey, TimeSpan.FromMinutes(SessionTimeOut));
            }
        }

首先獲取須要寫入cookie的key(session id的值),也就是sessionCookie的value,把當前MemberInfo實例經過jwt的方式轉換爲字符串,把它寫入到cookie,而後在寫redis,一個用戶在多個瀏覽器登陸,可是他的memberId是同樣的,因此redis只有一條記錄,這一條記錄用於統計在線人數,value值是 級別-用戶名。真正調用的地方以下:

SessionStateManage.SetLogin(memberId, GetMemberFromDB(memberId));  GetMemberFromDB方法從數據庫檢索數據並返回爲MemberInfo實例
SessionStateManage.RemoveCookie(SessionStateManage.CookieName + "_TempMemberId");

string loginGuid = HttpContext.Current.Request.Cookies[SessionStateManage.CookieName]; 返回的就是咱們sesssion id
string redisKey = RedisConsts.AccountMember + memberId;
RedisUtil.GetDatabase().HashSet(redisKey, "LoginGuid", loginGuid); 一個member只記錄最後一個login的session id

那麼加載用戶信息的code以下:

 public static MemberInfo GetUser()
        {
            MemberInfo member = null;
            HttpCookie sessionCookie = HttpContext.Current.Request.Cookies[CookieName];
            if (sessionCookie != null && !string.IsNullOrEmpty(sessionCookie.Value))
            {
                HttpCookie memberCookie = HttpContext.Current.Request.Cookies[sessionCookie.Value];
                member = Decode<MemberInfo>(memberCookie.Value);
                if (member != null)
                {
                    string loginKey = ConfigUtil.ApplicationName + ":" + member.MemberId;
                    // redis.KeyExpire(loginKey, TimeSpan.FromMinutes(SessionTimeOut));
                    string memberKey = $"{member.MemberLevel.ToString()}-{member.Account}";
                    redis.StringSet(loginKey, memberKey, TimeSpan.FromMinutes(SessionTimeOut));
                }
            }

            HttpContext.Current.Items[RedisConsts.SessionMemberInfo] = member;
            return member;
        }

首先須要獲取jwt的原始數據,存放在memberCookie裏面,而後解碼爲MemberInfo實例,而且保存到 HttpContext.Current.Items裏面(主要是維持之前的code不變),同時須要刷新 redis 裏面對應key的過時時間

 public static string Account
        {
            get
            {
                //return ConvertUtil.ToString(HttpContext.Current.Session["Account"], string.Empty);
                var memberinfo = HttpContext.Current.Items[RedisConsts.SessionMemberInfo] as MemberInfo;
                return memberinfo == null ? string.Empty : memberinfo.Account;
            }
            set
            {
                //HttpContext.Current.Session["Account"] = value;
                ExceptionUtil.ThrowMessageException("不能給session賦值");
            }
        }

看了這個code你們知道爲何須要保存到HttpContext.Current.Items裏面了。

protected override void OnAuthentication(AuthenticationContext filterContext)
        {
            object[] nonAuthorizedAttributes = filterContext.ActionDescriptor.GetCustomAttributes(typeof(NonAuthorizedAttribute), false);
            if (nonAuthorizedAttributes.Length == 0)
            {
                SessionStateManage.GetUser();
                //帳戶踢出檢測
                string loginGuid = RedisUtil.GetDatabase().HashGet(RedisConsts.AccountMember + memberId, "LoginGuid");
                if (loginGuid != SessionUtil.LoginGuid)
                {
                    //.......您的帳號已在別處登陸;
                }  
            }
        }

咱們在Controller裏面OnAuthentication方法調用 SessionStateManage.GetUser();方法,檢查redis裏面存放的session id和當前請求的session id是否一致,不一致踢出去。把MemberInfo放到HttpContext.Current.Items裏面來調用也算是歷史遺留問題,我的更建議把MemberInfo實例做爲Controller的屬性來訪問

在線統計:

  var accounts = new Dictionary<string, int>();
                var keys = SessionStateManage.Redis.GetReadServer().Keys(SessionStateManage.Redis.Database, pattern: ConfigUtil.ApplicationName + "*").ToArray();
                int count = 0;
                List<RedisKey> tempKeys = new List<RedisKey>();
                for (int i = 0; i < keys.Count(); i++)
                {
                    tempKeys.Add(keys[i]);
                    count++;
                    if (count > 1000 || i == keys.Count() - 1)
                    {
                        var vals = SessionStateManage.Redis.StringGet(tempKeys.ToArray()).ToList();
                        vals.ForEach(x =>
                        {
                            string[] acs = x.ToString().Split('-');
                            if (acs != null && acs.Length == 2)
                            {
                                accounts.TryAdd(acs[1], ConvertUtil.ToInt(acs[0]));
                            }
                        });
                        tempKeys.Clear();
                        count = 0;
                    }

                }

首先須要讀取當前須要讀取key的集合,記住在redis的Keys方法帶參數pattern的性能要低一點,它須要把key讀出來而後再過濾。若是運維能肯定當前database的key都是須要讀取的那麼就能夠不用pattern參數。爲了提升性能StringGet一次能夠讀取1000個key,這裏設計爲字符串的key而不是hash的緣由就是讀取方便。在使用redis我的不建議用異步和所謂的多線程,由於redis服務器是單線程,因此多線程可能感受和測試都要快一些,可是redis一直忙於處理你當前的請求,別的請求就很難處理了

完整的會話處理code以下:

  public class TempMemberInfo
    {
        public int TempMemberId { set; get; }
    }

    public class MemberInfo
    {
        public int MemberId { set; get; }
        public string Account { set; get; }
        public int ParentId { set; get; }
        public int CompanyId { set; get; }
        public int MemberLevel { set; get; }
        public int IsSubAccount { set; get; }
        public int AgentId { set; get; }
        public int BigAgentId { set; get; }
        public int ShareHolderId { set; get; }
        public int BigShareHolderId { set; get; }
        public int DirectorId { set; get; }
    }

    public class SessionStateManage
    {
        static RedisDatabase redis;
        static string jwtKey = "SevenStarKey";

        static SessionStateManage()
        {
            CookieName = ConfigUtil.CookieName;

            string conStr = ConfigUtil.RedisConnectionString;
            ConfigurationOptions option = ConfigurationOptions.Parse(conStr);
            int databaseId = option.DefaultDatabase ?? 0;
            option.DefaultDatabase = option.DefaultDatabase + 1;
            redis = RedisUtil.GetDatabase(option.ToString(true));
            SessionTimeOut = 20;
        }

        public static string CookieName { get; private set; }

        public static int SessionTimeOut { get; set; }

        public static RedisDatabase Redis
        {
            get
            {
                return redis;
            }
        }

        public static void InitCookie()
        {
            RemoveCookie();
            string cookiememberId = (new SessionIDManager()).CreateSessionID(HttpContext.Current);
            HttpCookie cookieId = new HttpCookie(CookieName, cookiememberId) { HttpOnly = true };
            HttpContext.Current.Response.SetCookie(cookieId);
        }

        public static void SetLogin(int memberId, MemberInfo member)
        {
            HttpCookie sessionCookie = HttpContext.Current.Request.Cookies[CookieName];
            if (sessionCookie != null && !string.IsNullOrEmpty(sessionCookie.Value))
            {
                string token = Encode(member);
                HttpCookie membercookie = new HttpCookie(sessionCookie.Value, token) { HttpOnly = true };
                HttpContext.Current.Response.SetCookie(membercookie);

                string loginKey = ConfigUtil.ApplicationName + "-" + member.MemberId.ToString();
                string memberKey = $"{member.MemberLevel.ToString()}-{member.Account}";
                redis.StringSet(loginKey, memberKey, TimeSpan.FromMinutes(SessionTimeOut));
            }

        }

        public static MemberInfo GetUser()
        {
            MemberInfo member = null;
            HttpCookie sessionCookie = HttpContext.Current.Request.Cookies[CookieName];
            if (sessionCookie != null && !string.IsNullOrEmpty(sessionCookie.Value))
            {
                HttpCookie memberCookie = HttpContext.Current.Request.Cookies[sessionCookie.Value];
                member = Decode<MemberInfo>(memberCookie.Value);
                if (member != null)
                {
                    string loginKey = ConfigUtil.ApplicationName + ":" + member.MemberId;
                    redis.KeyExpire(loginKey, TimeSpan.FromMinutes(SessionTimeOut));
                }
            }

            HttpContext.Current.Items[RedisConsts.SessionMemberInfo] = member;
            return member;
        }

        public static void Clear()
        {
            HttpCookie sessionCookie = HttpContext.Current.Request.Cookies[CookieName];
            if (sessionCookie != null && !string.IsNullOrEmpty(sessionCookie.Value))
            {
                HttpCookie membercookie = HttpContext.Current.Request.Cookies[sessionCookie.Value];
                if (membercookie != null)
                {
                    string loginKey = RedisConsts.AccountLogin + membercookie.Value;
                    redis.KeyDelete(loginKey);
                }

            }
            RemoveCookie();
        }

        public static void RemoveCookie(string key)
        {
            var cookie = new HttpCookie(key) { Expires = DateTime.Now.AddDays(-1) };
            HttpContext.Current.Response.Cookies.Set(cookie);
        }

        static void RemoveCookie()
        {
            foreach (string key in HttpContext.Current.Request.Cookies.AllKeys)
            {
                RemoveCookie(key);
            }
        }

        public static string Encode(object obj)
        {
            return JsonWebToken.Encode(obj, jwtKey, JwtHashAlgorithm.RS256); ;
        }
        public static T Decode<T>(string obj)
        {
            string token = JsonWebToken.Decode(obj, jwtKey).ToString();
            if (!string.IsNullOrEmpty(token))
            {
                return JsonConvert.DeserializeObject<T>(token);
            }
            return default(T);
        }
    }

    public enum JwtHashAlgorithm
    {
        RS256,
        HS384,
        HS512
    }

    public class JsonWebToken
    {
        private static Dictionary<JwtHashAlgorithm, Func<byte[], byte[], byte[]>> HashAlgorithms;

        static JsonWebToken()
        {
            HashAlgorithms = new Dictionary<JwtHashAlgorithm, Func<byte[], byte[], byte[]>>
            {
                { JwtHashAlgorithm.RS256, (key, value) => { using (var sha = new HMACSHA256(key)) { return sha.ComputeHash(value); } } },
                { JwtHashAlgorithm.HS384, (key, value) => { using (var sha = new HMACSHA384(key)) { return sha.ComputeHash(value); } } },
                { JwtHashAlgorithm.HS512, (key, value) => { using (var sha = new HMACSHA512(key)) { return sha.ComputeHash(value); } } }
            };
        }

        public static string Encode(object payload, string key, JwtHashAlgorithm algorithm)
        {
            return Encode(payload, Encoding.UTF8.GetBytes(key), algorithm);
        }

        public static string Encode(object payload, byte[] keyBytes, JwtHashAlgorithm algorithm)
        {
            var segments = new List<string>();
            var header = new { alg = algorithm.ToString(), typ = "JWT" };

            byte[] headerBytes = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(header, Formatting.None));
            byte[] payloadBytes = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(payload, Formatting.None));
            //byte[] payloadBytes = Encoding.UTF8.GetBytes(@"{"iss":"761326798069-r5mljlln1rd4lrbhg75efgigp36m78j5@developer.gserviceaccount.com","scope":"https://www.googleapis.com/auth/prediction","aud":"https://accounts.google.com/o/oauth2/token","exp":1328554385,"iat":1328550785}");  

            segments.Add(Base64UrlEncode(headerBytes));
            segments.Add(Base64UrlEncode(payloadBytes));

            var stringToSign = string.Join(".", segments.ToArray());

            var bytesToSign = Encoding.UTF8.GetBytes(stringToSign);

            byte[] signature = HashAlgorithms[algorithm](keyBytes, bytesToSign);
            segments.Add(Base64UrlEncode(signature));

            return string.Join(".", segments.ToArray());
        }

        public static object Decode(string token, string key)
        {
            return Decode(token, key, true);
        }

        public static object Decode(string token, string key, bool verify)
        {
            var parts = token.Split('.');
            var header = parts[0];
            var payload = parts[1];
            byte[] crypto = Base64UrlDecode(parts[2]);

            var headerJson = Encoding.UTF8.GetString(Base64UrlDecode(header));
            var headerData = JObject.Parse(headerJson);
            var payloadJson = Encoding.UTF8.GetString(Base64UrlDecode(payload));
            var payloadData = JObject.Parse(payloadJson);

            if (verify)
            {
                var bytesToSign = Encoding.UTF8.GetBytes(string.Concat(header, ".", payload));
                var keyBytes = Encoding.UTF8.GetBytes(key);
                var algorithm = (string)headerData["alg"];

                var signature = HashAlgorithms[GetHashAlgorithm(algorithm)](keyBytes, bytesToSign);
                var decodedCrypto = Convert.ToBase64String(crypto);
                var decodedSignature = Convert.ToBase64String(signature);

                if (decodedCrypto != decodedSignature)
                {
                    throw new ApplicationException(string.Format("Invalid signature. Expected {0} got {1}", decodedCrypto, decodedSignature));
                }
            }

            //return payloadData.ToString();  
            return payloadData;
        }

        private static JwtHashAlgorithm GetHashAlgorithm(string algorithm)
        {
            switch (algorithm)
            {
                case "RS256": return JwtHashAlgorithm.RS256;
                case "HS384": return JwtHashAlgorithm.HS384;
                case "HS512": return JwtHashAlgorithm.HS512;
                default: throw new InvalidOperationException("Algorithm not supported.");
            }
        }

        // from JWT spec  
        private static string Base64UrlEncode(byte[] input)
        {
            var output = Convert.ToBase64String(input);
            output = output.Split('=')[0]; // Remove any trailing '='s  
            output = output.Replace('+', '-'); // 62nd char of encoding  
            output = output.Replace('/', '_'); // 63rd char of encoding  
            return output;
        }

        // from JWT spec  
        private static byte[] Base64UrlDecode(string input)
        {
            var output = input;
            output = output.Replace('-', '+'); // 62nd char of encoding  
            output = output.Replace('_', '/'); // 63rd char of encoding  
            switch (output.Length % 4) // Pad with trailing '='s  
            {
                case 0: break; // No pad chars in this case  
                case 2: output += "=="; break; // Two pad chars  
                case 3: output += "="; break; // One pad char  
                default: throw new System.Exception("Illegal base64url string!");
            }
            var converted = Convert.FromBase64String(output); // Standard base64 decoder  
            return converted;
        }
    }
View Code
相關文章
相關標籤/搜索