>  기사  >  데이터 베이스  >  Redis에서 세션 공유를 구현하는 방법 소개

Redis에서 세션 공유를 구현하는 방법 소개

尚
앞으로
2020-05-12 09:19:023409검색

Redis에서 세션 공유를 구현하는 방법 소개

세션 구현 원리

세션과 쿠키는 웹 개발에서 일반적으로 사용되는 두 가지 객체와 관련이 있나요?

php 중국어 웹사이트 학습 주제: php 세션 (사진, 텍스트, 동영상, 사례 포함)

쿠키란 무엇인가요?

쿠키는 사용자 요청 및 페이지와 함께 웹 서버와 브라우저 간에 전달되는 작은 텍스트 정보입니다. 쿠키에는 사용자가 사이트를 방문할 때마다 웹 애플리케이션이 읽을 수 있는 정보가 포함되어 있습니다.

참고: 쿠키는 js, css, 이미지 등과 같은 정적 파일을 제외하고 각 HTTP 요청과 함께 서버에 전달됩니다. 이 프로세스는 IE와 함께 제공되는 피들러 또는 네트워크 모니터링에서 분석할 수 있습니다. 쿠키 줄이기

브라우저에 쿠키를 작성하는 프로세스: 다음 코드를 사용하여 Asp.net 프로젝트에 쿠키를 작성하고 클라이언트 브라우저로 보낼 수 있습니다(단순화를 위해 다른 속성을 설정하지 않았습니다).

HttpCookie cookie = new HttpCookie("RedisSessionId", "string value");Response.Cookies.Add(cookie);

Redis에서 세션 공유를 구현하는 방법 소개

응답 헤더 Set-Cookie를 통해 서버에 작성된 쿠키가 브라우저에 기록되는 것을 확인할 수 있습니다.

세션이란 무엇인가요?

Session 이를 사용하여 일부 세션 관련 정보를 서버 측에 쉽게 저장할 수 있습니다. 일반적인 로그인 정보 등.

Session 구현 원리는?

HTTP 프로토콜은 상태 비저장입니다. 브라우저에서 발행한 여러 요청의 경우 웹 서버는 해당 요청이 동일한 브라우저에서 발생했는지 여부를 구별할 수 없습니다. 그러면 이 프로세스를 구별하기 위해 서버는 sessionid를 통해 요청을 구별합니다. 이 sessionid는 어떻게 서버로 전송됩니까?

앞서 언급했듯이 쿠키는 요청마다 서버로 전송되며, 쿠키는 사용자에게 표시되지 않습니다. 이 세션 ID를 저장하는 데 사용하는 것이 가장 좋습니다. 다음 과정을 통해 확인하겠습니다.

Session["UserId"] = 123;

Redis에서 세션 공유를 구현하는 방법 소개

위 그림을 통해 세션과 쿠키의 관계를 다시 확인합니다. 서버는 쿠키 설정 작업을 생성하여 브라우저를 구분합니다. 다른 브라우저로 실험하려면 IE에서 로그인한 다음 Chrome에서 동일한 페이지를 열 수 있습니다. 현재 Chrome에는 세션 ID가 없기 때문에 여전히 Chrome에 로그인해야 합니다. httpOnly는 세션 ID의 인위적인 수정을 방지하기 위해 이 쿠키가 브라우저 측에서 js를 통해 작동되지 않음을 의미합니다.

asp.net에서 sessionid의 기본 키 값은 ASP.NET_SessionId입니다. 이 기본 구성은 web.config에서 수정할 수 있습니다.

<sessionState mode="InProc" cookieName="MySessionId"></sessionState>

서버 측 세션 읽기

서버 측에서는 세션 값을 어떻게 읽나요? 세션 ["키-값"]. 그렇다면 질문은 왜 이 Session 개체를 Defaule.aspx.cs 파일에서 얻을 수 있으며 이 Session 개체는 언제 초기화되었는가 하는 것입니다.

이를 알아내기 위해 정의로 이동하여 살펴볼 수 있습니다.

System.Web.UI.Page ->HttpSessionState(Session)

