Hướng dẫn lập trình Flask – Phần 5: Xử lý đăng nhập

flask_tutorial_1

Trong phần này, chúng ta sẽ tìm hiểu làm thế nào để tạo ra một hệ thống đăng nhập người dùng cho ứng dụng.

Để giúp cho bạn dễ theo dõi, sau đây là danh sách các bài viết trong loạt bài hướng dẫn này:

Bạn có thể truy cập mã nguồn cho phần này tại GitHub.

Trước đây, chúng ta đã tạo ra một form đăng nhập trong Phần 3 và tìm hiểu cách sử dụng cơ sở dữ liệu trong Phần 4. Tiếp theo, chúng ta sẽ phối hợp các kiến thức đã học trong hai phần trước để tạo ra một hệ thông đăng nhập người dùng đơn giản.

Mã băm cho mật mã của user (Password hashing)

Trong Phần 4, chúng ta đã định nghĩa trường password_hash trong mô hình dữ liệu người dùng. Trường này được dùng để chứa mã hash cho mật mã của user (Để bảo đảm tính nhất quán, từ đây chúng ta sẽ sử dụng thuật ngữ user thay vì người sử dụng). Mã hash này sẽ được dùng để xác thực mật mã do user nhập vào trong quá trình đăng nhập. Quá trình và những nguyên lý tạo ra hash rất phức tạp và chỉ có các chuyên gia về bảo mật mới hiểu rõ, vì thế chúng ta sẽ không đi sâu vào chi tiết ở đây. Tuy nhiên, chúng ta có thể sử dụng một số thư viện Flask để tạo ra hash một cách dễ dàng và nhanh chóng.

Trong các thư viện của Flask, có một gói cung cấp các phương thức để làm việc với hash tên là Werkzeug. Nếu để ý quá trình cài đặt Flask, bạn có thể thấy tên của gói này đâu đó trong các thông điệp mà Flask in ra trong suốt quá trình cài đặt bởi vì gói này là một trong những thành phần quan trọng nhất của Flask. Cũng vì lý do đó, Werkzeug đã được cài đặt sẵn vào trong môi trường ảo của bạn ngay từ đầu. Ví dụ sau sẽ minh họa cách tạo hash cho mật mã bằng  thư viện Werkzeug:

Trong ví dụ trên, chuỗi mật mã “foobar” ban đầu được chuyển đổi thành một chuỗi mã dài bằng các phương thức mã hóa không thể đảo ngược, nghĩa là bạn sẽ không thể chuyển đổi chuỗi mã này thành mật mã ban đầu được bằng bất kỳ cách nào. Quá trình này gọi là băm (hash). Và hơn thế nữa, nếu bạn tiếp tục hash một mật mã nhiều lần, bạn sẽ nhận được nhiều kết quả khác nhau. Vì vậy, bạn sẽ không thể nào xác định được là hai user có cùng mật mã hay không nếu chỉ nhìn vào giá trị hash từ mật mã của họ.

Để xác thực mật mã (verification), chúng ta sử dụng hàm thứ hai từ thư viện Werkzeug như trong ví dụ sau:

Quá trình xác thực sẽ nhận mật mã do user nhập vào, chuyển đổi thành mã hash và so sánh với mã hash được sinh ra từ mật mã ban đầu và đã được lưu lại trong cơ sở dữ liệu. Nếu mã hash từ mật mã nhập vào giống với mã hash đã được lưu trữ, hàm sẽ trả về True, ngược lại nó sẽ trả về False.

Quá trình tạo hash và xác thực mật mã sẽ được thêm vào hai phương thức mới trong mô hình user như sau:

app/models.py: Tạo mã hash và xác thực mật mã

Với hai phương thức này, một đối tượng user có thể xác thực mật mã của mình. Sau đây là một ví dụ về cách sử dụng hai phương thức này:

Giới thiệu về Flask-Login

Trong phần này, chúng ta cũng sẽ tìm hiểu thêm một thư viện Flask mở rộng rất phổ biến là Flask-Login.

