asp.net mvc使用JWT代替session,實現單點登陸

1. 什麼是Token?

什麼是token?token可以理解爲是一種令牌,常用在計算機身份認證。在與服務器進行數據傳輸之前,會進行身份覈驗。

2. 什麼是JWT?

什麼是JWT? JWT是Json Web Token的簡稱,是一種Token的規範。就是一個加密後的字符串,組成部分爲A.B.C。該字符串是由記錄token的加密方式,字符串長度(A部分),基本的用戶信息,載荷,簽發人,過期時間等(B部分),以及A和B共同的加密部分(C部分)構成。

3. Token與Session比較

傳統Session所暴露的問題
Session: 用戶每次在計算機身份認證之後,在服務器內存中會存放一個session,在客戶端會保存一個cookie,以便在下次用戶請求時進行身份覈驗。但是這樣就暴露了兩個問題。第一個問題是,session是存儲到服務器的內存中,當請求的用戶數量增加時,會加重服務器的壓力。第二個問題是,若是有多臺服務器,而session只能存儲到當前的某一臺服務器中,這就不適用於分佈式開發。

CSRF: Session是基於cookie來進行用戶識別的,如果cookie被截獲,用戶就很容易受到跨站請求僞造攻擊,本文暫時不考慮csrf(cross site request forgery)。

Token的驗證機制
token的驗證不需要在服務器端保留任何的用戶信息,因此,當用戶再客戶端通過單點登陸後,可以訪問多臺服務器,利於分佈式開發。而且token的是一串加密後的字符串,可以設置過期日期,不容易被仿造。

使用token,客戶端和服務端的交互流程大致是如下:

  1. 用戶使用用戶名密碼來請求服務器
  2. 服務器進行驗證用戶的信息
  3. 服務器通過驗證發送給用戶一個token
  4. 客戶端存儲token,並在每次請求時附送上這個token值
  5. 服務端驗證token值,並返回數據

token可以存放在cookie中,也可以保存在請求頭中,建議將token放到請求頭中,並且token攜帶mac地址和機器名。

4. ASP.NET MVC如何使用jwt實現單點登陸

定義一個UserState類

namespace LYQ.TokenDemo.Models.Infrastructure
{
    public class UserState
    {
        public string UserName { get; set; }
        public string UserID { get; set; }
        public int Level { get; set; }
    }
}

定義一個AppManager類和TokenInfo類

   public static UserState UserState
        {
            get
            {
                HttpContext httpContext = HttpContext.Current;
                var cookie = httpContext.Request.Cookies[Key.AuthorizeCookieKey];
                var tokenInfo = cookie?.Value ?? "";
                //token 解密
                var encodeTokenInfo = TokenHelper.GetDecodingToken(tokenInfo);
                UserState userState = JsonHelper<UserState>.JsonDeserializeObject(encodeTokenInfo);               
                return userState;
            }
        }
        
 public class TokenInfo
    {
        public TokenInfo()
        {
            iss = "LYQ";
            iat = (DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc)).TotalSeconds;
            exp = iat + 300;
            aud = "";
            sub = "LYQ.VIP";
            jti = "LYQ." + DateTime.Now.ToString("yyyyMMddhhmmss");
        }

        public string iss { get; set; }
        public double iat { get; set; }
        public double exp { get; set; }
        public string aud { get; set; }
        public double nbf { get; set; }
        public string sub { get; set; }
        public string jti { get; set; }

    }

定義JsonHelper

 public class JsonHelper<T> where T : class
    {
        public static T JsonDeserializeObject(string json)
        {
            return JsonConvert.DeserializeObject<T>(json);
        }

        public static string JsonSerializeObject(object obj)
        {
            return JsonConvert.SerializeObject(obj);
        }
    }

在Home控制器中定義一個Login的方法

        [HttpGet]
        [LYQ.TokenDemo.Models.CustomAttribute.Authorize(false)]
        public ActionResult Login()
        {
            return View();
        }

        [HttpPost]
        [LYQ.TokenDemo.Models.CustomAttribute.Authorize(false)]
        public ActionResult Login(string account, string password)
        {
            if (account == "Tim" && password == "abc123")
            {
                var cookie = new HttpCookie(Key.AuthorizeCookieKey, TokenHelper.GenerateToken());
                HttpContext.Response.Cookies.Add(cookie);
                return Json("y");
            }
            else
            {
                var cookie = new HttpCookie(Key.AuthorizeCookieKey, "");
                HttpContext.Response.Cookies.Add(cookie);
                return Json("n");
            }
        }

生成token
使用NuGet,下載JWT.dll

namespace LYQ.TokenDemo.Models
{
    public class TokenHelper
    {
        //jwt私鑰,不能公佈
        private const string SecretKey = "LYQ.abcqwe123";

        public static string GenerateToken()
        {
            var tokenInfo = new TokenInfo();
            var payload = new Dictionary<string, object>
            {
                {"iss", tokenInfo.iss},
                {"iat", tokenInfo.iat},
                {"exp", tokenInfo.exp},
                {"aud", tokenInfo.aud},
                {"sub", tokenInfo.sub},
                {"jti", tokenInfo.jti},
                { "userName", "Tim" },
                { "userID", "001" },
                { "level",18}
            };

            IJwtAlgorithm algorithm = new HMACSHA256Algorithm();
            IJsonSerializer serializer = new JsonNetSerializer();
            IBase64UrlEncoder urlEncoder = new JwtBase64UrlEncoder();
            IJwtEncoder encoder = new JwtEncoder(algorithm, serializer, urlEncoder);
           
            var token = encoder.Encode(payload, SecretKey);
            return token;
        }