protected internal override HttpContext Context {
[System.Runtime.TargetedPatchingOptOut("Performance critical to inline across NGen image boundaries")]
  get {
       if (_context == null) {
           _context = HttpContext.Current;
       }
       return _context;
    }
 }
 public virtual HttpSessionState Session {
        get {
            if (!_sessionRetrieved) {
                /* try just once to retrieve it */
                _sessionRetrieved = true;

                try {
                    _session = Context.Session;
                }
                catch {
                    //  Just ignore exceptions, return null.
                }
            }

            if (_session == null) {
                throw new HttpException(SR.GetString(SR.Session_not_enabled));
            }

            return _session;
        }
    }

위 단락은 Page 객체가 Session 객체를 초기화하는 곳입니다. Session의 값이 HttpContext.Current에서 오는 것을 볼 수 있습니다. HttpContext.Current? 이때 초기화되는데, 살펴보겠습니다.

public sealed class HttpContext : IServiceProvider, IPrincipalContainer
    {

        internal static readonly Assembly SystemWebAssembly = typeof(HttpContext).Assembly;
        private static volatile bool s_eurlSet;
        private static string s_eurl;

        private IHttpAsyncHandler  _asyncAppHandler;   // application as handler (not always HttpApplication)
        private AsyncPreloadModeFlags _asyncPreloadModeFlags;
        private bool               _asyncPreloadModeFlagsSet;
        private HttpApplication    _appInstance;
        private IHttpHandler       _handler;
        [DoNotReset]
        private HttpRequest        _request;
        private HttpResponse       _response;
        private HttpServerUtility  _server;
        private Stack              _traceContextStack;
        private TraceContext       _topTraceContext;
        [DoNotReset]
        private Hashtable          _items;
        private ArrayList          _errors;
        private Exception          _tempError;
        private bool               _errorCleared;
        [DoNotReset]
        private IPrincipalContainer _principalContainer;
        [DoNotReset]
        internal ProfileBase       _Profile;
        [DoNotReset]
        private DateTime           _utcTimestamp;
        [DoNotReset]
        private HttpWorkerRequest  _wr;
        private VirtualPath        _configurationPath;
        internal bool              _skipAuthorization;
        [DoNotReset]
        private CultureInfo        _dynamicCulture;
        [DoNotReset]
        private CultureInfo        _dynamicUICulture;
        private int                _serverExecuteDepth;
        private Stack              _handlerStack;
        private bool               _preventPostback;
        private bool               _runtimeErrorReported;
        private PageInstrumentationService _pageInstrumentationService = null;
        private ReadOnlyCollection<string> _webSocketRequestedProtocols;
}

HttpContext에는 일반적으로 사용되는 요청, 응답 및 기타 개체가 포함되어 있습니다. HttpContext는 ASP.NET 파이프라인으로 시작됩니다. IIS 6.0을 예로 들면 작업 프로세스 w3wp.exe에서 Aspnet_ispai.dll은 .NET 런타임을 로드하는 데 사용됩니다(.NET 런타임이 아직 로드되지 않은 경우).

IIS 6.0에는 응용 프로그램 풀 개념이 도입되었습니다. 작업자 프로세스는 응용 프로그램 풀에 해당합니다. 응용 프로그램 풀은 하나 이상의 웹 응용 프로그램을 호스팅할 수 있으며 각 웹 응용 프로그램은 IIS 가상 디렉터리에 매핑됩니다. IIS 5.x와 마찬가지로 각 웹 응용 프로그램은 자체 응용 프로그램 도메인에서 실행됩니다.

HTTP.SYS에서 수신한 HTTP 요청이 웹 애플리케이션에 대한 첫 번째 액세스인 경우 런타임이 성공적으로 로드된 후 AppDomainFactory를 통해 웹 애플리케이션에 대한 애플리케이션 도메인(AppDomain)이 생성됩니다.

이후 특별한 런타임 IsapiRuntime이 로드됩니다. IsapiRuntime은 System.Web 어셈블리에 정의되어 있으며 해당 네임스페이스는 System.Web.Hosting입니다.

IsapiRuntime이 HTTP 요청을 대신합니다. IsapiRuntime은 먼저 IsapiWorkerRequest 개체를 만들어 현재 HTTP 요청을 캡슐화하고 IsapiWorkerRequest 개체를 ASP.NET 런타임인 HttpRuntime에 전달합니다. 이 시점부터 HTTP 요청은 공식적으로 ASP.NET 파이프라인에 들어갑니다.

IsapiWorkerRequest 개체에 따라 HttpRuntime은 현재 HTTP 요청을 나타내는 데 사용되는 컨텍스트(Context) 개체인 HttpContext를 생성합니다.

