iT邦幫忙

2018 iT 邦幫忙鐵人賽
DAY 24
1

定義目標

在這個系列的「IT 鐵人排程發文」這篇文章底下,
pilipala 提到想知道如何用 request 來做登入的動作,那我們今天就來實作 iThelp 的登入吧。


實際探訪

在我們登出後,若要登入 iThelp,那麼就會被導向 https://member.ithome.com.tw/login ,接著我們輸入帳號密碼,按登入後會出現一個「登入中...」的畫面,然後畫面會被帶向 https://ithelp.ithome.com.tw/ ,看看右上角有出現登入者的名稱。

接著我們來觀察一下 request,首先會有個 login 的 post request,裡面帶著我們的帳號密碼,另外還有一組 _token,而這組 _token 肯定是在登入的頁面裡面所產生的,我們檢查一下,在一個 hidden input 中找的到 _token

接著會有一個 oauth 的 get request,雖然看不到 response,但我們可以直接在新的 tab 打開,確認這個頁面其實就是顯示著「登入中...」這個頁面。觀察一下 html,這裡面的 javascript 會將我們在導向 https://ithelp.ithome.com.tw/users/callback ,同時 query string 會帶一個 code。

接著我們就跟著 js 的轉址,送出了 callback request,然後就會看到 response 有 Set-Cookie 將驗證登入的 cookie 傳給我們了,如此一來,就能完成登入並且將我們在導向 iThome 首頁。

在探訪的過程中,我們也發現到,登入的網域其實和 iThelp 首頁的網域是不同的,網域不同 cookie 就不同,而 iThelp 使用了最常見的 oAuth 的機制來串連起兩邊的登入狀態。


分解研究

要確保執行登入,那麼會拆解成以下四個步驟:

  1. 取得 _token
  2. 傳送登入 request
  3. oAuth 主頁面的登入
  4. 檢查是否登入成功

取得 _token

這部分我們直接 request https://member.ithome.com.tw/login 然後將 _token select 出來就可以了。

傳送登入 request

我們使用 postman 並帶上剛剛的取得 token 的 cookie 來測試一下,對 login post 我們的帳號密碼,然後再去 request https://member.ithome.com.tw//oauth/authorize?client_id=ithelp&redirect_uri=https%3A%2F%2Fithelp.ithome.com.tw%2Fusers%2Fcallback&response_type=code ,然後觀察一下裡面的 js redirect,裡面有包含 oAuth code 的 url,那就沒問題了。

oAuth 主頁面的登入

接著我們模擬 帶上 code 的 oAuth request,然後檢查一下 response,看到有 Set-Cookie 就表示有網站有受理這個 request。

檢查是否登入成功

最後我們 request 一下首頁,然後檢查看看 .menu__account 裡面有出現帳號名稱,那麼就確保登入完成了。


實作程式碼

getToken funxtion

首先我們先來製作 getToken function,這部分只需要一個 get request 然後在 select 出 token,接著再丟給 callback 就行了。

function getToken(callback){
  request('https://member.ithome.com.tw/login', (err, res, body)=>{
    var $ = cheerio.load(body)
    var token = $('input[name=_token]').val()
    callback(token)
  })
}

login function

接著我們來製作 login request,只需要一個 post request,同時帶上帳號、密碼、token 就能夠順利送出。

function login(token, callback){
  var options = {
    url: 'https://member.ithome.com.tw/login',
    method: 'POST',
    form: {
      _token: token,
      account: 'account',
      password: 'password',
    }
  }
  request(options, (err, res, body)=>{
    callback()
  })
}

oAuth function

接著我們來製作 oAuth 的動作,先發出一個 get request 去取得 response 中 javascript 的 code,然後在帶上 code 去呼叫 callback url,這樣就完成了。

function oauth(callback){
  request('https://member.ithome.com.tw//oauth/authorize?client_id=ithelp&redirect_uri=https%3A%2F%2Fithelp.ithome.com.tw%2Fusers%2Fcallback&response_type=code', (err, res, body)=>{
    request('https://ithelp.ithome.com.tw/users/callback?code=' + body.match(/code=(.+)'/)[1], (err, res, body)=>{
        callback()
    })
  })
}

checkLoginStatus function

最後我們來檢查是否已經成功登入,只需要 request 首頁,然後將 .menu__account 的內容丟給 callback 就完成了。

積木組合

然後我們將這四個 function 組合起來,因為都是 async function,所以就出現傳說中的 callback hell,其實這個用 promise 來做會更適合。

getToken((token)=>{
  login(token, ()=>{
    oauth(()=>{
      checkLoginStatus((result)=>{
        console.log(result);
      })
    })
  });
})

完整程式碼

const request = require('request').defaults({jar: true});
const cheerio = require('cheerio');

getToken((token)=>{
  login(token, ()=>{
    oauth(()=>{
      checkLoginStatus((result)=>{
        console.log(result);
      })
    })
  });
})

function getToken(callback){
  request('https://member.ithome.com.tw/login', (err, res, body)=>{
    var $ = cheerio.load(body)
    var token = $('input[name=_token]').val()
    callback(token)
  })
}

function login(token, callback){
  var options = {
    url: 'https://member.ithome.com.tw/login',
    method: 'POST',
    form: {
      _token: token,
      account: 'account',
      password: 'password',
    }
  }
  request(options, (err, res, body)=>{
    callback()
  })
}

function oauth(callback){
  request('https://member.ithome.com.tw//oauth/authorize?client_id=ithelp&redirect_uri=https%3A%2F%2Fithelp.ithome.com.tw%2Fusers%2Fcallback&response_type=code', (err, res, body)=>{
    request('https://ithelp.ithome.com.tw/users/callback?code=' + body.match(/code=(.+)'/)[1], (err, res, body)=>{
        callback()
    })
  })
}

function checkLoginStatus(callback){
  request('https://ithelp.ithome.com.tw/', (err, res, body)=>{
    var $ = cheerio.load(body)
    callback($('.menu__account').text())
  })
}


衍伸應用

現行有許多網站是透過 fb 或 google 登入,而這些也就是 oAuth 的應用。常常我們在玩爬蟲的時候也都需要去做登入驗證,一定都會碰到 oAuth 的狀況,透過這次的 case,我們大概可以了解到,通常 oAuth 不是去要 token 就是去要 code,有時候是直接 token 用到底,有些時候他就會塞 cookie 給你,換成 cookie 驗證,不過這一切都能透過觀察和探訪取得這些資訊。


上一篇
7-11 超商門市爬取
下一篇
台鐵(半)自動訂票
系列文
爬蟲始終來自於墮性34

1 則留言

0
pilipala
iT邦新手 5 級 ‧ 2017-12-27 11:55:09

form: {
my[email]: 'account@emal.com',
my[password]: 'password',
}
請教request時若form的欄位名與欄位值有特殊符號時要怎處理(如上例[]@)?

謝謝!

Howard iT邦新手 5 級‧ 2017-12-27 13:24:28 檢舉

每個 server 所接收和 encode 的方式可能不同,最正確的做法,是去看 source,看他到底是送了什麼出去。另外也能看 curl,同時測試看看。

node在中括號那就直接報錯了

我會了,欄位名加""就好了!/images/emoticon/emoticon01.gif

我要留言

立即登入留言