Xác thực người dùng với JWT trong Python

Giới thiệu về Xác thực JWT

Quy trình xác thực truyền thống:

  1. Khi người dùng đăng nhập lần đầu tiên, hệ thống tạo một session và gửi trả về phía khách hàng, đồng thời lưu trữ session này cùng khóa chính của người dùng trên máy chủ (có thể là cơ sở dữ liệu hoặc bộ nhớ cache).
  2. Lần truy cập tiếp theo yêu cầu đăng nhập, session sẽ được gửi kèm theo.
  3. Máy chủ sử dụng session để kiểm tra trong cơ sở dữ liệu hoặc cache xem session có tồn tại hay không. Nếu tồn tại, xác thực thành công; nếu không, thất bại.

Nhược điểm của phương pháp xác thực truyền thống:

  1. Lưu trữ session ở phía máy chủ làm tăng chi phí lưu trữ và truy xuất.
  2. Trong trường hợp có nhiều máy chủ, việc đồng bộ session giữa các máy chủ trở nên phức tạp.

Quy trình xác thực JWT (giải quyết nhược điểm của phương pháp truyền thống):

  1. Khi người dùng đăng nhập lần đầu tiên, hệ thống tạo ra một token nhưng không lưu trữ token này ở phía máy chủ.
  2. Lần truy cập tiếp theo yêu cầu đăng nhập, token sẽ được gửi kèm theo.
  3. Máy chủ phân tích và kiểm tra token. Nếu phân tích thành công, xác thực thành công; nếu không, thất bại.

Nguyên lý mã hóa JWT:

Token JWT bao gồm ba phần: HEADER.PAYLOAD.SIGNATURE, tất cả đều được mã hóa bằng thuật toán base64 và kết nối bằng dấu chấm (.). Ví dụ:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

1. HEADER

Đại diện cho thuật toán mã hóa và loại token. Mặc định như sau:

{
    "alg": "HS256",
    "typ": "JWT"
}

Sau khi mã hóa: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.

2. PAYLOAD

Chứa thông tin nghiệp vụ cần truyền và thời gian hết hạn (tùy chọn), ví dụ:

{
    "sub": "1234567890",
    "name": "John Doe",
    "iat": 1516239022,
    "exp": 451154141
}

Sau khi mã hóa: eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.

3. SIGNATURE

Là phần quan trọng nhất của JWT. Quy tắc tạo SIGNATURE là ghép chuỗi từ hai phần đã mã hóa trước đó cùng với chuỗi muối tùy chỉnh, sau đó mã hóa bằng thuật toán không thể đảo ngược (trong trường hợp này là HS256) được chỉ định trong HEADER.

Muối (salt): Là chuỗi tùy chỉnh thêm vào quá trình mã hóa, thường là một chuỗi ngẫu nhiên hoặc phức tạp để đảm bảo tính độc nhất của chuỗi mã hóa. Trong Django, bạn có thể sử dụng SECRET_KEY từ settings.

Sau khi mã hóa: SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c.

Nguyên lý giải mã JWT:

Khi nhận được token từ yêu cầu, quy trình giải mã như sau:

  1. Chia token thành ba phần theo dấu chấm.
  2. Giải mã từng phần bằng thuật toán base64 để lấy thuật toán mã hóa, dữ liệu nghiệp vụ và thời gian hết hạn.
  3. Ghép hai phần đầu với chuỗi muối và sử dụng thuật toán trong HEADER để mã hóa lại.
  4. So sánh kết quả mã hóa với phần thứ ba của token. Nếu khớp, xác thực thành công; nếu không, token đã bị thay đổi và xác thực thất bại.

Tại sao JWT lại có ba phần?

JWT dựa trên ý tưởng mã hóa dữ liệu nghiệp vụ bằng thuật toán không thể đảo ngược và lưu trữ trong token. Vậy tại sao không đơn giản là mã hóa trực tiếp dữ liệu nghiệp vụ với muối và gửi đi? Lý do là vì thuật toán không thể đảo ngược khiến chúng ta không thể lấy lại dữ liệu gốc. Do đó, cần thêm một phần để lưu trữ dữ liệu nghiệp vụ rõ ràng. Phần thứ ba giúp người dùng tự xác định thuật toán mã hóa, tăng tính linh hoạt.