        public static string GetDecodingToken(string strToken)
        {
            try
            {
                IJsonSerializer serializer = new JsonNetSerializer();
                IDateTimeProvider provider = new UtcDateTimeProvider();
                IJwtValidator validator = new JwtValidator(serializer, provider);
                IBase64UrlEncoder urlEncoder = new JwtBase64UrlEncoder();
                IJwtDecoder decoder = new JwtDecoder(serializer, validator, urlEncoder);

                var json = decoder.Decode(strToken, SecretKey, verify: true);
                return json;
            }
            catch (Exception)
            {
                return "";
            }
        }
    }
}

自定義身份認證
本文這裏採取的是自定義的身份認證模式,自定義了一個AuthorizeAttribute。

namespace LYQ.TokenDemo.Models.CustomAttribute
{
    public class AuthorizeAttribute : FilterAttribute, IAuthorizationFilter
    {
        public AuthorizeAttribute(bool _isCheck = true)
        {
            this.isCheck = _isCheck;
        }

        private bool isCheck { get; }

        public void OnAuthorization(AuthorizationContext filterContext)
        {
            var httpContext = filterContext.HttpContext;
            var actionDescription = filterContext.ActionDescriptor;

            if (actionDescription.IsDefined(typeof(AllowAnonymousAttribute), false) ||
                actionDescription.ControllerDescriptor.IsDefined(typeof(AllowAnonymousAttribute), false)) { return; }

            if (!isCheck) return;

            if (AppManager.UserState == null)
            {
                if (httpContext.Request.IsAjaxRequest())
                {
                    filterContext.Result = new JsonResult()
                    {
                        Data = new { Status = "Fail", Message = "403 Forbin", StatusCode = "403" },
                        JsonRequestBehavior = JsonRequestBehavior.AllowGet
                    };
                }
                else
                {
                    filterContext.Result = new RedirectResult(("/Home/Login"));
                }
            }
            else
            {
                //每次身份驗證通過後,重新響應一個新的token給客戶端
                var cookie = new HttpCookie(Key.AuthorizeCookieKey, TokenHelper.GenerateToken());
                filterContext.HttpContext.Response.Cookies.Add(cookie);
            }
        }
    }
}

HTML頁面

@{
    ViewBag.Title = "Login";
}

<link href="~/Content/bootstrap.min.css" rel="stylesheet" />

<h2>This is login page.</h2>

<div class="container">
    <form class="box-body" action="/Home/Login" method="post">
        <div class="form-group row">
            <label class="col-sm-1 col-md-1">Account:</label>
            <div class="col-sm-5 col-md-5">
                <input type="text" class="form-control" id="account" name="account" />
            </div>
        </div>
        <div class="form-group row">
            <label class="col-sm-1 col-md-1">Password:</label>
            <div class="col-sm-5 col-md-5">
                <input type="password" class="form-control" id="password" name="password" />
            </div>
        </div>
        <div class="form-group row">
            <div class="col-sm-1 col-md-1"></div>
            <div class="col-sm-5 col-md-5">
                <button type="button" class="btn btn-info" onclick="Login();">Login</button>
                <button type="reset" class="btn btn-info">Reset</button>
            </div>
        </div>
        <div class="form-group row">
            <div class="col-sm-1 col-md-1"></div>
            <div class="col-sm-5 col-md-5">
                <span>account:Tim; password:abc123</span>
            </div>
        </div>
    </form>
    
</div>

<script src="~/Scripts/jquery-3.3.1.min.js"></script>
<script src="~/StaticFiles/Frontend/Scripts/Common.js"></script>

<script>

    function Login() {
        var paras =
        {
            account: $("#account").val(),
            password: $("#password").val()
        };

        LYQ.sendAjaxRequest({
            type: "post",
            url: "/Home/Login",
            param: paras,
            dataType: "json",
            callBack: function (result) {
                if (result == "y") {
                    console.log("Login success");
                    alert("Login success");
                    window.location = "/";
                } else {
                    console.log("Login fail");
                    alert("Login fail");
                }
            }
        });     
    }

</script>

Common.js

!(function (window) {
    var functions = {
        sendAjaxRequest: function (opts) {
            var self = this;
            $.ajax({
                type: opts.type || "post",
                url: opts.url,
                data: opts.param || {},
                contentType: opts.contentType === null ? true : opts.contentType,
                cache: opts.cache === null ? true : opts.cache,
                processData: opts.processData === null ? true : opts.processData,
                beforeSend: function (XMLHttpRequest) {
                    XMLHttpRequest.setRequestHeader(LYQ.getAuthorizationKey(), "");
                },
                dataType: opts.dataType || "json",
                success: function (result) {
                    if (Object.prototype.toString.call(opts.callBack) === "[object Function]") {   //判斷callback 是否是 function               
                        opts.callBack(result);
                    } else {
                        console.log("CallBack is not a function");
                    }
                }
            });
        },
        getRequestHeaderAuthorizationToken: function () {
            var document_cookie = document.cookie;
            //var reg = new RegExp("(^| )" + name + "=([^;]*)(;|$)");
            //if (document_cookie = document.cookie.match(reg))
            //    return unescape(arr[2]);
            //else
            //    return null;
            console.log(document_cookie);
            return document_cookie;
        },
        getAuthorizationKey: function () {
            return 'Authorization';
        }
    };

    window.LYQ = functions;
})(this);

源碼地址:

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章