Authentication and Ajax

22 בינואר 2015

תגיות: , , , ,
אין תגובות

לקוח תיאר לי את הבעייה הבאה,

“יש לי אתר שעובד ב – forms authentication, וה – timeout מוגדר עשרים דקות (ברירת המחדל היא 30), כמובן שבצורה אוטומטית ברגע שהמשתמש לוחץ על לחצן הפונה לשרת, לאחר שהזמן תם, הוא מועבר לדף הלוגין.

לאחרונה עברנו לעבודה עם ajax, תוך שימוש ב – angular ו – singal page application, מה שקורה שברגע שהמשתמש לוחץ על לחצן (שעכשיו עושה פניית ajax) במידה שעברו ה – 20 דקות, התשובה מהשרת היא דף ה – html של הלוגין – אבל המשתמש לא רואה כלום, מכיוון שלא חזרה תשובת אין הרשאות לדפדפן”

הבעייה שלו היא די פשוטה, ודורשת כתיבת תשתית בסיסית עבור תמיכה ב – ajax, אבל לפני שנראה את הפיתרון, בואו נראה את הבעייה.

(הקוד המלא ניתן להורדה מכאן.)

הקוד משתמש בטכנולוגיות הבאות (שאת רובם אני לא מסביר אותם בפוסט הזה, אבל כן אנצל את ההזדמנות לדבר על מספר דברים של אנגולר)

  • asp.net mvc – לצד השרת
  • bootstrap  – לעיצוב
  • angular (לפוסט בסיסי) – לצד הלקוח.

 

ראשית, כדי לגרום לקוד שלנו לעבוד ללא שנצטרך לחכות כל פעם 30 דקות במהלך הדמו, נשנה את ההגדרות של הלוגין לדקה.

נפתח את קובץ הקונפיג ונכתוב תחת system.web את הקוד הבא:

Code Snippet
<authentication mode="Forms">
  <forms loginUrl="~/account/index" timeout="1"></forms>
</authentication>

מי שצריך עוד הסבר על מנגנון ה – authentication  יכול לקרוא כאן.

כעת נראה ראשית את צד השרת, הוא מכיל שני Controllers,  האחד בשם Account והשני Home.

AccountController מכיל את הקוד הבא:

Code Snippet
public class AccountController : Controller
{
    public ActionResult Index()
    {
        return View();
    }

    [HttpPost]
    public ActionResult Login(string name, string pwd)
    {
        if (name == "shlomo" && pwd == "123")
        {
            FormsAuthentication.SetAuthCookie(name, false);
            return Json(true);
        }

        return Json(false);
    }

}

בסך הכול שני Actions, הראשון שמחזיר את ה – View (שמייד נראה אותו), והשני שזו מתודת הלוגין, מקבלת שני פרמטרים, בודקת האם הלוגין תקין, (כמובן שיש כאן בדיקה hard code, ולא בדיקה אמיתית), במידה והלוגין תקין, נודיע ל – asp.net שהמשתמש הנוכחי ביצע לוגין, בעזרת קריאה למתודה FormsAuthentication.SetAuthCookie, ונחזיר לצד הלקוח תשובה שהכול תקין.

ה – HomeController מכיל גם כן שני Actions פשוטים,

Code Snippet
[Authorize]
public class HomeController : Controller
{
    //
    // GET: /Home/

    public ActionResult Index()
    {
        return View();
    }

    public ActionResult Test()
    {

        return Json("Test method Invoked at " + DateTime.Now.ToLongTimeString());
    }
}

שימו לב, שה – Controller קיבל Authorize עליו, כדי לוודא שמי שיגלוש ל – Home לא יוכל להגיע ללא לוגין (בקונפיג מוגדר הפרמטר loginUrl, שיידע להפנות אותנו בחזרה לדף הלוגין).

ויש לנו שני מתודות, אחת שמחזירה את ה – View ושהשנייה מתודה שמחזירה טקסט כלשהו עם חתימת הזמן הנוכחית.

 

קוד השרת הוא פשוט, נתחיל להסתכל על ה – UI,

בדוגמה המלאה שתוכלו להוריד אותה מהלינק למעלה, יש הרבה קוד עיצובי, שבקטעי הקוד בפוסט אני מוריד אותם לבהירות והבנת הקוד,

גלישה לדף הלוגין, תציג את הדף הבא:

image

להלן הקוד (ללא העיצוב של bootstrap)

Code Snippet
<body class="container" ng-app="account">
    <div ng-controller="loginCtrl">
        <form name="loginForm">
              <input type="text" ng-model="user.name" required />
            <input type="password" ng-model="user.pwd" class="form-control" required />
            <button ng-disabled="loginForm.$invalid" ng-click="login()">Login</button>
        </form>
        <div class="alert alert-{{status}}">
            {{message()}}
        </div>
    </div>
</body>

הקוד שלנו מקושר לאנגולר בעזרת ng-app למודול בשם account (מיד נראה אותו), ובתוכו div אחד שמקושר ל – loginCtrl.

הטופס מכיל שני inputs שכל אחד מהם מקושר בעזרת ng-model לאובייקט user של ה – controller (בצד ה – JS), וכל אחד מהם מוגדר שהוא חובה, הלחצן עצמו משתמש ב – ng-disabled כדי לוודא שכל הגדרות הולידציה על ה – inputs תקינות לפני שניתן יהיה ללחוץ על הלחצן (כרגע רק מוגדר required).

בחלק התחתון יש לנו div שתפקידו להציג הודעה לאחר לחיצה,

במידה והלוגין לא יצליח נראה הודעת שגיאה, (ואז גם ה – class שלו יוגדר עם alert-danger),

במידה והלוגין יצליח נראה ששהכול תקין, ויוצג timer של שלוש שניות שיעביר אותו מדף הלוגין. (בדרך כלל נעביר מיד, בדוגמא אני רוצה להסביר על הדרך, עוד מספר דברים של אנגולר, ולכן אני לא מעביר מיד)

הסיבה שה – message מוגדרת כפונקציה ולא כמשתנה רגיל, תוסבר בהמשך.

נסתכל על קוד ה – JS.

Code Snippet
<script>
    angular.module('account', [])
        .controller('loginCtrl', function ($scope, $http, $interval) {
            $scope.login = function () {
                $http.post('/account/login', $scope.user).success(function (res) {
                    if (JSON.parse(res)) {
                        loginSuccess();
                    }
                    else {
                        loginFail();
                    }
                });

                function loginSuccess() {
                    //….
                }

                function loginFail() {
                    //…
                }
            };
        });
</script>

אנחנו רואים כאן את הגדרת המודול account (שנרשמנו אליו באלמנט body במאפיין ng-app), בתוכו יש רישום ל – controller שמקבל את ה – scope, http, interval)

  • ה – scope ישמש אותנו עבור ה – binding.
  • ה – http עבור הפניות לשרת.
  • וה – interval כדי לאפשר מעבר לדף הבית כעבור מספר שניות אחרי שהלוגין הצליח (לא נעביר מיד, כדי שהמשתמש יוכל לראות הודעה).

בזמן לחיצה על הלחצן תופעל הפונקציה login, שתעשה פניה לשרת בעזרת http, ל – account/login, נשלח את האובייקט user, שנבנה אוטומטית דרך אנגולר מכיוון שיש לנו binding אליו, מה – view.

האובייקט, מכיל שני משתנים (name, password – לפי ההגדרה ב – ng-model), כשנקבל תשובה מהשרת, נפעיל את הפונקציה המתאימה בהתאם לתשובה.

פונקציית loginFail, נראת כך:

Code Snippet
function loginFail() {
    $scope.status = 'danger';
    $scope.message = function () {
        return 'Login fail, please try again, (User Name=shlomo and Password=123)';
    };
}

נגדיר ב – scope את הערך של status הוא – danger, כדי לקבל את האפקט של הודעת שגיאה (בקוד אמיתי, זה לא נכון להגדיר שם של מחלקת css ב – scope), ונגדיר את התוכן של message (הסיבה שזה פונקציה תוסבר במתדות loginSuccess)..

הדף ייראה כך:

image

 

במידה והמידע שהמשתמש הכניס נכון, נראה את התוצאה הבאה:

image

 

שימו לב לחלק המוקף באדום, המשתמש יראה שהוא הולך לעבור לדף הבית בעוד X שניות, כשה – X הוא משתנה של ה – scope ומשתנה ב – timer פנימי .

ההודעה עצמה מוצגת בעזרת שורת ה – binding