Sử dụng JWT trong Python

pyjwt

Pyjwt là thư viện nền tảng để sử dụng JWT trong Python. Bạn có thể cài đặt nó bằng lệnh: pip install pyjwt. Thư viện này đã triển khai sẵn logic mã hóa và giải mã, chúng ta chỉ cần cung cấp thuật toán mã hóa, dữ liệu nghiệp vụ và chuỗi muối.

Sử dụng pyjwt trong rest_framework

Định nghĩa hai API: Đăng nhập (login) và Xem đơn hàng (order). Chỉ những người dùng đã đăng nhập mới có thể truy cập API Xem đơn hàng. Chúng ta có thể trả về token JWT khi đăng nhập thành công và kiểm tra token trong API Xem đơn hàng.

1. Cấu hình urls.py

from django.urls import path
from users import views

urlpatterns = [
    path('login/', views.LoginView.as_view()),
    path('order/', views.OrderView.as_view()),
]

2. Tạo view cho Đăng nhập và Xem đơn hàng

  1. Sau khi đăng nhập thành công, gọi hàm generate_token() để tạo token, truyền vào thông tin người dùng và thời gian hết hạn (đơn vị: phút), mặc định là 1 phút.
  2. Trong view Xem đơn hàng, cấu hình lớp xác thực CustomJWTAuthentication.
  3. Các hàm generate_tokenCustomJWTAuthentication được định nghĩa trong utils/JWTAuth.py.
from rest_framework.views import APIView
from rest_framework.response import Response
from utils.JWTAuth import generate_token, CustomJWTAuthentication

class LoginView(APIView):
    def post(self, request, *args, **kwargs):
        # Lấy tên người dùng và mật khẩu
        username = request.data.get('username')
        password = request.data.get('password')
        
        try:
            user = models.User.objects.filter(username=username, password=password).first()
        except Exception:
            return Response({'status': 1, 'message': 'Tên người dùng hoặc mật khẩu không đúng!'})
        
        if not user:
            return Response({'status': 1, 'message': 'Tên người dùng hoặc mật khẩu không đúng!'})
        
        # Tạo token
        token = generate_token({'user_id': user.id, 'username': user.username}, timeout=1)
        
        # Trả về phản hồi thành công
        return Response({'status': 0, 'token': token})

class OrderView(APIView):
    authentication_classes = [CustomJWTAuthentication, ]

    def get(self, request):
        return Response({'status': 0, 'message': 'Thành công'})

3. Định nghĩa JWTAuth.py

import jwt
from jwt import exceptions as JWTException
from django.conf import settings
import datetime
from rest_framework.authentication import BaseAuthentication
from rest_framework.exceptions import AuthenticationFailed

def generate_token(payload, timeout=1):
    payload['exp'] = datetime.datetime.utcnow() + datetime.timedelta(minutes=timeout)
    secret_key = settings.SECRET_KEY
    token = jwt.encode(payload=payload, key=secret_key, algorithm='HS256')
    return token.decode('utf-8')  # Chuyển bytes sang string

class CustomJWTAuthentication(BaseAuthentication):
    def authenticate(self, request):
        token = request.query_params.get('token')
        secret_key = settings.SECRET_KEY
        
        try:
            payload = jwt.decode(token, secret_key, algorithms=['HS256'])
        except JWTException.ExpiredSignatureError:
            raise AuthenticationFailed('Token đã hết hạn')
        except jwt.DecodeError:
            raise AuthenticationFailed('Token không hợp lệ')
        except jwt.InvalidTokenError:
            raise AuthenticationFailed('Token không hợp lệ')

        return (payload.get('username'), None)

Lưu ý: Thời gian hết hạn phải được thiết lập trong phần PAYLOAD với khóa cố định là 'exp', giá trị là datetime.datetime.utcnow() + datetime.timedelta(...).

Thẻ: JWT PyJWT DjangoRESTFramework

Đăng vào ngày 20 tháng 6 lúc 06:30