Thư viện mở rộng này quản lý tình trạng đăng nhập của user. Nhờ đó, hệ thống có thể ghi nhớ các thông tin về user đã đăng nhập trong một phiên làm viện và cho phép họ truy nhập các trang Web đòi hỏi user phải đăng nhập. Nó cũng cung cấp chức năng “Remember me” để user vẫn giữ được tình trạng đăng nhập ngay cả khi họ đã đóng trình duyệt. Để bắt đầu phần này, chúng ta hãy cài đặt Flask-Login trong môi trường ảo của bạn:

Tương tự như các thư viện mở rộng khác, Flask-Login phải được khởi tạo ngay sau thực thể ứng dụng trong file app/__init__.py như sau:

app/__init__.py: Khởi tạo Flask-Login

Điều chỉnh mô hình dữ liệu user (User) cho Flask-Login

Thư viện Flask-Login sẽ làm việc với mô hình dữ liệu user với một số các thuộc tính và phương thức nhất định. Đây là một thiết kế hay bởi vì nếu các thuộc tính và phương thức này được cung cấp trong mô hình dữ liệu, Flask-Login sẽ làm việc đúng theo yêu cầu bất chấp các điều kiện khác. Ví dụ như nó có thể hoạt động với các mô hình dữ liệu user từ bất kỳ hệ cơ sở dữ liệu nào.

Sau đây là bốn yêu cầu của Flask-Login với mô hình dữ liệu user:

  • is_authenticated: một thuộc tính sẽ được gán là True nếu user có tên và mật mã hợp lệ, False nếu một trong hai không đúng.
  • is_active: một thuộc tính được gán là True nếu tài khoản user trong chế độ hoạt động (active) và False nếu ngược lại.
  • is_anonymous: một thuộc tính được gán là False cho những user bình thường, và True cho những user ẩn danh (anonymous)
  • get_id(): một phương thức để trả về định danh người dùng (id) dưới dạng chuỗi

Chúng ta có thể viết mã cho bốn yêu cầu này một cách dễ dàng. Nhưng bởi vì những yêu cầu này tương đối tổng quát, Flask-Login cung cấp một lớp mixin gọi là UserMixin (Nếu chưa hiểu về khái niệm mixin trong Python, bạn có thể tham khảo tại đây). Lớp này có sẵn các khai báo và mã thực thi có thể sử dụng được với phần lớn mô hình dữ liệu User. Sau đây là cách thêm lớp mixin vào mô hình dữ liệu:

app/models.py: lớp mixin cho user trong Flask-Login

Hàm tải thông tin user

Flask-Login theo dõi tình trạng của những user đã đăng nhập bằng cách lưu các ID tương ứng trong các phiên làm việc (user session) – một vùng lưu trữ được xác lập cho mỗi user đang kết nối vào ứng dụng. Mỗi khi một user đã đăng nhập truy cập một trang mới trong ứng dụng, Flask-Login sẽ lấy ID của user đó từ phiên làm việc và tải dữ liệu về user đó vào bộ nhớ.

Bởi vì Flask-Login không trực tiếp làm việc với cơ sở dữ liệu, nó cần có sự trợ giúp của các thành phần khác trong ứng dụng để tìm kiếm và tải dữ liệu về user. Vì vậy, thư viện này sẽ cần có một hàm hỗ trợ để tải thông tin user. Hàm này sẽ tìm kiếm và tải các thông tin về user từ cơ sở dữ liệu dựa trên Id của user đó. Chúng ta sẽ thêm mã cho hàm này vào module app/models.py như sau:

app/models.py: Hàm tải dữ liệu người dùng cho Flask-Login

Chúng ta sẽ đăng ký hàm này với Flask-Login qua decorator @login.user_loader. Tham số id sẽ được truyền vào hàm này dưới dạng chuỗi (String), vì vậy nếu cơ sở dữ liệu định dạng Id kiểu số nguyên (integer), chúng ta sẽ cần chuyển đổi Id thành số nguyên như trong ví dụ ở trên.

Đăng nhập người dùng

