almost 2 years ago

最近需要用Angular2實作Oauth2認證流程,但是原本的方法會先讓整個NG2 App重導到外部頁面
之後如果要從Server要資料就十分的不方便,所以改用Pop頁面處理,這篇主要講解法,Oauth2申請流程等請參考前一篇網誌
以下是效果圖:


源碼在此,作法說明圖:

Angular2與Popup頁面溝通方式透過window.postMessage()
在Angular2 Login Component中,有一個github的按鈕,點選後觸發註冊流程,我將註冊流程包成一個Directive,命名為myOauth,在Login Comp中

<button id="github-login" myOauth [oauthUrl]="'http://127.0.0.1:3000/user/github'" (oauthResponse)="handleOauthResponse($event)"> Github </button>

其中oauthUrl代表自己Server的路徑,在對應Nodejs中

let github_oauth_url = "https://github.com/login/oauth/authorize" +
        "?client_id=" + github_client_id  +
        "&scope=user";
    res.json({'redirect_url':github_oauth_url});

之所以不想直接在Angular2直接放github的認證頁面是為了保護自己的github client id不要外漏(理論上Server總比瀏覽器安全)

接著看Oauth Directive,oauthService表示Angular2處理Popup頁面的資料傳輸,主要就是看Popup頁面傳什麼訊息坐相對應的事,其中"close"表示Popup頁面關閉同時要關閉Event Listener避免重複綁定事件,而"redirect"表示Popup頁面成功跳轉。
onClick單純把資料回傳,使用EventEmitter

@HostListener('click') onClick(ev){
        this.subOpenOauthPage = this.oauthService(this.oauthUrl).subscribe((data)=>{
            console.log(data);
            this.oauthResponse.emit(data);
        },(err)=>{
            console.log(err);
            this.oauthResponse.emit({msg:"err"});
        });
    }
    
oauthService(oauthUrl:string) {
        let promise = new Promise((resolve, reject) => {
            let oauth_page;
            //開啟新頁面,處理oauth
            oauth_page = window.open("", "Oauth Page", "width="+window.innerWidth+"height="+window.innerHeight);
            oauth_page.document.write(this.oauthPageHTML);

            //母頁面聆聽
            window.addEventListener('message', function recieveData(e) {
                switch (e.data.msg){
                    //子頁面準備完成,母頁面發送oauthUrl
                    case "ready":
                        oauth_page.postMessage(oauthUrl, location.origin);
                        break;
                    case "error":
                        window.removeEventListener('message', recieveData, false);
                        reject("error");
                        break;
                    case "close":
                        window.removeEventListener('message', recieveData, false);
                        break;
                    case "redirect":
                        window.addEventListener('message',recieveData, false);
                        break;
                    //成功收到資料,Promise結束
                    case "success":
                        window.removeEventListener('message', recieveData, false);
                        resolve(e.data);
                        break;
                }
            }, false);
        });
        return Observable.fromPromise(promise);
    }

接著是Popup頁面的程式碼,主要就是頁面創建後向母頁面回說ready,接著就等母頁面傳url,後續用XHR執行步驟(2,3),成功後跳轉。

private oauthPageHTML:string = `
        <script type="text/javascript">
            console.log('ready');
            (function(){
                //回傳母頁面:準備完成
                window.opener.postMessage({msg:'ready'}, location.origin);
                
                window.addEventListener('message', function reciever(e) {
                    //確保訊息來源與母頁面相同
                    if(e.origin == location.origin){
                        var xhttp = new XMLHttpRequest();
                        if(!e.data){
                            console.log('oauth miss oauth url');
                        }
                        console.log(e.data);
                        xhttp.open("GET", e.data, true);
                        xhttp.withCredentials = true;
                        xhttp.send();
                        xhttp.onreadystatechange = function () {
                            //成功收到伺服器回傳的oauth2頁面後,跳轉
                            console.log(xhttp);
                            if (xhttp.status == 200) {
                                window.opener.postMessage({msg:'redirect'}, location.origin);
                                window.location = JSON.parse(xhttp.responseText).redirect_url;
                                window.removeEventListener('message', recieveData, false);
                            }else{
                                window.opener.postMessage({msg:'error'}, location.origin);
                                window.removeEventListener('message', recieveData, false);
                            }
                        };
                    }
                }, false);
            })();
            //頁面關閉時通知母頁面
            window.onunload = function(){
                window.opener.postMessage({msg:'close'}, location.origin);
                window.removeEventListener('message', recieveData, false);
            }
        </script>
    `

NodeJS Server處理Github callback

router.get('/github/callback', function(req, res) {
    //拿code換access_token
    let code = req.query.code;
    githubOauthHandle(code, (err, body) => {
        //body中就是所有的資料,我是只有取出id使用
        //cookie可以正常使用喔!
        res.cookie(....);
        //資料透過動態渲染,資料放content中,詳述見下
        res.render('oauth_redirect', {
        status: "success",
        content: JSON.stringify({
                user: {
                    isLogin: true,
                    id: user.id
                }
            })
        });
    });
});

function githubOauthHandle(code, cb){
    let token_option = {
        url:"https://github.com/login/oauth/access_token",
        method:"POST",
        form:{
            code: code,
            client_id: github_client_id,
            client_secret: github_secret_id
        }
    };
    request(token_option, function(err, response, body){
        if(err){
            returnErrorCode(response,res);
            return;
        }
        //回傳值不是JSON Format,所以要自己用Regular Expression取出
        let regex = /\=([a-zA-Z0-9]+)\&([a-zA-Z])+\=([a-zA-Z0-9]+)/;
        let result = body.match(regex);
        let token = result && result[1];
        if(!token){
            returnErrorCode("bad internet connection",res);
            return;
        }
        console.log(body);
        //拿access_token換使用者資料
        let info_option = {
            url:"https://api.github.com/user",
            method:"GET",
            headers:{
                "User-Agent": "Awesome-Octocat-App",
                "Authorization":"token "+ token
            }
        };
        request(info_option, function(err, response, body){
            cb(err, body);
        });
    });
};

最後一個大問題 原本的Popup頁面也因為認證而跳轉了,我如何將Server的資料送回Angular2中呢?
答案是同樣使用window.postMessage(),因為window本身沒有改變,只有裡頭的document變了而已!
所以還是可以跟母頁面進行溝通,這邊我把Server取得的資料用動態Render方式回傳(也就是Data Page)
Data Page中有內嵌JS code,主要就讀值、回傳

doctype html
html
    head
        title= "Oauth Page"
    body
        p Welcome to Oauth page
        p#status #{status}
        p#content #{content}
        script(type="text/javascript").
            window.onload = function(){
                // return msg to host window
                if(document.getElementById('status').innerHTML=='success'){
                    window.opener.postMessage({
                        msg: 'success',
                        info: document.getElementById('content').innerHTML
                    }, '*');
                    window.close();
                }
                window.opener.postMessage({msg:'error', infor:document.getElementById('content').innerHTML} , '*');
                window.close();
            };
← CSS-常用功能與佈局 Angular2 - 練習一:TodoList →
 
comments powered by Disqus