Code Snippet
<div class="alert alert-{{status}}">
    {{message()}}
</div>

במידה וה – message היה סתם משתנה ולא פונקציה, הטקסט הפנימי של השעון, לא היה מתעדכן בכל פעם שהערך של המשתנה שמתייחס לשעון היה משתנה.

נראה את קוד ה – loginSuccess

Code Snippet
function loginSuccess() {
    $scope.secounds = 3;
    $scope.status = 'success';
    $scope.message = function () {
        return 'Login success, you will redirect to home page in ' + $scope.secounds + ' secounds';
    };

    $interval(function () {
        $scope.secounds–;

        if ($scope.secounds == 0) {
            window.location = '/home/index';
        }
    }, 1000);
}

פונקצית ה – message, תחזיר הודעה ל – UI, כשיש בה את הערך של המשתנה secounds, בנוסף יש הפעלה של interval, שבכל שניה מורידה אחד מהמשתנה (שגורם כמובן לעדכון ההודעה על המסך) כשנגיע ל – 0, ננווט את המשתמש לדף הבית.

הסיבה לשימוש ב – interval של אנגולר, ולא ב – setInterval של JS רגיל, היא הדרך שבה אנגולר עובד.

ברמה בסיסית בכל סיום של פונקציה הוא בודק מה התעדכן ב – scope ומעדכן את ה – UI, במידה והשינוי יהיה באירוע של setInterval אנגולר לא יידע על כך, ולכן ה – UI לא ישתנה, (אפשר להתמודד עם זה, אבל הכי פשוט זה להשתמש ב – interval של אנגולר).

עד כאן ראינו את דף הלוגין, (שהיה פחות רלוונטי לבעייה עצמה) ונתקדם מכאן לדף הבית.

 

כשנגלוש למסך נראה את הדף הבא:

image

 

הלחצן Test יפעיל את מתודת Test בשרת (שראינו בתחילת הפוסט), בצד שמאל נראה את התוצאה שהשרת מחזיר.

נסתכל לרגע על הקוד ה – html.

Code Snippet
<body class="container" ng-app="app">
    <div ng-controller="testCtrl">

        <input type="button" ng-click="test()" value="Test" class="btn btn-default" />

        <p class="lead">The Respone:</p>
        <div class="well" ng-bind-html="res">
        </div>

    </div>
</body>

די דומה לקוד הקודם (מבחינת אנגולר) ה – div המרכזי, שמציג את התשובה, מקושר בעזרת ng-bind-html, מכיוון שאני רוצה להציג את ה – html שחוזר מפניית ה – ajax מהשרת.

קוד ה – JS, נראה ככה:

Code Snippet
<script>
    var app = angular.module('app', []);
    app.controller('testCtrl', function ($scope, $http, $sce, $interval) {
        $scope.test = function () {
            $http.post('/home/test').success(function (res) {
                $scope.res = $sce.trustAsHtml(res);
            });
        };
    });
</script>

כל מה שיש כאן, זה פנייה לשרת ב – ajax, שמחזיר תוצאה כלשהו, והיות שאנחנו רוצים להחזיר html, אנחנו משתמשים בפונקציה מיוחדת של אנגולר בשם sce.trustHtml, שאמור לאפשר לעשות bind לתוכן html שחוזר.

 

לחיצה על הלחצן בדקה הראשונה (כזכור, הגדרנו את ה – login timeout לדקה), תציג את התוצאה הבאה:

image

 

הבעייה, שאיתה התחלנו את הפוסט, תתרחש לאחר סיום הלוגין, כשנפנה לשרת.

image

כפי שאפשר לראות, מה שחזר מהשרת זה תוכן ה – html של דף הלוגין, הסיבה די ברורה, היות שיש את ה – attribute של authorize על HomeController, יש בדיקה בצד השרת שהמשתמש עדיין logedin, ובמידה ולא (למשל עברה דקה מהלוגין) הוא מפנה אותנו לדף הלוגין.

באפליקציות שהכול עובד עם submit (או Post Back) לא תהיה בעייה, מכיוון שהמשתמש ינווט לדף הלוגין, אבל באפליקציות מבוססות ajax, יש בעיה רצינית עם התהליך הזה, בדוגמה שלנו אנחנו מציגים את מה שחוזר מהשרת כ-  html טהור, אבל בחיים האמיתיים, אנחנו מקבלים את המידע ב – JS ומנתחים אותו, ואת התשובה שחזרה אנחנו לא ממש יודעים לנתח..

 