이제 세션 초기화 과정과 세션과 쿠키의 관계에 대해 다들 잘 이해하셨을 거라 믿습니다. 먼저 세션 공유 구현 계획부터 살펴보겠습니다.

세션 공유 구현 계획

1. StateServer 방식

这种是asp.net提供的一种方式,还有一种是SQLServer方式(不一定程序使用的是SQLServer数据库,所以通用性不高,这里就不介绍了)。也就是将会话数据存储到单独的内存缓冲区中,再由单独一台机器上运行的Windows服务来控制这个缓冲区。

状态服务全称是“ASP.NET State Service ”(aspnet_state.exe)。它由Web.config文件中的stateConnectionString属性来配置。该属性指定了服务所在的服务器,以及要监视的端口。

<sessionState mode="StateServer"      stateConnectionString="tcpip=127.0.0.1:42424"     cookieless="false" timeout="20" />

在这个例子中,状态服务在当前机器的42424端口(默认端口)运行。要在服务器上改变端口和开启远程服务器的该功能,可编辑HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\services\aspnet_state\Parameters注册表项中的Port值和AllowRemoteConnection修改成1。

 显然,使用状态服务的优点在于进程隔离,并可在多站点中共享。 使用这种模式,会话状态的存储将不依赖于iis进程的失败或者重启,然而,一旦状态服务中止,所有会话数据都会丢失(这个问题redis不会存在,重新了数据不会丢失)。

这里提供一段bat文件帮助修改注册表,可以复制保存为.bat文件执行

reg add "HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\services\aspnet_state\Parameters" /v "AllowRemoteConnection" /t REG_DWORD  /d 1 /f

reg add "HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\services\aspnet_state\Parameters" /v "Port" /t REG_DWORD  /d 42424 /f

net stop aspnet_state
net start aspnet_state

pause

Redis에서 세션 공유를 구현하는 방법 소개

完成这些配置以后还是不能实现共享,虽然站点间的SessionId是一致的,但只有一个站点能够读取的到值,而其它站点读取不到。下面给出解决方案,在Global文件里面添加下面代码

public override void Init()
 {
      base.Init();

      foreach (string moduleName in this.Modules)
      {
           string appName = "APPNAME";
           IHttpModule module = this.Modules[moduleName];
           SessionStateModule ssm = module as SessionStateModule;
           if (ssm != null)
           {
                FieldInfo storeInfo = typeof(SessionStateModule).GetField("_store", BindingFlags.Instance | BindingFlags.NonPublic);
                FieldInfo configMode = typeof(SessionStateModule).GetField("s_configMode", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Static);

                SessionStateMode mode = (SessionStateMode)configMode.GetValue(ssm);
                if (mode == SessionStateMode.StateServer)
                {
                    SessionStateStoreProviderBase store = (SessionStateStoreProviderBase)storeInfo.GetValue(ssm);
                    if (store == null)//In IIS7 Integrated mode, module.Init() is called later
                    {
                        FieldInfo runtimeInfo = typeof(HttpRuntime).GetField("_theRuntime", BindingFlags.Static | BindingFlags.NonPublic);
                        HttpRuntime theRuntime = (HttpRuntime)runtimeInfo.GetValue(null);
                        FieldInfo appNameInfo = typeof(HttpRuntime).GetField("_appDomainAppId", BindingFlags.Instance | BindingFlags.NonPublic);
                        appNameInfo.SetValue(theRuntime, appName);
                    }
                    else
                    {
                       Type storeType = store.GetType();
                       if (storeType.Name.Equals("OutOfProcSessionStateStore"))
                       {
                           FieldInfo uribaseInfo = storeType.GetField("s_uribase", BindingFlags.Static | BindingFlags.NonPublic);
                           uribaseInfo.SetValue(storeType, appName);
                           object obj = null;
                           uribaseInfo.GetValue(obj);
                        }
                    }
                }
                break;
            }
        }
  }

二.redis实现session共享

下面我们将使用redis来实现共享,首先要弄清楚session的几个关键点,过期时间,SessionId,一个SessionId里面会存在多组key/value数据。基于这个特性我将采用Hash结构来存储,看看代码实现。用到了上一篇提供的RedisBase帮助类。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.SessionState;
using ServiceStack.Redis;
using Com.Redis;