Sau các bước chuẩn bị ở trên, chúng ta có thể trở lại với hàm hiển thị đăng nhập mà chúng ta đã tạo ra trong Phần 3. Lưu ý là tại thời điểm này, hàm này chưa có chức năng đăng nhập thật sự mà chỉ đưa ra một thông báo nhờ hàm flash() nếu user nhập vào đầy đủ tên người dùng và mật mã (với bất kỳ giá trị nào). Hiện giờ, sau khi đã có các cơ sở dữ liệu và biết cách để tạo ra hash cho mật mã, chúng ta có thể làm cho nó thật sự hoạt động.

app/routes.py: Hàm hiển thị đăng nhập

Trong hai dòng lệnh đầu tiên của hàm login(), chúng ta sẽ giải quyết tình huống khi có một user đã thành công đăng nhập nhưng lại truy nhập vào trang /login vì lý do nào đó. Dù về mặt lập trình, điều này không có gì sai, nhưng đây là một điều không hay cho user (theo nguyên tắc thiết kế ứng dụng, để có UX – User eXperience hay trải nghiệm của người sử dụng – tốt thì user không nên thấy các giao diện hay chức năng mà họ không nên hoặc không cần sử dụng). Vì vậy, trong trường hợp này, chúng ta sẽ dùng giải pháp là chuyển hướng user của chúng ta trở lại trang chủ (với URL /index) nếu đây là một user có đăng ký và được hệ thống xác nhận (đăng nhập thành công hay theo định nghĩa của thư viện Flask-Login là authenticated). Để làm được điều này, chúng ta cần sự trợ giúp của biến current_user có sẵn trong Flask-Login. Nếu user đã đăng nhập, biến này sẽ chứa các thông tin về user từ cơ sở dữ liệu (việc tải thông tin của user từ cơ sở dữ liệu được thực hiện qua hàm load_user() mà chúng ta đã tạo ra ở trên). Trong trường hợp user chưa đăng nhập, biến này sẽ chứa giá trị đại diện cho user ẩn danh (anonymous). Ngoài ra, biến này còn có thuộc tính is_authenticated rất hữu ích để giúp xác định một user có đăng nhập hay không. Đoạn mã của chúng ta sẽ kiểm tra thuộc tính này, và nếu nó trả về True – đồng nghĩa đây là một user đã đăng nhập thành công – thì chúng ta sẽ chuyển hướng đến và hiển thị trang chủ thay vì hiển thị trang đăng nhập.

Trước đây, chúng ta cũng dùng hàm flash() để hiển thị một thông báo là người sử dụng đăng nhập thành công mà không thực sự tiến hành quá trình đăng nhập. Nhưng lần này thì chúng ta có thể thay thế mã giả này bằng mã thật cho quá trình đăng nhập. Để làm điều này, trước hết, chúng ta cần tải dữ liệu về user từ cơ sở dữ liệu. Thông qua form đăng nhập, chúng ta sẽ nhận được username. Giá trị này sẽ được dùng để tìm Id tương ứng của user này trong cơ sở dữ liệu bằng hàm filter_by() của đối tượng query (truy vấn) trong thư viện SQLAlchemy. Hàm filter_by sẽ trả về các đối tượng có giá trị username khớp với username mà chúng ta đã sử dụng. Bởi vì chúng ta biết rằng các username mang tính duy nhất nên chỉ có tối đa một đối tượng user trong cơ sở dữ liệu có username trùng với username mà chúng ta đang tìm. Do đó, chúng ta có thể sử dụng hàm first(), hàm này sẽ trả về một kết quả nếu có một user như vậy hoặc None nếu user không tồn tại trong cơ sở dữ liệu. Trong Phần 4, chúng ta đã thấy kết quả khi gọi hàm all() trong một truy vấn: nó sẽ trả về tất cả kết quả trùng hợp với truy vấn đó trong cơ sở dữ liệu. Hàm first() thường được sử dụng trong trường hợp chúng ta biết trước chỉ có thể có tối đa một kết quả.

