zoukankan      html  css  js  c++  java
  • asp.net 用JWT来实现token以此取代Session

    先说一下为什么要写这一篇博客吧,其实个人有关asp.net 会话管理的了解也就一般,这里写出来主要是请大家帮我分析分析这个思路是否正确。我以前有些有关Session的也整理如下:

    你的项目真的需要Session吗? redis保存session性能怎么样?

    asp.net mvc Session RedisSessionStateProvider锁的实现

    用redis来实现Session保存的一个简单Demo

    大型Web 网站 Asp.net Session过期你怎么办

    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在跨域的时候会走复杂跨域请求哦。

    2.再用统计计划用redis的key来做,通过查找可以的个数确认在线的人数,key的value将存放用户名和级别

    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
  • 相关阅读:
    《使用Hibernate开发租房系统》内部测试笔试题
    C++第十课 字符串
    【011】字符数组
    C++第九课 数组
    C++第八课 局部变量和全局变量
    【010】递归函数
    C++第七课 函数2
    【009】阅读代码
    C++第六课 函数1
    【008】查找素数
  • 原文地址:https://www.cnblogs.com/majiang/p/6650459.html
Copyright © 2011-2022 走看看