This post provides a generic class that allows easy implementation and usage of Session Variables in an AspNetCore web application. Session Variables in AspNetCore web applications
are somewhat cryptic when compared to other programming languages like PHP. This class attempts to alleviate the complexities by providing a very simple object that manages
a website user's session meanwhile allowing the programmer to concentrate on the data model representing what he or she wants to store on the server side.
Due to the complexity of this topic, there is a sample project at http://github.com/sirpenski/XSession for reference.
To use this class:
1. Define a class that will hold all your session data. It can consist of any properties, lists, etc. The constraint is that the class is serializable and that it has a constructor. For example.
2. In each controller, include the namespaces:
3. In each controller, define a controller level variable to hold the session data.
4. In the OnActionExecuting function, instantiate a new instance, load it, set, and test session properties as necessary.
5. In startup.cs, in the ConfigureServices method, add the following:
6. In startup.cs, in the Configure method, add the following before the UseMvc() statement.
7. Using the class and referencing the session variables in code
XSession Class Source Code
Here is the code for the class.
Due to the complexity of this topic, there is a sample project at http://github.com/sirpenski/XSession for reference.
To use this class:
1. Define a class that will hold all your session data. It can consist of any properties, lists, etc. The constraint is that the class is serializable and that it has a constructor. For example.
[Serializable] public class UserSessionVariables { public bool IsAuthorized {get; set;} = false; public string UserID {get; set;} = ""; public string UserIPAddress {get; set;} = ""; public int UserPort {get; set; } = 0; public UserSessionVariables() {} }
2. In each controller, include the namespaces:
using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Http;
3. In each controller, define a controller level variable to hold the session data.
PFSXSessionsession;
4. In the OnActionExecuting function, instantiate a new instance, load it, set, and test session properties as necessary.
public override void OnActionExecuting(ActionExecutingContext context) { string action = this.ControllerContext.ActionDescriptor.ActionName.ToUpper(); // Define new session object and pass in the context in the constructor session = new PFSXSession(context.HttpContext); // load the user session. usr.Load(AutoInitialize: true); // generic login, login submit, and logout filtering if (string.Compare(action, "LOGIN") != 0 && string.Compare(action, "LOGINSUBMIT") != 0 && string.Compare(action, "LOGOUT") != 0) { // if the session is expired or the session is not authenticated, then redirect // to login page. if (session.IsExpired || !session.SessionVariables.IsAuthenticated) { context.Result = new RedirectResult(APP.LOGIN_URL); } } base.OnActionExecuting(context); }
5. In startup.cs, in the ConfigureServices method, add the following:
services.AddDistributedMemoryCache(); services.AddSession(options => { options.IdleTimeout = TimeSpan.FromMinutes(30); });
6. In startup.cs, in the Configure method, add the following before the UseMvc() statement.
app.UseSession();
7. Using the class and referencing the session variables in code
public IActionResult LoginSubmit(string uid, string pwd) { IActionResult rslt = new RedirectResult("/Admin/Login"); if (string.Compare(uid, USER_ID) == 0) && string.Compare(pwd, PASSWORD) == 0) { session.SessionVariables.IsAuthenticated = true; session.UserID = uid; session.Save(); rslt = new RedirectResult("/Admin/Index"); } return rslt; }
XSession Class Source Code
Here is the code for the class.
using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using System.IO; using System.Diagnostics; using System.Security.Cryptography; using System.Runtime.Serialization.Formatters.Binary; using Microsoft.AspNetCore.Http; namespace XSession.Models { public class PFSXSession:IDisposable where T: new() { private const string DEFAULT_SESSION_VARIABLE_NAME = "USR"; private const int DEFAULT_SESSION_EXPIRATION_INCREMENT_HOURS = 0; private const int DEFAULT_SESSION_EXPIRATION_INCREMENT_MINUTES = 20; private const int DEFAULT_SESSION_EXPIRATION_INCREMENT_SECONDS = 0; // this is the xsession data. this is a combination of the user session variables T and // the internal session variables of the class. private XSessionData xSessionData = null; // creates a new hash generator private XHash256Generator xHash = null; // random number generator private Random xRND = null; // private session corrupt flag private bool boolSessionIsCorrupt = false; /// Ctx is the current http context. It must be set prior to /// loading or saving public HttpContext Context { get; set; } = null; // name of the session variable public string Name { get; set; } = DEFAULT_SESSION_VARIABLE_NAME; /// Gives access to the serializable session variables public T SessionVariables { get { return xSessionData.SessionVariables; } set { xSessionData.SessionVariables = value; } } /// Determines if the session is initialized public bool SessionInitialized { get { return xSessionData.SessionInitialized; } set { xSessionData.SessionInitialized = value; } } /// Session Start Time public DateTime SessionStart { get { return xSessionData.SessionStart; } set { xSessionData.SessionStart = value; } } /// Session Expiration Time public DateTime SessionExpires { get { return xSessionData.SessionExpires; } set { xSessionData.SessionExpires = value; } } public DateTime SessionCurrentTime { get { return xSessionData.SessionCurrentTime; } set { xSessionData.SessionCurrentTime = value; } } /// The amount of time the session expiration date is incremented each call to extend public TimeSpan SessionExpirationIncrement { get; set; } /// This is a unique session id generated in initialize public byte[] SessionID { get { return xSessionData.SessionID; } set { xSessionData.SessionID = value; } } /// Gets the session id as a hexadecimal string public string SessionIDHex { get { return ByteArrayToHex(xSessionData.SessionID); } } /// Session Hash Value public byte[] SessionHashValue { get { return xSessionData.SessionHashValue; } set { xSessionData.SessionHashValue = value; } } /// Session Hash Value as a Hexadecimal string public string SessionHashValueHex { get { return ByteArrayToHex(SessionHashValue); } } /// Session Random number generator. This might be used instead of using the /// internal one. public Random SessionRandomNumberGenerator { get { return xRND; } set { xRND = value; } } /// Is Session Expired public bool IsExpired { get { bool rt = false; if (DateTime.Now > xSessionData.SessionExpires) { rt = true; } return rt; } } /// This flag is set if the session is corrupt, ie the hash value is not correct. public bool IsCorrupt { get { return boolSessionIsCorrupt; } } /// Session is error public bool IsError { get { return IsExpired || IsCorrupt; } } /// Read only variant of SessionInitialized public bool IsInitialized { get { return SessionInitialized; } } // -------------------------------------------------------------------------- // BEGIN CONSTRUCTORS // -------------------------------------------------------------------------- /// Base Constructor public PFSXSession() { ConstructorCommon(); } /// Constructor that accepts httpcontext public PFSXSession(HttpContext ctx) { Context = ctx; ConstructorCommon(); } /// Constructor public PFSXSession(string SessionVariableName, HttpContext ctx) { Context = ctx; Name = SessionVariableName; ConstructorCommon(); } // constructor public PFSXSession(HttpContext ctx, Random xr) { Context = ctx; xRND = xr; ConstructorCommon(); } // constructor public PFSXSession (string SessionVariableName, HttpContext ctx, Random xr ) { Name = SessionVariableName; Context = ctx; xRND = xr; } /// Common Constructor Code private void ConstructorCommon() { bool b = Attribute.IsDefined(typeof(T), typeof(SerializableAttribute)); if (!b) { throw new Exception("Object Is Not Serializable"); } SessionExpirationIncrement = new TimeSpan(DEFAULT_SESSION_EXPIRATION_INCREMENT_HOURS, DEFAULT_SESSION_EXPIRATION_INCREMENT_MINUTES, DEFAULT_SESSION_EXPIRATION_INCREMENT_SECONDS); xSessionData = new XSessionData(); xSessionData.SessionExpirationIncrement = SessionExpirationIncrement; xHash = new XHash256Generator(); } // -------------------------------------------------------------------------- /// Increments the Session Expiration Time // -------------------------------------------------------------------------- public void IncrementSessionExpiration() { DateTime cNow = DateTime.Now; xSessionData.SessionCurrentTime = cNow; xSessionData.SessionExpires = cNow.Add(xSessionData.SessionExpirationIncrement); } // -------------------------------------------------------------------------- /// Initializes the session // -------------------------------------------------------------------------- public void Initialize() { // define new session data object xSessionData = new XSessionData(); // set the session start time xSessionData.SessionStart = DateTime.Now; // create a unique session id xSessionData.SessionID = CreateNewSessionID(); // create a new user variable xSessionData.SessionVariables = new T(); // set the session expiration increment xSessionData.SessionExpirationIncrement = SessionExpirationIncrement; // increment the session expiration date time IncrementSessionExpiration(); // init the session SessionInitialized = true; Save(); Debug.WriteLine("**EXIT INITIALIZE SESSION"); } // -------------------------------------------------------------------------- /// Converts the byte array to a hex string of the form aaaa:bbbb:cccc // -------------------------------------------------------------------------- public string ByteArrayToHex(byte[] b, string default_sep = "") { string rt = ""; string sep = ""; for (int i = 0; i < b.Length; i++) { rt += sep + b[i].ToString("X2"); if ((i % 2) == 1) { sep = default_sep; } else { sep = ""; } } return rt; } // -------------------------------------------------------------------------- /// ObjectToByteArray serializes an object to a byte array. /// This can then be saved to a session variable // -------------------------------------------------------------------------- private byte[] ObjectToByteArray(Object obj) { BinaryFormatter bf = new BinaryFormatter(); using (var ms = new MemoryStream()) { bf.Serialize(ms, obj); return ms.ToArray(); } } // -------------------------------------------------------------------------- /// ByteArrayToObject unserializes an object // -------------------------------------------------------------------------- private object ByteArrayToObject(byte[] arrBytes) { using (var memStream = new MemoryStream()) { var binForm = new BinaryFormatter(); memStream.Write(arrBytes, 0, arrBytes.Length); memStream.Seek(0, SeekOrigin.Begin); Object obj = (Object) binForm.Deserialize(memStream); return obj; } } // -------------------------------------------------------------------------- /// Load loads the session variables from the HttpContext.Session object, /// unserializes it and makes it available. // -------------------------------------------------------------------------- public void Load(bool AutoInitialize = false) { byte[] b = null; Debug.WriteLine(""); Debug.WriteLine("**BEGIN SESSION LOAD"); // try and get the session variable from the http context try { b = Context.Session.Get(Name); } catch (Exception) { b = null; } // if found, then load the session data. if (b != null) { // load the data from the byte array xSessionData = (XSessionData) ByteArrayToObject(b); // now we want to verify the data is valid, ie it hasn't been tampered with byte[] CurrentHashValue = ComputeHash(xSessionData); // determine if the two byte arrays are equal. if (CurrentHashValue.SequenceEqual(xSessionData.SessionHashValue)) { // check if session expired. If not, then // increment expiration time. if (!IsExpired) { // increment the expiration data IncrementSessionExpiration(); // Saving Session Save(); } } // hashes do not match, set session corrup value else { boolSessionIsCorrupt = true; } } // b was null, session not found, initialize else { // If session not found, create a new and initialize if (AutoInitialize) { Initialize(); } } } // -------------------------------------------------------------------------- /// Saves the current session variables to the HttpContext.Session object // -------------------------------------------------------------------------- public void Save() { // if the HttpContext is not null then proceed if (Context != null) { // compute a SHA256 hash of the data xSessionData.SessionHashValue = ComputeHash(xSessionData); // convert the x_session_data to an byte array byte[] b = ObjectToByteArray(xSessionData); // set the session variable to the byte array Context.Session.Set(Name, b); } } // -------------------------------------------------------------------------- /// Destroys the Session // -------------------------------------------------------------------------- public void Kill() { if (Context != null) { if (Context.Session != null) { try { Context.Session.Remove(Name); Dispose(); } catch (Exception) { } } } } // ---------------------------------------------------------- // destroys session variables and reinitializes system // ---------------------------------------------------------- public void Reset() { // save the session increment TimeSpan tmpSessionIncrement = SessionExpirationIncrement; // reinstantiate system. ConstructorCommon(); // now rename the session incremet SessionExpirationIncrement = tmpSessionIncrement; // reinitialize system. note that save is called in // initialize so we don't have to call it again Initialize(); } // -------------------------------------------------------------------------- /// Creates a new 32 byte unique session id. Note, this uses a random number generator. /// The best idea is to use one that is active in the main application. However, if one /// isn't present, we need to create one. // -------------------------------------------------------------------------- private byte[] CreateNewSessionID() { byte[] rt = new byte[32]; // if the random number generator is null, then we need to create one. // we try and compute a unique seed so that other sessions that may be occuring // simultaneously do not get an identical number generator. if (xRND == null) { // what we are trying to do here is compute a random number between // o and xRND = new Random(); UInt32 a = (UInt32)xRND.Next(); // get the lowest byte. a = (a & 0xFF); // get the next random integer UInt32 b = (UInt32)xRND.Next(); // recompute a byte value b = (b & 0xFF000000) >> 24; // compute the seed int c = (int)Math.Abs(a ^ b); xRND = new Random(c); } // now we loop 32 bytes for (int i = 0; i < 32; i++) { // get a random integer and cast to UInt32 UInt32 RandomUInt = (UInt32) xRND.Next(); rt[i] = (byte)(RandomUInt & 0x000000FF); } return rt; } // -------------------------------------------------------------------------- /// Computes the session hash of the current state of data // -------------------------------------------------------------------------- public byte[] ComputeHash() { return ComputeHash(xSessionData); } // -------------------------------------------------------------------------- /// Computes the hash of the session variables // -------------------------------------------------------------------------- private byte[] ComputeHash(XSessionData xsCurrent) { // create a new xession data XSessionData xsNew = new XSessionData(); // copy over the session variables. Note, we won't be // copying over the session hash value. we are going to hash a // byte[0] for that value. Otherwise, we'd be recursively hashing. xsNew.SessionExpirationIncrement = xsCurrent.SessionExpirationIncrement; xsNew.SessionExpires = xsCurrent.SessionExpires; xsNew.SessionCurrentTime = xsCurrent.SessionCurrentTime; xsNew.SessionInitialized = xsCurrent.SessionInitialized; xsNew.SessionStart = xsCurrent.SessionStart; // these are the user session variables, we are going to // set a reference only, no deep copy xsNew.SessionVariables = xsCurrent.SessionVariables; // now, create a byte array by serializing the xsnew object byte[] byteArrayToHash = ObjectToByteArray(xsNew); // now, get the sha256 hash of this value. byte[] hashValue = xHash.Create(byteArrayToHash); // return the hash value return hashValue; } // -------------------------------------------------------------------------- /// Destroys the session. Called By Logout // -------------------------------------------------------------------------- public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } // -------------------------------------------------------------------------- /// Dispose method // -------------------------------------------------------------------------- protected virtual void Dispose(bool disposing) { if (disposing) { if (Context != null) { try { SessionVariables = default(T); Context = null; xSessionData = null; xHash.Dispose(); } catch (Exception) { } } } } // ************************************************************************* /// XSessionData is a class that contains both the data for the session and /// the user session variables // ************************************************************************* [Serializable] private class XSessionData { // unique session id public byte[] SessionID { get; set; } = new byte[32]; // session initializion flag public bool SessionInitialized { get; set; } = false; // session start public DateTime SessionStart { get; set; } = DateTime.MinValue; // session expires public DateTime SessionExpires { get; set; } = DateTime.MaxValue; // session Current Time public DateTime SessionCurrentTime { get; set; } = DateTime.MinValue; // session expiration increment. public TimeSpan SessionExpirationIncrement { get; set; } = new TimeSpan(0, 20, 0); // session data hash value. public byte[] SessionHashValue { get; set; } = new byte[0]; // set a new session varialbe public T SessionVariables { get; set; } = new T(); } // ************************************************** /// A Private Hash Generator. // ************************************************** private class XHash256Generator: IDisposable { private SHA256 algorithm = null; public XHash256Generator() { algorithm = SHA256.Create(); } public byte[] Create(byte[] b) { return algorithm.ComputeHash(b); } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } /// Dispose method protected virtual void Dispose(bool disposing) { if (disposing) { try { algorithm = null; } catch (Exception) { } } } } // End class XHash256Generator } // End PFSXSession class } // End namespace