技术生活

flask登录认证及动态加载js,css等静态资源

最近做flash项目的时候,遇到了一个这样的需求:

  1. 做一个用户名/密码的登录认证。
  2. 通过认证的用户,因为安全原因考虑,只能存储其登录后的token到sessionStorage。
  3. 通过认证后,页面跳到管理首页面。管理首页面的所有静态资源都要求加在HTTP headers中加token的方式进行加载。
  4. 登录页面及登录后的回调页面可以不用在HTTP header中加载token而直接加载出来。
  5. 后端登录验证的api接口也可以不用在 HTTP header中加载token而直接调用。

基本实现如下:

一、做一个 css,js,html全部inline 在一个文件中的login.html文件。再做一个登录后写入用户浏览器端sessionStorage的callback.html页面。callback.html也要求css,js,html全部inline在一个文件中。

二、程序的入口页面是index.html(因逻辑复杂,其相关的css, js, font, image等静态资源以外链方式引入)。以前没有做登录验证,现在需加一个验证的逻辑:如果浏览器sessionStorage无存储登录的token,则真直接跳入登录页login.html.

index.html
...
    function onLoaded() {
        var token = sessionStorage.getItem("token");
        if (token) {
            var name = sessionStorage.getItem("name");
            document.getElementById("hello").innerHTML = "Hello " + name + "!"
            loadModules(token);
            //fallingLeaves();
        } else {
            window.location.href = "/login";
        }
    }
...

三、login.html中提交username, password表单到后端认证接口。

login.html
...
<form class="login-form" action="/login" method="post">
    <input name="username" type="text" class="login-input" placeholder="User" required autofocus />
    <input name="password" type="password" class="login-input" placeholder="Password" required />
    <div class="submit-container">
         <button type="submit" class="login-button">SIGN IN</button>
    </div>
</form>
...

四、后端认证接口认证成功后,不能直接返回到index.html,而是要先返回到一个callback.html这样的中转页,然后再跳转回index.html.

app.py
...
@app.route('/login', methods=['POST', 'GET'])
def login():
    error = None
    if request.method == 'POST':
        if valid_login(request.form['username'],
                       request.form['password']):
            name = request.form['username']
            token = "test_token"
            return redirect(url_for("login_callback", name=name, token=token))
        else:
            error = 'Invalid username/password'
    # the code below is executed if the request method
    # was GET or the credentials were invalid
    return render_template('login.html', error=error)
...

五、中转页callback.html存储认证接口返回的token到客户端浏览器的sessionStorage,然后再跳转回index.html。

callback.html
...
<body onload="javascript: onLoaded();">
    <!--
        Note:
        In this page, we store user's token into sessionStorage and redirect it to Home page.
    -->
</body>
<script>
    function onLoaded(){
        var token = sessionStorage.getItem("token");
        if(token){
            window.location.href = "/";
        } else{
            const urlParams = new URLSearchParams(window.location.search);
            if(urlParams.has('token') && urlParams.has('name')){
                token = urlParams.get('token');
                name = urlParams.get('name');
                sessionStorage.setItem("token", token);
                sessionStorage.setItem("name", name);
                window.location.href = "/";
            } else {
                window.location.href = "/login";
            }
        }
    }
</script>
...

六、管理首页面,把原来通过src,href 等方式引用的静态资源,通过动态加载的方式,添加token到HTTP header之后,再从服务端加载出来。

1.加载css
    httpGet("/static/css/default.css", token, loadCssCallbackHandler);

