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
  • 相关阅读:
    Saltstack module acl 详解
    Saltstack python client
    Saltstack简单使用
    P5488 差分与前缀和 NTT Lucas定理 多项式
    CF613D Kingdom and its Cities 虚树 树形dp 贪心
    7.1 NOI模拟赛 凸包套凸包 floyd 计算几何
    luogu P5633 最小度限制生成树 wqs二分
    7.1 NOI模拟赛 dp floyd
    springboot和springcloud
    springboot集成mybatis
  • 原文地址:https://www.cnblogs.com/majiang/p/6650459.html
Copyright © 2011-2022 走看看