namespace ResidSessionDemo.RedisDemo
{
    public class RedisSession
    {
        private HttpContext context;

        public RedisSession(HttpContext context, bool IsReadOnly, int Timeout)
        {
            this.context = context;
            this.IsReadOnly = IsReadOnly;
            this.Timeout = Timeout;
            //更新缓存过期时间
            RedisBase.Hash_SetExpire(SessionID, DateTime.Now.AddMinutes(Timeout));
        }

        /// <summary>
        /// SessionId标识符
        /// </summary>
        public static string SessionName = "Redis_SessionId";

        //
        // 摘要:
        //     获取会话状态集合中的项数。
        //
        // 返回结果:
        //     集合中的项数。
        public int Count
        {
            get
            {
                return RedisBase.Hash_GetCount(SessionID);
            }
        }

        //
        // 摘要:
        //     获取一个值,该值指示会话是否为只读。
        //
        // 返回结果:
        //     如果会话为只读,则为 true;否则为 false。
        public bool IsReadOnly { get; set; }

        //
        // 摘要:
        //     获取会话的唯一标识符。
        //
        // 返回结果:
        //     唯一会话标识符。
        public string SessionID
        {
            get
            {
                return GetSessionID();
            }
        }

        //
        // 摘要:
        //     获取并设置在会话状态提供程序终止会话之前各请求之间所允许的时间(以分钟为单位)。
        //
        // 返回结果:
        //     超时期限(以分钟为单位)。
        public int Timeout { get; set; }

        /// <summary>
        /// 获取SessionID
        /// </summary>
        /// <param name="key">SessionId标识符</param>
        /// <returns>HttpCookie值</returns>
        private string GetSessionID()
        {
            HttpCookie cookie = context.Request.Cookies.Get(SessionName);
            if (cookie == null || string.IsNullOrEmpty(cookie.Value))
            {
                string newSessionID = Guid.NewGuid().ToString();
                HttpCookie newCookie = new HttpCookie(SessionName, newSessionID);
                newCookie.HttpOnly = IsReadOnly;
                newCookie.Expires = DateTime.Now.AddMinutes(Timeout);
                context.Response.Cookies.Add(newCookie);
                return "Session_"+newSessionID;
            }
            else
            {
                return "Session_"+cookie.Value;
            }
        }

        //
        // 摘要:
        //     按名称获取或设置会话值。
        //
        // 参数:
        //   name:
        //     会话值的键名。
        //
        // 返回结果:
        //     具有指定名称的会话状态值;如果该项不存在,则为 null。
        public object this[string name]
        {
            get
            {
                return RedisBase.Hash_Get<object>(SessionID, name);
            }
            set
            {
                RedisBase.Hash_Set<object>(SessionID, name, value);
            }
        }

        // 摘要:
        //     判断会话中是否存在指定key
        //
        // 参数:
        //   name:
        //     键值
        //
        public bool IsExistKey(string name)
        {
            return RedisBase.Hash_Exist<object>(SessionID, name);
        }

        //
        // 摘要:
        //     向会话状态集合添加一个新项。
        //
        // 参数:
        //   name:
        //     要添加到会话状态集合的项的名称。
        //
        //   value:
        //     要添加到会话状态集合的项的值。
        public void Add(string name, object value)
        {
            RedisBase.Hash_Set<object>(SessionID, name, value);
        }
        //
        // 摘要:
        //     从会话状态集合中移除所有的键和值。
        public void Clear()
        {
            RedisBase.Hash_Remove(SessionID);
        }

        //
        // 摘要:
        //     删除会话状态集合中的项。
        //
        // 参数:
        //   name:
        //     要从会话状态集合中删除的项的名称。
        public void Remove(string name)
        {
            RedisBase.Hash_Remove(SessionID,name);
        }
        //
        // 摘要:
        //     从会话状态集合中移除所有的键和值。
        public void RemoveAll()
        {
            Clear();
        }
    }
}

下面是实现类似在cs文件中能直接使用Session["UserId"]的方式,我的MyPage类继承Page实现了自己的逻辑主要做了两件事  1:初始化RedisSession  2:实现统一登录认证,OnPreInit方法里面判断用户是否登录,如果没有登录了则跳转到登陆界面

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI;

