10.9. การพิสูจน์ตัวตนสำหรับไคลเอนต์แบบโปรแกรม¶
Bearer token คือวิธีที่โทรศัพท์, ไมโครคอนโทรลเลอร์คู่หู และ cloud worker พิสูจน์ตัวตนกับเซิร์ฟเวอร์ -- ไคลเอนต์ใส่ token ใน Authorization header ในทุกคำขอและเซิร์ฟเวอร์ตรวจสอบ แอปโทรศัพท์คู่หูส่ง POST "ack" เมื่อเจ้าของแตะ "เห็น" บนการแจ้งเตือนการเคลื่อนไหว คำขอเหล่านั้นต้องการ token
10.9.1. ความลับการลงนาม¶
ความลับคือคีย์ส่วนตัวของกล้องสำหรับการลงนามและการยืนยัน token สร้าง 32 ไบต์สุ่มจาก hardware RNG บนการบูตครั้งแรกของกล้อง บันทึกลงในระบบไฟล์ และนำไบต์เดียวกันมาใช้ซ้ำในทุกการบูตถัดไป:
# auth/tokens.py
import os
try:
with open('secret.bin', 'rb') as f:
SECRET = f.read()
except OSError:
SECRET = os.urandom(32)
with open('secret.bin', 'wb') as f:
f.write(SECRET)
os.urandom() เป็นแหล่งสุ่มที่เหมาะสมทางเข้ารหัสลับบนพอร์ตกล้องแต่ละพอร์ต -- บนพอร์ตส่วนใหญ่จะอ่านตัวสร้างตัวเลขสุ่มฮาร์ดแวร์ของชิปโดยตรง ไฟล์อยู่ในไดเรกทอรีทำงานของกล้อง (/sdcard ถ้ามีการ์ด SD ติดตั้ง, /flash มิฉะนั้น) และไม่เคยออกจากกล้อง การลบ secret.bin และรีบูตจะหมุนเวียนความลับและทำให้ token ทุกตัวที่ออกภายใต้ตัวเก่าไม่ถูกต้อง
Note
machine.unique_id() ไม่ใช่ สิ่งทดแทน บนพอร์ตกล้องบางพอร์ต มันยังถูกใช้เพื่อสร้างที่อยู่ MAC เครือข่าย ซึ่งหมายความว่าค่าของมันเดินทางกับทุกแพ็กเก็ตที่กล้องส่ง -- ไม่ใช่คุณสมบัติที่ความลับการลงนามต้องการ
10.9.2. การออก token¶
โทรศัพท์ต้องการวิธีที่จะ ได้รับ token ในตอนแรก JSON Web Token (JWT) อายุสั้น -- เพย์โหลด JSON ที่เข้ารหัส base64 พร้อมลายเซ็นต่อท้าย -- เพียงพอสำหรับการผ่านครั้งแรก: เจ้าของแลกรหัสผ่านกับ token จากนั้นใช้ token จนหมดอายุ:
import jwt
import time
@app.post('/api/login')
async def api_login(request):
creds = request.json
if creds.get('user') != 'owner' or creds.get('pass') != load_password():
abort(401)
token = jwt.encode({
'sub': 'owner',
'exp': int(time.time()) + 3600,
}, SECRET)
return {'token': token}
โทรศัพท์ส่ง POST {user, pass} ครั้งเดียว เก็บ token ที่ส่งคืนใน local storage และรวมมันเป็น Authorization: Bearer <token> ในทุกคำขอถัดไป
claim exp คือ Unix timestamp ตัวตรวจสอบในส่วนถัดไปอาศัย claim นี้เพื่อปฏิเสธ token ที่หมดอายุโดยอัตโนมัติ -- ซึ่งหมายความว่านาฬิกาแขวนของกล้องต้องตั้งค่า เพราะมิฉะนั้น token ทุกตัวจะดูเหมือนอยู่ในอนาคตไกลหรือหมดอายุแล้ว ดู เวลาและ NTP สำหรับสูตร NTP-sync
10.9.3. การยืนยัน token ด้วย TokenAuth¶
microdot.auth.TokenAuth คือ decorator factory สำหรับเส้นทางที่ต้องการ Authorization: Bearer <token> header คอลแบ็กตัวตรวจสอบรับสตริง token และส่งคืนออบเจกต์ identity ใดก็ตามที่ควรแนบกับคำขอ -- หรือ None เพื่อปฏิเสธ:
from microdot.auth import TokenAuth
tokens = TokenAuth()
@tokens.authenticate
async def check_token(request, token):
try:
claims = jwt.decode(token, SECRET)
except jwt.exceptions.PyJWTError:
return None
return claims['sub']
@app.post('/api/ack')
@tokens
async def ack(request):
body = request.json
if not body or 'count' not in body:
abort(400, 'missing count')
return {'ok': True, 'user': request.g.current_user}
jwt.decode() ยก jwt.exceptions.PyJWTError (หรือคลาสย่อย) เมื่อลายเซ็นผิด token มีรูปแบบไม่ถูกต้อง หรือ claim exp ผ่านไปแล้ว ตัวตรวจสอบกลืนทั้งหมดนั้นและส่งคืน None ดังนั้น microdot ตอบสนอง 401 โดยที่ handler ไม่เห็นอะไร
เมื่อตัวตรวจสอบส่งคืนค่าที่ไม่ใช่ None microdot จะจัดเก็บไว้ใน request.g.current_user เพื่อให้ handler อ่านได้ -- ในกรณีนี้คือ sub (subject) claim ของ JWT
10.9.4. BasicAuth สำหรับเครือข่ายปิด¶
สำหรับ admin endpoint ที่ถูกเข้าถึงจากเครือข่ายที่เชื่อถือได้เท่านั้น microdot.auth.BasicAuth ง่ายกว่า -- เบราว์เซอร์แสดงกล่องโต้ตอบชื่อผู้ใช้/รหัสผ่านของระบบเนทีฟและส่งมันใน Authorization: Basic ... header ในทุกคำขอ:
from microdot.auth import BasicAuth
basic = BasicAuth(realm='Backyard cam admin')
@basic.authenticate
async def check_basic(request, username, password):
if username == 'owner' and password == load_password():
return 'owner'
return None
@app.get('/admin')
@basic
async def admin(request):
return 'hi ' + request.g.current_user
ข้อมูลรับรองเดินทางในรูปแบบชัดเจนเว้นแต่การเชื่อมต่อเป็น HTTPS ดังนั้นวิธีนี้สมเหตุสมผลเฉพาะบนเครือข่ายที่เชื่อถือได้หรือมี HTTPS ในตำแหน่ง
/api/ack และ /api/login ตอนนี้เข้าใจ token /admin ต้องการการพิสูจน์ตัวตนแบบ Basic