2.httpGet的具体实现,加载前会在header中加token,加载后会执行callbackHandler以进行进一步的处理
    /*
    References:
        https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest
    */
    function httpGet(url, token, callbackHandler, postLoadFunction) {
        var request = new XMLHttpRequest();
        // set readystatechange listener
        request.addEventListener('readystatechange', function (e) {
            if (request.readyState == 2 && request.status == 200) {
                // Download is being started
            }
            else if (request.readyState == 3) {
                // Download is under progress
            }
            else if (request.readyState == 4) {
                // Downloading has finished
                // request.response holds the binary data of the font
                if (callbackHandler && typeof (callbackHandler) == "function") {
                    callbackHandler(url, request.responseText, postLoadFunction);
                }
            }
        });
        // Open url to request resource
        //  open(method, url, async, user, psw)
        //  method: the request type GET or POST
        //  url: the file location
        //  async: true (asynchronous) or false (synchronous)
        //  user: optional user name
        //  psw: optional password
        request.open("GET", url, true);
        // set request header
        request.setRequestHeader("x-token", token);
        //usr GET method
        request.send(null);
        //USE POST Method
        //xmlHttp.send({"key": "test_key", "value": "test_value"});
    }

3.loadCssCallbackHandler的具体实现.主要是处理一下字体和背景图片.
    /*
    References:
        https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions
        https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/matchAll
        https://regex101.com/
    */
    function loadCssCallbackHandler(cssUrl, cssText, postLoadFunction) {
        // compress css text
        cssText = cssText.replace(/\s+/g, '');
        console.log(cssText);
        var cssRootUrl = cssUrl.substr(0, cssUrl.lastIndexOf("/") + 1);
        var replacedText = processFontFaces(cssRootUrl, cssText);
        replacedText = processBackgroundImages(cssRootUrl, cssText);
        createStyleTag(replacedText);
    }

4.processFontFaces的具体实现.处理字体时,使用正则表达式匹配出所有诸如'@font-face{...}'这样的字体资源,然后以header中加token的方式进行加载,同时把相关的字体资源文本中css文本中抹去.
    function processFontFaces(cssRootUrl, cssText) {
        var fontFaces = [];
        var regex = /@font-face{font-family:'([^']*)';src:(?<urls>(url\('?([^']*)'?\)format\('([^']*)'\)[,;]){1,})[\w-:;]{1,}}/g;
        var match = regex.exec(cssText);
        while (match != null) {
            //console.log(match[0]);
            var fontFaceName = match[1];
            //console.log(fontFaceName);
            var fontFamily = { fontFamily: fontFaceName, urls: [] };
            fontFaces.push(fontFamily);
            //console.log(match[2]);
            var regexUrl = /url\('([^']*)'\)/g
            var innerMatch = regexUrl.exec(match[2]);
            while (innerMatch != null) {
                var fontFaceUrl = cssRootUrl + innerMatch[1];
                //console.log(fontFaceUrl);
                fontFamily.urls.push(fontFaceUrl);
                innerMatch = regexUrl.exec(match[2]);
            }
            match = regex.exec(cssText);
        }
        fontFaces.forEach(function (fontFace) {
            console.log("familyName:" + fontFace.fontFamily + ",url:" + fontFace.urls[0]);
            loadFont(fontFace.fontFamily, fontFace.urls[0]);
        });
        var replacedText = cssText.replace(regex, "");
        return replacedText;
    }

