10.9. 프로그래밍 방식 클라이언트를 위한 인증¶
베어러 토큰은 휴대폰, 동반 마이크로컨트롤러, 클라우드 워커가 서버에 인증하는 방식입니다. 클라이언트는 모든 요청의 Authorization 헤더에 토큰을 넣고 서버는 이를 확인합니다. 동반 휴대폰 앱은 소유자가 동작 알림에서 “확인함”을 탭하면 “ack”를 POST하는데, 그런 요청에는 토큰이 필요합니다.
10.9.1. 서명 비밀키¶
비밀키는 토큰에 서명하고 검증하기 위한 카메라의 개인 키입니다. 카메라의 첫 부팅 시 하드웨어 RNG에서 32개의 랜덤 바이트를 생성해 파일 시스템에 영구 저장하고, 이후의 모든 부팅에서 같은 바이트를 재사용하십시오:
# 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() 은 각 카메라 포트에서 암호학적으로 적합한 랜덤 소스입니다. 대부분의 포트에서는 칩의 하드웨어 난수 생성기를 직접 읽습니다. 이 파일은 카메라의 작업 디렉터리(SD 카드가 마운트되어 있으면 /sdcard, 그렇지 않으면 /flash)에 위치하며 절대 카메라를 떠나지 않습니다. secret.bin 을 삭제하고 재부팅하면 비밀키가 교체되고, 기존 비밀키로 발급된 모든 토큰이 무효화됩니다.
참고
machine.unique_id() 는 대체재가 아닙니다. 일부 카메라 포트에서는 이것이 네트워크 MAC 주소를 유도하는 데에도 사용되며, 이는 그 값이 카메라가 보내는 모든 패킷과 함께 이동한다는 뜻입니다 – 서명 비밀키에 필요한 속성이 아닙니다.
10.9.2. 토큰 발급¶
휴대폰은 우선 토큰을 얻을 방법이 필요합니다. 수명이 짧은 JSON Web Token(JWT) – 서명이 덧붙은 base64 인코딩 JSON 페이로드 – 만으로도 첫 단계로는 충분합니다: 소유자가 비밀번호를 토큰으로 교환한 다음, 토큰이 만료될 때까지 그 토큰을 사용합니다:
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}
휴대폰은 {user, pass} 를 한 번 POST하고, 반환된 토큰을 로컬 저장소에 보관하며, 이후의 모든 요청에 Authorization: Bearer <token> 으로 포함시킵니다.
exp 클레임은 Unix 타임스탬프입니다. 다음 섹션의 검증기는 만료된 토큰을 자동으로 거부하기 위해 이 클레임에 의존합니다 – 즉 카메라의 벽시계가 설정되어 있어야 하는데, 그렇지 않으면 모든 토큰이 먼 미래에 있거나 이미 만료된 것처럼 보이기 때문입니다. NTP 동기화 방법은 시간과 NTP 를 참조하십시오.
10.9.3. TokenAuth로 토큰 검증하기¶
microdot.auth.TokenAuth 는 Authorization: Bearer <token> 헤더를 요구하는 라우트를 위한 데코레이터 팩토리입니다. 검증기 콜백은 토큰 문자열을 받아 요청에 첨부될 신원 객체를 반환하거나, 거부하려면 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() 는 서명이 잘못되었거나, 토큰의 형식이 올바르지 않거나, exp 클레임이 지났을 때 jwt.exceptions.PyJWTError (또는 그 하위 클래스)를 발생시킵니다. 검증기는 이들을 모두 삼키고 None 을 반환하므로, 핸들러가 아무것도 보지 못한 채 microdot가 401로 응답합니다.
검증기가 None이 아닌 값을 반환하면 microdot는 그것을 request.g.current_user에 저장하여 핸들러가 읽을 수 있게 합니다 – 이 경우에는 JWT의 sub(subject) 클레임입니다.
10.9.4. 폐쇄망을 위한 BasicAuth¶
신뢰된 네트워크에서만 접근되는 관리자 엔드포인트의 경우, microdot.auth.BasicAuth 가 더 간단합니다 – 브라우저가 네이티브 사용자 이름/비밀번호 대화상자를 띄우고 모든 요청의 Authorization: Basic ... 헤더에 이를 보냅니다:
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 은 토큰을 이해합니다. /admin 은 Basic 인증을 요구합니다.