כדי לפתור את הבעייה, נגדיר authorize משלנו:

Code Snippet
public class AjaxAuthorizeAttribute : AuthorizeAttribute
{
    protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
    {
        if (filterContext.RequestContext.HttpContext.Request.IsAjaxRequest())
        {
            filterContext.Result = new JsonResult()
            {
                Data = "AjaxSupportAuthorize"
            };
        }
        else
        {
            base.HandleUnauthorizedRequest(filterContext);
        }
    }
}

 

במידה והקוד יופעל, זה אומר שאנחנו בזמן פנייה שאינה מורשית (HandleUnauthorizedRequest), נוודא שהפנייה הנוכחית הינה Ajax בעזרת פונקציית IsAjaxRequest, במידה וכך, נדאג שמה שחוזר לצד הלקוח, תהיה תשובה ברורה כלשהי (ולא ה – html שהשרת מחזיר בדרך כלל).

כמובן נוודא להוסיף אותו על ה – Controllers שלנו.

Code Snippet
[AjaxAuthorize]
public class HomeController : Controller
{

 

כעת בצד ה – UI, נוכל לכתוב את הקוד הבא:

Code Snippet
$http.post('/home/test').success(function (res) {
    $scope.res = $sce.trustAsHtml(res);

    if (res == '"AjaxSupportAuthorize"') {
        $('#modal').modal('show');

        $scope.secounds = 7;
        $scope.message = function () {
            return 'You need to log in, you will be redirect to login page at ' + $scope.secounds + ' secounds';
        };

        $interval(function () {
            $scope.secounds–;

            if ($scope.secounds == 0) {
                window.location = '/account/index';
            }
        }, 1000);
    }
});

 

לאחר שהשרת החזיר תשובה, במידה והתשובה היא קוד המימוש שלנו לפניות Ajax בלתי מורשות, נציג הודעת שגיאה (בעזרת דיאלוג של bootsrap – מיד נראה את ה – html), שכמו בדוגמה של הלוגין, לאחר X שניות, נעבור לדף הלוגין.

נדאג להוסיף את קטע ה – html הבא בתוך ה – div שמנוהל ע”י ה – testCtrl.

Code Snippet
<div class="modal fade" id="modal">
    <div class="modal-dialog">
        <div class="modal-content">
            <div class="modal-header">
                <h3>Ajax Support Authorize</h3>
            </div>
            <div class="modal-body">
                <p class="lead">{{message()}}</p>
            </div>
            <div class="modal-footer">
                <input type="button" class="btn btn-default" value="Redirect now" ng-click="redirect()" />
            </div>
        </div>
    </div>
</div>

 

הקוד כמעט מושלם, הבעייה היחידה, ש – MVC בודק האם מדובר בפניית ajax בפונקציה שכתבנו (IsAjaxRequest) משתמשת ב – header מיוחד, שנהוג לצרף אותו לפניות ajax, משום מה אנגולר החליטו שהם לא מצרפים אותו, מה שיגרום שכל הרעיון הנחמד שלנו לא ממש יעבוד.

כדי לתמוך בזה, נודיע לאנגולר, שכל פנית ajax שהוא עושה, אמורה להוסיף את ה – header המיוחד.

Code Snippet
var app = angular.module('app', []);

app.config(function ($httpProvider) {
    $httpProvider.defaults.headers.common["X-Requested-With"] = 'XMLHttpRequest';
});

בקונפיג של מודולים, ניתן להגדיר הגדרות שונות שיתבצעו בזמן טעינת המודול, בקוד שלנו השתמשנו ב –httpProvider שמאפשר לקנפג את פניות ה – ajax, כשכל מה שנעשה, זה לוודא שכל פניית ajax, תוסיף את ה – header המקובל לצרף לפניות ajax.

 

כעת לחיצה על לחצן test, אם עבר זמן הלוגין, תתן את התוצאה הבאה:

image

 

כשכמובן לאחר אותם X שניות, או לחיצה על redirect now תפנה אותנו לדף הלוגין.

הוסף תגובה
facebook linkedin twitter email

כתיבת תגובה

האימייל לא יוצג באתר. שדות החובה מסומנים *