---
# System prepended metadata

title: Flask實作_WEB API_03_身份驗證
tags: [flask, python, api]

---

# Flask實作_WEB API_03_身份驗證
###### tags: `python` `flask` `api`
開發網頁的時候我們可以利用flask_login來記錄使用者的登入狀況，利用cookie讓使用者在切換網頁的時候可以判斷是否已經正確登入。但是依據REST所述，API是一種沒有狀態的服務，這意味著我們沒有辦法利用flask_login來記錄目前呼叫API的用戶端是否已經是登入狀況，怎麼辦?

## 說明
現在我們遇到一個問題，在沒有辦法利用cookie的情況下怎麼知道這個使用者是否被授權?總不能每次使用者呼叫一次就輸入一次帳號密碼然然後跟著每次的呼叫帶著走，這有資安風險。因此，作法上，我們會在使用者第一次呼叫的時候讓他們輸入帳號密碼，然後回傳一個token，而且這個token還必需要有著有效期限制，然後在使用者每次呼叫api的時候就帶著這組token來做身份上的認證。這聽起來很熟悉?就是我們在開發網頁的時候所用到的註冊帳號時候所用的手法，忘了?可以回憶一下[Flask實作_建置一個使用者註冊頁面_07產生一個用戶認證連結](https://hackmd.io/@shaoeChen/ryhSwsOBM)。

實作這種作法之前要先認識一種認證方式，[OAuth 2.0](https://oauth.net/2/)，如果想瞭解可以看[良葛格在iThome](https://www.ithome.com.tw/voice/129385)上的說明。舉例來說，第三方登入，可能在某個網站上問你要不要加入會員，要嘛申請一個新的，要嘛就是使用你的facebook或google身份，如果你選擇採用facebook或google身份的時候，就會有一個視窗跟你說，你的blahblahblah將被使用之類的，在你輸入完帳號、密碼之後你可以順利的註冊登入。這時候其實你的facebook或google的帳號密碼並不會被這個網站所取得，你在登入facebook或google的時候所取得的是一組access token，你登入想登入的網站所依賴的就是這個access token。

![](https://i.imgur.com/hotFU0L.png)

## 實作
![](https://i.imgur.com/JfLIRbY.png)

實作上的部份可以參考[阮一峰網站說明](https://www.ruanyifeng.com/blog/2019/04/oauth-grant-types.html)

上圖是我們預計採用的模式，Client的部份就是使用者使用的設備之類的，像是你用手機或桌機。這邊預計實作的是Password Credentials，因為最簡單。另外，不管如何，實際上線還是建議採用HTTPS。

首先，一樣的專案，一樣的python文件，清空，重來：
```python=
from flask import Flask, jsonify, request
from flask.views import MethodView
from itsdangerous import TimedJSONWebSignatureSerializer as TJSS

app = Flask(__name__)
app.config['SECRET_KEY'] = 'ABCDEFhijklm'


class AuthorToken(MethodView):
    def post(self):

        grant_type = request.form.get('grant_type')
        username = request.form.get('username')
        password = request.form.get('password')

        # 判裝grant_type是否為password，這是我們預計採用的模式
        if grant_type is None or grant_type != 'password':
            response = jsonify(message='bad grant type!')
            response.status_code = 400
            return response

        # 測試用，僅判斷是否為shaoe.chen
        if username != 'shaoe.chen' or password != '123456':
            response = jsonify(message='wrong username or password!')
            response.status_code = 400
            return response

        # 產生token，有效期設置為3600秒
        res = TJSS(app.config['SECRET_KEY'], expires_in=3600)
        token = res.dumps({'username': username}).decode('utf-8')

        return token


app.add_url_rule('/author_token/', view_func=AuthorToken.as_view('author_token'))

if __name__ == '__main__':
    app.run(debug=True)

```

接下來執行專案，然後另外開一個python command line：
```python
>>> import requests
>>> url = 'http://127.0.0.1:5000/author_token/'
>>> res = requests.post(url=url, data={'grant_type': 'password', 'username': 'shaoe.chen', 'password': '123456'})
res.text
'eyJhbGciOiJIUzUxMiIsImlhd....(以下省略)
```

故意弄一個錯誤的呼叫：
```python
>>> res2 = requests.post(url=url, data={'grant_type':'password', 'username': 'shaoe.chen1', 'password': '123456'})
>>> res2.status_code
400
```

只是，我們要設置的不僅僅是單純的回傳token，而是要符合[RFC6750](https://tools.ietf.org/html/rfc6750)中所定義的`Bearer`，除了`Bearer`之外，Authentication schemes尚有`Basic`，`Digest`...，這部份如有相關需求就關鍵字搜尋一下。

調整我們的程式碼：
```python=
from flask import Flask, jsonify, request
from flask.views import MethodView
from itsdangerous import TimedJSONWebSignatureSerializer as TJSS

app = Flask(__name__)
app.config['SECRET_KEY'] = 'ABCDEFhijklm'


class AuthorToken(MethodView):
    def post(self):

        grant_type = request.form.get('grant_type')
        username = request.form.get('username')
        password = request.form.get('password')

        # 判裝grant_type是否為password，這是我們預計採用的模式
        if grant_type is None or grant_type != 'password':
            response = jsonify(message='bad grant type!')
            response.status_code = 400
            return response

        # 測試用，僅判斷是否為shaoe.chen
        if username != 'shaoe.chen' or password != '123456':
            response = jsonify(message='wrong username or password!')
            response.status_code = 400
            return response

        # 產生token，有效期設置為3600秒
        s = TJSS(app.config['SECRET_KEY'], expires_in=3600)
        token = s.dumps({'username': username}).decode('utf-8')
        # 回傳符合RFC 6750的格式
        response = jsonify({
            'access_token': token,
            'token_type': 'Bearer',
            'expires_in': 3600
        })
        response.headers['Cache-Control'] = 'no-store'
        response.headers['Pragma'] = 'no-cache'

        return response


app.add_url_rule('/author_token/', view_func=AuthorToken.as_view('author_token'))

if __name__ == '__main__':
    app.run(debug=True)

```
第31行：我們不再單純的回傳token，而是回傳符合RFC6750格式的響應。

現在讓我們重新取得響應看看：
```json
{
    'access_token': 'eyJhbGciOiJIUzUxMiIsIml..(中略)', 
    'expires_in': 3600, 
    'token_type': 'Bearer'
}
```

事實上，如果你有認真的去看一下RFC6750的定義的話，裡面還有放一個`refresh_token`，不過這邊暫時沒有提到，後續有實作的機會再來提比較有感覺。

## 結論
這次我們討論了一個身份認證的方式，OAuth 2.0，這有四種方式，我們所用的是一個比較簡單的方式，而這種方式適用於對伺服器的信任，另外要注意的是，正式的生產環境上一定要使用HTTPS。再次的，這邊對OAuth的說明只是單純的過水，如果真的要用，多少要看一下文件會比較恰當。

下一次，我們來看看應該怎麼利用取得的access token做資源的請求。