namespace ResidSessionDemo.RedisDemo
{
    /// <summary>
    /// 自定义Page 实现以下功能
    /// 1.初始化RedisSession
    /// 2.实现页面登录验证,继承此类,则可以实现所有页面的登录验证
    /// </summary>
    public class MyPage:Page
    {
        private RedisSession redisSession;

        /// <summary>
        /// RedisSession
        /// </summary>
        public RedisSession RedisSession
        {
            get
            {
                if (redisSession == null)
                {
                    redisSession = new RedisSession(Context, true, 20);
                }
                return redisSession;
            }
        }

        protected override void OnPreInit(EventArgs e)
        {
            base.OnPreInit(e);
            //判断用户是否已经登录,如果未登录,则跳转到登录界面
            if (!RedisSession.IsExistKey("UserCode"))
            {
                Response.Redirect("Login.aspx");
            }
        }
    }
}

我们来看看Default.aspx.cs是如何使用RedisSession的,至此我们实现了和Asp.netSession一模一样的功能和使用方式。

RedisSession.Remove("UserCode");

相比StateServer,RedisSession具有以下优点

1、redis服务器重启不会丢失数据  2.可以使用redis的读写分离个集群功能更加高效读写数据  

测试效果,使用nginx和iis部署两个站点做负载均衡,iis1地址127.0.0.1:8002 iis2地址127.0.0.1:9000  nginx代理服务地址127.0.0.1:8003,不懂如何配置的可以去阅读我的nginx+iis实现负载均衡这篇文章。我们来看一下测试结果。

访问127.0.0.1:8003 需要进行登录   用户名为admin  密码为123

Redis에서 세션 공유를 구현하는 방법 소개

登录成功以后,重点关注端口号信息

Redis에서 세션 공유를 구현하는 방법 소개

刷新页面,重点关注端口号信息

Redis에서 세션 공유를 구현하는 방법 소개

可以尝试直接访问iis1地址127.0.0.1:8002 iis2地址127.0.0.1:9000 这两个站点,你会发现都不需要登录了。至此我们的redis实现session功能算是大功告成了。

问题拓展

使用redis实现session告一段落,下面留个问题讨论一下方案。微信开发提供了很多接口,参考下面截图,可以看到获取access_token接口每日最多调用2000次,现在大公司提供的很多接口针对不对级别的用户接口访问次数限制都是不一样的,至于做这个限制的原因应该是防止恶意攻击和流量限制之类的。

那么我的问题是怎么实现这个接口调用次数限制功能。大家可以发挥想象力参与讨论哦,或许你也会碰到这个问题。

Redis에서 세션 공유를 구현하는 방법 소개

先说下我知道的两种方案:

1、使用流量整形中的令牌桶算法,大小固定的令牌桶可自行以恒定的速率源源不断地产生令牌。如果令牌不被消耗,或者被消耗的速度小于产生的速度,令牌就会不断地增多,直到把桶填满。后面再产生的令牌就会从桶中溢出。最后桶中可以保存的最大令牌数永远不会超过桶的大小。

간단히 말하면: 예를 들어 위의 access_token 인터페이스는 하루에 2000회, 즉 1회/분의 빈도를 갖습니다. 우리의 토큰 버킷 용량은 2000이며, 이는 redis의 가장 간단한 키/값을 사용하여 저장할 수 있습니다. 키는 사용자 ID이고 값은 정수 저장소를 사용할 수 있는 횟수입니다. 그런 다음 타이머를 사용하여 클라이언트를 호출합니다. 1분 동안 Incr(key)를 사용하여 횟수를 늘립니다. 사용자가 이 인터페이스에 액세스할 때마다 해당 client.Decr(key)를 사용하여 사용 횟수를 줄입니다.

하지만 여기에는 성능 문제가 있습니다. 사용자가 100,000명이라고 가정하면 이 자체 증가 작업을 구현하려면 어떻게 해야 할까요? 그리고 client.Incr(key)을 호출해야 합니다. )? 이것은 명확하게 생각되지 않았습니다.

2. 먼저 총 횟수를 판단합니다. 조건이 충족되면 자동 증가를 수행합니다. .

Redis에서 세션 공유를 구현하는 방법 소개

위 내용은 Redis에서 세션 공유를 구현하는 방법 소개의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
이 기사는 cnblogs.com에서 복제됩니다. 침해가 있는 경우 admin@php.cn으로 문의하시기 바랍니다. 삭제