Nếu chúng ta tìm được một user từ cơ sở dữ liệu có cùng username như trong form đăng nhập, chúng ta sẽ tiến hành bước tiếp theo là kiểm tra mật mã. Quá trình này được thực hiện bằng cách sử dụng hàm check_password() mà chúng ta đã nói ở phần đầu của bài này. Hàm này sẽ tạo hash từ mật mã do user nhập vào và so sánh với giá trị password_hash từ bảng User. Nếu hai giá trị này giống nhau thì mật mã do user nhập vào là đúng (valid), ngược lại thì mật mã này là sai (invalid). Như vậy, cuối cùng chúng ta có hai khả năng người dùng không được cho phép đăng nhập: username hoặc password nhập vào là không đúng. Trong cả hai trường hợp, chúng ta sẽ dùng flash() để hiển thị một thông báo lỗi và chuyển hướng về trang đăng nhập để user có thể nhập các thông tin lần nữa.

Nếu username và password tương ứng đều đúng, chúng ta sẽ gọi một hàm trong thư viện Flask-Login là login_user() để ghi nhận tình trạng của user này là đã đăng nhập thành công. Điều này cũng đồng nghĩa với việc ứng dụng sẽ tải và lưu giữ các thông tin về user từ database vào bộ nhớ và giữ lại các thông tin này trong suốt phiên làm việc của user (cho đến khi user đăng xuất – logout – ra khỏi ứng dụng). Nhờ đó, khi user truy cập bất kỳ trang nào, ứng dụng cũng có thể dễ dàng tìm được thông tin về họ qua biến current_user.

Sau khi user đăng nhập thành công, chúng ta sẽ chuyển (redirect) user đến trang chủ.

Đăng xuất (Log Out)

Để hoàn tất quá trình xuất nhập, chúng ta cũng phải cung cấp chức năng đăng xuất (log out) ra khỏi ứng dụng. Chúng ta sẽ sử dụng một hàm khác của thư viện Flask-Login gọi là logout_user() cho mục đích này:

app/routes.py: Hàm hiển thị Logout

Chúng ta cũng cần thay link Login trong thay định hướng bằng link Logout sau khi user đăng nhập thành công. Chúng ta sẽ làm điều này bằng cách cập nhật file base.html:

app/templates/base.html: Hiển thị liên kết login và logout trên thanh định hướng

Như đã nói ở trên, thuộc tính is_anonymous được thư viện Flask-Login cung cấp qua lớp UserMixin. Biểu thức current_user.is_anonymous sẽ có giá trị True khi user chưa đăng nhập.

Yêu cầu User đăng nhập

Flask-Login có một chức năng rất hữu ích để yêu cầu người sử dụng đăng nhập nếu họ muốn truy nhập vào một số trang nhất định trong ứng dụng. Nếu một user không đăng nhập tìm cách truy cập các trang này, Flask-Login sẽ tự động chuyển họ đến trang đăng nhập và chuyển trở lại trang được yêu cầu truy cập sau khi user đã đăng nhập thành công.

Để sử dụng chức năng này, Flask-Login cần biết hàm hiển thị cho quá trình đăng nhập. Chúng ta sẽ thay đổi file app/__init__.py để làm điều này:

Giá trị ‘login‘ ở trên là tên của hàm hiển thị đăng nhập. Hay nói cách khác, đó là tên bạn sẽ dùng khi gọi hành url_for() để lấy giá trị của URL.

Chúng ta sẽ dùng decorator @login_required cho những hàm hiển thị nào chỉ cho phép user có đăng nhập truy cập. Khi bạn thêm decorator này vào một hàm hiển thị và dưới decorator @app.route, hàm này sẽ được bảo vệ và không cho phép những user không đăng nhập truy cập. Sau đây là ví dụ với hàm hiển thị cho trang chủ của ứng dụng:

app/routes.py: decorator @login_required

Chúng ta cần làm một việc nữa là thêm tính năng trở về trang mà user yêu cầu truy cập sau khi đã đăng nhập thành công. Khi một user chưa đăng nhập truy cập vào một trang được hiển thị bởi một hàm hiển thị đã được bảo vệ với decorator @login_required, decorator này sẽ chuyển hướng user đó đến trang đăng nhập, nhưng nó sẽ thêm vào một vài thông số để ứng dụng biết rằng cần phải trở lại trang này sau khi user đăng nhập thành công. Ví dụ như khi user muốn truy cập trang /index khi chưa đăng nhập, decorator @login_required sẽ kiểm tra yêu cầu này và chuyển hướng user đến /login, nhưng nó sẽ thêm một tham số vào URL của trang /login như sau: /login?next=/index. Tham số next trong URL này sẽ được gán giá trị của URL được yêu cầu ban đầu, nhờ đó ứng dụng biết sẽ cần phải trở lại trang /index sau khi user đăng nhập thành công.