5.processBackgroundImages的具体实现.这里只替换了一下背景图片的相对路径.因为原css中引用图片是以css文件所在目录为当前目录.但动态加载时当前的目录变为js执行所在文件的目录.如不替换,则图片不能正确加载.但是还没有想办法把图片也以header中加token的方式进行加载.
    function processBackgroundImages(cssRootUrl, cssText) {
        var regex = /([.#]\w*){[\w\d:;%'-]*(?<bkimg>background[-image]*:url\('?([^']*(.png|.jpg|.bmp|.gif|.jpge|.svg))'?\);?)[\w\d:;%'-\s]*}/g;
        var backgroundImages = [];
        var match = regex.exec(cssText);
        while (match != null) {
            var backgroundImage = match[1];
            var bkImg = { className: backgroundImage, backgroundImageUrl: '', bkImageStyle: match[2] };
            backgroundImages.push(bkImg);
            //console.log(backgroundImage);
            var imageUrl = match[3];
            //console.log(imageUrl);
            bkImg.backgroundImageUrl = imageUrl;
            match = regex.exec(cssText);
        }
        var replacedText = cssText;
        backgroundImages.forEach(function (backgroundImage) {
            console.log(backgroundImage.backgroundImageUrl + "\r\n");
            replacedText = replacedText.replace(backgroundImage.backgroundImageUrl, cssRootUrl + backgroundImage.backgroundImageUrl);
        });
        console.log(replacedText);
        return replacedText;
    }

6.loadFont的具体实现.通过在header中加token的方式动态加载woff字体资源,注意其返回类型是'arraybuffer'.注意完成加载后,是对body的fontFamily进行了动态设置.项目中有灵活变动.
    /*
    References:
        https://usefulangle.com/post/74/javascript-dynamic-font-loading
    */
    function loadFont(fontFaceName, fontFileUrl) {
        var request = new XMLHttpRequest();
        request.addEventListener('readystatechange', function (e) {
            if (request.readyState == 2 && request.status == 200) {
                // Download is being started
            }
            else if (request.readyState == 3) {
                // Download is under progress
            }
            else if (request.readyState == 4) {
                // Downloading has finished
                // request.response holds the binary data of the font
                var junction_font = new FontFace(fontFaceName, request.response);
                junction_font.load().then(function (loaded_face) {
                    document.fonts.add(loaded_face);
                    document.body.style.fontFamily = '"' + fontFaceName + '"';
                }).catch(function (error) {
                    // error occurred
                });
            }
        });
        request.addEventListener('progress', function (e) {
            var percent_complete = (e.loaded / e.total) * 100;
            console.log(percent_complete);
        });
        request.responseType = 'arraybuffer';
        // Downloading a font from the path
        request.open('GET', fontFileUrl);
        request.send();
    }

7.createStyleTag的具体实现.抹去诸如'@font-face{...}'这样的字体引用文本之后,动态地把css文本加入到页面中.
    function createStyleTag(cssText) {
        if (cssText) {
            var css = cssText
            var head = document.head || document.getElementsByTagName('head')[0];
            var style = document.createElement('style');
            style.appendChild(document.createTextNode(cssText));
            head.appendChild(style);
        }
    }

七、管理首页面,动态加载js并执行必要的回调函数。

1.加载js
    // load js
    httpGet("/static/js/TweenMax.min.js", token, loadJsCallbackHandler, fallingLeaves);

2.加载后执行JS文本处理回调.其实js并没像css那样做替换或抹去处理,故而直接创建script标签.
    function loadJsCallbackHandler(jsUrl, jsText, postLoadFunction) {
        // compress js text
        //jsText = jsText.replace(/\s+/g, '');
        //console.log(jsText);
        createScriptTag(jsText, postLoadFunction);
    }

3.动态创建script标签. 注意创建完script标签后,可以根据实际需要,执行一个回调,以保证程序正常运行.
    function createScriptTag(jsText, postLoadFunction) {
        if (jsText) {
            var head = document.head || document.getElementsByTagName('head')[0];
            var script = document.createElement('script');
            //script.onload = postLoadFunction;
            script.appendChild(document.createTextNode(jsText));
            head.appendChild(script);
            if (postLoadFunction&& typeof (postLoadFunction) == "function") {
                postLoadFunction();
            }
        }
    }

八、参考与引用:

  1. 动态加载字体: https://usefulangle.com/post/74/javascript-dynamic-font-loading
  2. javascript正则表达式: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions
  3. javascript正则表达式在线测试: https://regex101.com/
  4. javascript代码在线编辑器: https://playcode.io/
  5. httpRequest在线参考: https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest
  6. flask在线参考: https://flask.palletsprojects.com/en/1.1.x/quickstart/
  7. 源码下载: https://jackzhou.co/flask_study.zip

发表评论

电子邮件地址不会被公开。 必填项已用*标注