Sau đây là đoạn mã để đọc và xử lý tham số next:

app/routes.py: Chuyển hướng đến trang tại “next”

Ngay sau khi hàm login_user() trả về kết quả là user đăng nhập thành công, chúng ta sẽ lấy giá trị của tham số next trong URL. Flask cung cấp biến request với tất cả thông tin được user nhập vào và gởi qua trình duyệt. Cụ thể là thuộc tính request.args trả về tất cả các tham số được thêm vào URL trong một dictionary. Có ba khả năng cần được xem xét để quyết định sẽ chuyển hướng đến vị trí nào sau khi user đăng nhập thành công:

  • Nếu URL đăng nhập không có tham số next, user sẽ được chuyển hướng đến trang chủ (/index) theo mặc định.
  • Nếu URL đăng nhập có tham số next được gán với giá trị là một đường dẫn tương đối (hay nói cách khác là một URL không có phần domain -tên miền), user sẽ được chuyển hướng đến URL đó.
  • Nếu URL đăng nhập có tham số next được gán với giá trị là một URL đầy đủ với cả tên miền, user sẽ được chuyển hướng đến trang chủ của ứng dụng.

Trường hợp đầu và trường hợp thứ hai tương đối dễ hiểu. Trường hợp thứ ba nhằm bảo đảm cho ứng dụng được bảo mật hơn. Một người có ác ý có thể đặt một URL dẫn đến một trang Web nguy hiểm vào trong tham số next, vì vậy, ứng dụng chỉ chuyển hướng khi gặp đường dẫn tương đối, nhờ đó bảo đảm là mọi trang Web được chuyển đến đều nằm trong cùng site với ứng dụng. Để phân biệt các URL tương đối và đầy đủ, chúng ta sử dụng hàm url_parse() từ thư viện Werkzeug và kiểm tra xem thành phần netloc có được gán hay không.

Hiển thị user đã đăng nhập trong các template

Bạn còn nhớ trong Phần 2 chúng ta đã tạo ra một user giả để mô phỏng hệ thống đăng nhập user khi thiết kế trang chủ khi chúng ta còn chưa có hệ thống này hay không? Bây giờ chúng ta đã có một hệ thống đăng nhập hoàn chỉnh với các user thật, vì vậy, chúng ta có thể bỏ user giả đó và làm việc với các user thật. Thay vì user giả, chúng ta có thể dùng biến current_user trong các template:

app/templates/index.html: Truyền thông tin về user đang đăng nhập vào template

Chúng ta cũng có thể loại bỏ tham số user trong các hàm hiển thị:

app/routes.py: Không truyền tham số user đến các template nữa

Đây là lúc để kiểm tra xem chức năng đăng nhập và đăng xuất làm việc thế nào. Bởi vì chúng ta còn chưa có chức năng đăng ký user, cách duy nhất để thêm user vào cơ sở dữ liệu là dùng chế độ dòng lệnh của Python. Chúng ta hãy bắt đầu bằng cách gọi flask shell và dùng các lệnh sau đây để đăng ký một user với hệ thống:

Bây giờ, nếu bạn chạy ứng dụng và truy cập địa chỉ http://localhost:5000/ or http://localhost:5000/index, ứng dụng sẽ lập tức chuyển hướng bạn đến trang đăng nhập. Và sau khi đã nhập vào các thông tin về user mà bạn vừa thêm vào trong cơ sở dữ liệu, bạn sẽ được chuyển hướng đến trang mà bạn yêu cầu và một lời chào mừng sẽ được hiển thị.

Đăng ký user

Chức năng cuối mà chúng ta sẽ xây dựng trong bài hôm nay là form đăng ký để user mới có thể đăng ký và đăng nhập vào ứng dụng. Chúng ta sẽ cần tạo ra một lớp web form mới trong app/forms.py:

app/forms.py: form đăng ký user

Có vài điểm đáng lưu ý liên quan đến việc kiểm tra dữ liệu nhập trong form mới này. Đầu tiên, chúng ta thêm một biến kiểm tra dữ liệu (validator) thứ hai ngoài DataRequired vào trường email. BIến này gọi là Email. Đây là một biến kiểm tra dữ liệu có sẵn trong WTForms để bảo đảm rằng địa chỉ email do user nhập vào phù hợp với định dạng của địa chỉ email.

Tiếp theo, chúng ta cũng tuân theo quy ước chung của các form đăng ký là yêu cầu user nhập mật mã hai lần để tránh tình trạng nhập sai mật mã ngoài ý muốn. Vì vậy, chúng ta dùng hai trường passwordpassword2. Trường password2 cũng sử dụng một biến kiểm tra có sẵn gọi là EqualTo để bảo đảm rằng giá trị nhập vào trường này phải giống như giá trị của trường password đã nhập trước đó.

Và cuối cùng, chúng ta cũng thêm hai hàm mới gọi là validate_username()validate_email(). Khi chúng ta thêm bất kỳ hàm nào với tên gọi theo mẫu validate_, WTForms sẽ hiểu những hàm này như là những biến kiểm tra dữ liệu tùy biến (custom validator) và sẽ sử dụng chúng cho các trường tương ứng sau khi đã dùng các biến kiểm tra dữ liệu sẵn có. Trong trường hợp này, chúng ta dùng các hàm kiểm tra dữ liệu này để chắc chắn rằng các giá trị username và email đã được user nhập vào không tồn tại trong cơ sở dữ liệu, vì thế các truy vấn dữ liệu phải không tìm được dữ liệu trong bảng User căn cứ trên các giá trị này. Nếu có kết quả trả về, các hàm này sẽ đưa ra ngoại lệ ValidationError. Tham số cho các ngoại lệ Validation là các thông báo lỗi tương ứng và sẽ được hiển thị bên cạnh các trường này khi các dữ liệu nhập vào không đúng.

Để hiển thị form này, chúng ta cần xây dựng một template HTML. Chúng ta sẽ đưa template này vào file app/templates/register.html. Chúng ta cũng viết mã cho template này tương tự như cho form đăng nhập:

app/templates/register.html: Template cho form đăng ký user

Chúng ta cũng cần thêm liên kết từ trang login đến trang đăng ký để các user mới có thể đăng ký với hệ thống như sau:

app/templates/login.html: Liên kết đến trang đăng ký

Và cuối cùng, chúng ta cần viết hàm hiển thị cho trang đăng ký trong app/routes.py:

app/routes.py: Hàm hiển thị cho form đăng ký user

Trong hàm này, chúng ta làm một số việc theo tuần tự như sau: bảo đảm rằng các user đã đăng ký không thể truy cập trang này (cách xử lý tương tự như khi khi một user đã đăng nhập tìm cách truy cập trang đăng nhập). Tiếp theo, chúng ta sẽ kiểm tra các dữ liệu đăng ký có hợp lệ hay không, nếu không có lỗi (được xác nhận qua điều kiện if_validate_on_submit()), chúng ta sẽ tạo một user mới với các giá trị của username, email và password do user đã nhập vào, lưu vào cơ sở dữ liệu và chuyển hướng đến trang đăng nhập để user có thể đăng nhập.

Error when password and password2 are not the same

Với các thay đổi này, user sẽ có thể tạo tài khoản người sử dụng, đăng nhập và đăng xuất. Hãy thử tất cả các tình huống có lỗi để hiểu cách làm việc của mã kiểm tra lỗi mà chúng ta đã thêm vào chương trình. Chúng ta sẽ trở lại với hệ thống xác nhận người dùng và thêm chức năng để user có thể reset lại mật mã trong trường hợp họ quên trong một bài viết sau. Nhưng bây giờ, chúng ta hãy chuyển sang phần khác của ứng dụng.

Chúng ta sẽ kết thúc phần này ở đây. Hẹn gặp bạn trong phần tiếp theo.

Leave a Reply

Your email address will not be published. Required fields are marked *