Thứ Tư, 4 tháng 1, 2017

[Series] Hướng dẫn sử dụng ASP.NET Identity (phần 1)

ASP.NET Identity là 1 công nghệ ra đời sau ASP.NET Membership vốn đã khá quen thuộc với các tín đồ của ASP.NET trong bài toán xác thực và phân quyền người dùng trên website. ASP.NET Identity khắc phục một số yếu điểm của ASP.NET Membership và bổ sung thêm nhiều tính năng hấp dẫn để bắt kịp xu thế như hỗ trợ đăng nhập qua mạng xã hội, đăng nhập thông qua 2 bước, gửi mail xác nhận khi đăng ký ... Series hướng dẫn này sẽ giúp bạn làm việc với công nghệ này trong nền tảng ASP.NET MVC 5, giúp bạn không chỉ làm việc thuần thục với nó mà còn hiểu rõ cơ chế hoạt động của nó. Mình sẽ cố gắng hoàn thành chuỗi Series này sớm nhất có thể.  Trong quá trình viết bài mình không tránh được sai sót. Mọi thắc mắc, góp ý các bạn comment dưới bài viết hộ mình nhé.

Nội dung chính

1. Tạo project sử dụng ASP.NET Identity

Lưu ý: Vì ASP.NET Identity chỉ hỗ trợ từ ASP.NET MVC5 trở lên nên các bạn phải cài Visual Studio 2013 trở lên nhé. Series này mình sử dụng Visual Studio 2015
Đây là các bước để tạo 1 project có tích hợp sẵn ASP.NET Identity:
Bước 1: Đầu tiên các bạn mở Visual studio lên và chọn New Project -> Templates -> Visual C# -> Web -> ASP.NET Web Application sau đó các bạn chọn nơi chứa Project (Location), nhập tên Project và tên Solution tùy ý và nhấn OK



Bước 2: Khung New ASP.NET Project hiện ra, các bạn chọn MVC và check vào checkbox MVC.


Bước 3: Ấn nút Change Authentication và chọn Individual User Account để Visual Studio tích hợp sẵn code của ASP.NET Identity trong project ASP.NET MVC được tạo. Sau đó ấn OK để thực hiện tạo project


2. Giới thiệu về cấu trúc tổ chức thư mục của ASP.NET Identity


Khi project được tạo xong các bạn sẽ thấy 1 số files được tạo sẵn trong project như sau:
  • App_Start/IdentityConfig.cs: chứa các lệnh để cấu hình ASP.NET Identity
  • Controller/AccountController: controller chứa các action method có tác dụng xác thực người dùng như Login, Register, ForgotPassword, ...
  • Controller/ManageController: controller chứa các action method có tác dụng quản lý user (khi user đã login vào web) như ChangePassword, SetPassword, ...
  • Model/AccountViewModels: chứa các View Model hiển thị trong các view của AccountController
  • Model/ManageViewModels: chứa các View Model hiển thị trong các view của ManageController
  • Model/IdentityModels: chứa class ApplicationUser để quản lý thông tin user và class ApplicationDbContext để quản lý kết nối với database ở dạng Entity Framework Code First (các bạn nên có kiến thức căn bản về Entity Framework Code First) để có thể bổ sung thêm các field cho user hoặc loại bỏ bớt các field mà bạn không cần thiết 1 cách dễ dàng và ít bỡ ngỡ.

3. Chức năng đăng ký thành viên (Register)

Ok chúng ta đã hiểu khái quát về cấu trúc tổ chức của ASP.NET Identity trong 1 project ASP.NET MVC 5, cách tổ chức này mình nghĩ cũng tương tự ở ASP.NET WebForm. Bây giờ chúng ta thử đăng ký 1 tài khoản xem sao, nhưng trước khi đăng ký thì các bạn phải cấu hình chuỗi kết nối (connectionString) trong file Web.config ở thư mục gốc của project nhé. Bởi vì khi đăng ký thành viên thành công thì Visual Studio sẽ tự tạo mới 1 database (nếu database chưa tồn tại) chứa các table của ASP.NET Identity hoặc add thêm các table của ASP.NET Identity vào database (nếu database đó đã tồn tại) trong SQL Server của bạn. Tùy theo server SQL Server của các bạn là gì và tên database các bạn là gì thì các bạn sửa cho đúng trong file Web.config nhé. Ở đây mình muốn kết nối đến server .\sqlexpress với database là TestIdentity (database này mình chưa tạo và mình muốn  Visual Studio tự tạo khi mình sử dụng chức năng đăng ký thành viên thành công).
Đây là đoạn connectionStrings của mình:



Bây giờ chúng ta tiến hành thử dùng chức năng đăng ký thành viên (register) trong ASP.NET Identity nào
Bước 1: Chạy website và click vào link Register ở góc phải menu. Giao diện mặc định của trang Register hiện ra như hình dưới. Các bạn có thể tùy chỉnh giao diện theo ý muốn nhưng tránh sai sót ở code của Form trong View nhé


Bước 2: Nhập thử thông tin vào. Ở đây mình cố tính nhập Email không hợp lệ và nhấn nút Register luôn thì thấy thông báo lỗi: "The Email field is not a valid e-mail address" hoặc khi mình không nhập đủ thông tin thì trang cũng hiển thị lỗi. Như vậy các bạn có thể thấy rằng ASP.NET Identity mặc định đã thiết lập 1 số nguyên tắc (rule) khi kiểm tra dữ liệu Form trước khi submit phải không nào. Chúng ta có thể tùy chỉnh thông báo lỗi bằng tiếng Việt nếu muốn. Test bắt lỗi như vậy là đủ rồi bây giờ mình sẽ nhập thông tin register như sau:
Email: tuannguyen@gmail.com
Password: 123456
Confirm password: 123456
Sau đó mình ấn Register thì nhận được thông báo như hình bên dưới:


Ô HAY vẫn chưa thành công sao 😮. OK các bạn không cần lo bởi vì ASP.NET Identity mặc định thiết lập ràng buộc password rất "chặt chẽ". Nó yêu cầu user phải nhập password tối thiểu 6 ký tự bao gồm cả ký tự hoa, ký tự thường, ký tự số và cả ký tự đặc biệt . Đoạn code cấu hình password được tìm thấy ở dòng thứ 54 trong file IdentityConfig.cs
Đây là nội dung đoạn lệnh cấu hình ràng buộc password:
 // Configure validation logic for passwords
            manager.PasswordValidator = new PasswordValidator
            {
                RequiredLength = 6,
                RequireNonLetterOrDigit = true,
                RequireDigit = true,
                RequireLowercase = true,
                RequireUppercase = true,
            };

Mình giải thich 1 số thuộc tính bạn cần biết:
  • RequiredLength: độ dài tối thiểu của password
  • RequireNonLetterOrDigit:  bắt buộc password chứa ký tự đặc biệt hoặc ký tự số
  • RequireDigit: bắt buộc chứa ký tự số
  • RequireLowercase: bắt buộc chứa ký tự in thường
  • RequireUppercase: bắt buộc chứa ký tự in HOA
Các bạn có thể tùy ý cấu hình theo ý muốn nhé. Ở đây mình chỉ muốn đặt ràng buộc cho mật khẩu là tối thiểu 6 ký tự bao gồm cả ký tự số thì đoạn code mình sẽ sửa lại như sau:
  // Configure validation logic for passwords
            manager.PasswordValidator = new PasswordValidator
            {
                RequiredLength = 6,
                RequireNonLetterOrDigit = false
                RequireDigit = true,
                RequireLowercase = false,
                RequireUppercase = false,
            };
Sau khi sửa xong mình tiến hành build lại project và load lại trang register. Sau đó nhập thông tin như sau:
Email: tuannguyen@gmail.com
Password: iloveyouok123
Confirm password: iloveyouok123
Sau đó nhấn Register để thực hiện đăng ký thành viên. Sau khi đăng ký xong các bạn vào SQL Server sẽ thấy Visual Studio đã tạo ra 1 database tên như các bạn đặt ở Web.config, trong database này sẽ chứa 5 table dbo.AspNetRoles, dbo.AspNetUserClaims, dbo.AspNetUserLogins, dbo.AspNetUserRoles, dbo.AspNetUsers và 1 table lưu giữ lịch sử sửa đổi DbContext trong project là dbo.__MigrationHistory  phục vụ trong việc quản lý user trên web. Mở table dbo.AspNetUsers lên thì thấy 1 record chứa 1 các thông tin mà bạn vừa nhập ở form Register (xem hình dưới)






Đồng thời website sẽ trả về lại trang chủ với trạng thái người dùng đã đăng nhập như hình dưới:


Chúng ta cùng tìm hiểu cơ chế hoạt động của chức năng đăng ký trong ASP.NET Identity nào. Đầu tiên khi mọi người click vào link Register thì ASP.NET sẽ gọi tới action Register (dòng 140) trong AccountController.  Action này có attribute [AlowAnonymous] định nghĩa trên đầu để biểu thị rằng action cho phép cả người dùng vô danh (khách mới đến website) truy cập vào, điều này rất dễ hiểu bởi nếu chức năng đăng ký thành viên mà chỉ cho phép người dùng đã đăng nhập truy cập vào thì rất vô lý phải không nào. Nội dung action này đơn giản chỉ trả về 1 view Register. Nội dung của view Register được tìm thấy file Views/Account/Register.cshtml. 
Mở view này lên chúng ta sẽ thấy rằng view định nghĩa model là 1 đối tượng của class RegisterViewModel được định nghĩa trong file Models/AccountViewModel. Đoạn lệnh thể hiện cho điều này: @model Demo_ASP.NET_Identity.Models.RegisterViewModel. Đây là đoạn code định nghĩa class RegisterViewModel: 



Đoạn định nghĩa này sử dụng các attribute nằm trong namespace System.ComponentModel.DataAnnotations. Mình giải thích 1 số attribute được sử dụng trong class này:
  • Required: biểu thị rằng thuộc tính có bắt buộc phải nhập (khác null) hay không
  • EmailAddress: biểu thị rằng thuộc tính này phải đúng định dạng Email
  • Display: tên được hiển thị ra View
  • StringLength: độ dài ký tự tối thiểu, tối đa của 1 thuộc tính có kiểu string
  • DataType: định dạng của thuộc tính
  • Compare: so sánh giá trị thuộc tính này với 1 thuộc tính khác phải khớp 
Như vậy chúng ta có thể rút ra kết luận về ràng buộc trong RegisterViewModel như sau:
Thuộc tínhQuy tắc
Email
  • Không được bỏ trống
  • Đúng định dạng Email
  • Tên hiển thị là Email
Password
  • Không được bỏ trống
  • Tối thiểu 6 ký tự, tối đa 100 ký tự
  • Định dạng Password
  • Tên hiển thị là Password
ConfirmPassword
  • Định dạng Password
  • Tên hiển thị Confirm Password
  • Giá trị phải khớp với thuộc tính Password

Các bạn thấy rằng mặc định tên hiển thị và thông báo lỗi ở dạng tiếng Anh. Bây giờ mính muốn tùy chỉnh nó sang tiếng Việt thì đơn giản là chúng ta thay đổi giá trị của thuộc tính ErrorMessage trong các atrribute và giá trị thuộc tính Name trong attribute [Display]. Mình sẽ thay đoạn lệnh thành như sau:


Vậy là đã cấu hình lại RegisterViewModel xong ^^. Bây giờ các bạn quan sát trong view Register sẽ thấy 1 form có method là POST với action là Register và controller là Account, phía dưới form là đoạn khai báo script jqueryval để xử lý bắt lỗi ở client. Nội dung như sau:


Form sử dụng các HTML Helper trong ASP.NET MVC (cái này mình không giải thích bởi vì những kiến thức này là căn bản trong ASP.MVC ^^) . Khi người dùng ấn Register thì form sẽ được đệ trình về phía server sau đó server sẽ gọi đến action Register có attribute [HttpPost]  trong AccountController. Đây là nội dung của action này:


Action này sử dụng thêm attribute [ValidateAntiForgeryToken] để bảo mật website tránh hacker mạo danh website và gửi 1 request không hợp lệ đến server. Chi tiết về tác dụng của attribute này các bạn tham khảo thêm ở link này nhé http://www.oasearch.net/Tai-sao-su-dung-ValidateAntiForgeryToken-trong-ASPNET-MVC-83 .
Từ khóa async và Task để biểu thị rằng đây là 1 action xử lý không đồng bộ, tham số của action là 1 đối tượng model được truyền từ form về. Đầu tiên đoạn code kiểm tra xem thông tin user nhập có hợp lệ hay không, nếu hợp lệ thì thực hiện tạo 1 đối tượng user của class ApplicationUser. Tiếp theo là gọi đến phương thức CreateAsync trong class UserManager (sau này các bạn dùng class này thường xuyên trong việc quản lý User) để thực hiện tạo 1 user mới. Lệnh if (result.Success) kiểm tra nếu việc tạo user thành công thì sẽ thực hiện đăng nhập vào website bằng cách gọi đến phương thức SignInAsync trong class SignInManager. Tất cả 2 lợi gọi đều ở dạng không đồng bộ (async) để tránh việc website bị đơ. Sau khi thực hiện gọi method SignInAsync xong thì website sẽ trả về lại trang chủ bằng lệnh RedirectToAction("Index","Home");
Giả sử nếu thông tin user nhập ở form là không hợp lệ thì đoạn lệnh if sẽ không được thực hiện thay vào đó là nó sẽ gọi tới phương thức AddErrors(result) và trả về chính View Register kèm theo model để hiển thị những lỗi ra view. Chi tiết về phương thức AddErrors các bạn tìm đến dòng 439 nhé. Các bạn thấy rằng ASP.NET Identity có 1 đoạn chú thích rất dài trong action này đúng không?  Chúng ta sẽ tìm hiểu về nó khi thực hiện bổ sung chức năng xác thực user đăng ký qua email sau nhé.
OK bây giờ thử build lại website và vào lại trang Register để thử đăng ký thêm 1 user nữa nào. Dưới đây là 1 số hình ảnh test báo lỗi các bạn thử test xem có giống mình không ^^
Màn hình khi mình không nhập đủ thông tin


Màn hình khi mình nhập email không hợp lệ và không nhập mật khẩu, xác thực mật khẩu


Màn hình khi mình nhập mật khảu và xác thực mật khẩu không khớp

Tất cả đều OK hết đúng không nào. Nếu bạn nhập chính xác thì sẽ đăng ký thành công bình thường và table dbo.AspNetUsers sẽ có thêm 1 record nữa.

4. Chức năng đăng nhập (Login)

1 user khi đăng ký thành công thì phải đăng nhập được vào website phải không nào. Bây giờ các bạn thử đăng xuất tài khoản hiện tại bằng cách click vào link Log off bên góc phải menu nhé. Sau đó các bạn click vào link Log in để thực hiện đăng nhập nào. Hình bên dưới cho thấy giao diện mặc định của trang Login, tất nhiên là các bạn có thể tùy chỉnh lại giao diện Login theo ý các bạn. Luôn luôn có thể như vậy.


Thử bỏ trống email và password rối ấn Log in thì sẽ nhận được báo lỗi bằng tiếng Anh. Các bạn có thể chỉnh sang tiếng Việt bằng cách sửa định nghĩa của class LoginViewModel trong file Models/AccountViewModel.cs như làm lúc trước nhé. Đây là nội dung định nghĩa của class LoginViewModel khi mình sửa lại:


Giờ chúng ta cùng tìm hiểu lõi bên trong của chức năng này nhé. Đầu tiên khi các bạn click vào link Log in thì server sẽ gọi đến action Login (dòng 58) trong AccountController. Action này nhận 1 tham số string returnUrl để trả về Url này khi user log in thành công. Nội dung bên trong chỉ có 2 dòng, dòng đấu gán tham số returnUrl qua ViewBag.ReturnUrl để sử dụng bên trong view Login, dòng thứ hai để trả ra 1 view Login. Nội dung view này được tìm thấy tại file Views/Account/Login.cshtml. Hình bên dưới thể hiện nội dung của view này:



View này sử dụng model là 1 đối tượng của class LoginViewModel mà bạn vừa tùy chỉnh. View có 1 form với method là POST, action Login, controller là AccountController. Tạm thời các bạn chỉ quan tâm đến form này chưa cần quan tâm đến lệnh  @Html.Partial("_ExternalLoginsListPartial", new ExternalLoginListViewModel { ReturnUrl = ViewBag.ReturnUrl }) nhé. Chúng ta sẽ nói về lệnh này ở bài viết khác.
Khi user ấn nút Log in thì form sẽ đệ trình về server sau đó server gọi đến action Login có attribute [HttpPost] tại dòng 66 trong AccountController. Hình dưới thể hiện nội dung của action này:



3 attribute này mình đã giải thích ở trên rồi. Action này cũng sử dụng từ khóa asyncawait để xử lý bất đồng bộ, đồng thời nó nhận vào 2 tham số là modelreturnUrl. Đầu tiên action kiểm tra xem nếu dữ liệu người dùng nhập không hợp lệ thì sẽ trả về View Login kèm theo model để hiển thị lỗi. Nếu hợp lệ thì sẽ nhảy xuống lệnh gọi phương thức PasswordSignInAsync của class SignInManager kèm theo các tham số cần thiết được lấy từ model để thực hiện đăng nhập vào website. Bên dưới là đoạn lệnh switch - case để kiểm tra giá trị result. Dưới đây là các case:
  • SignInStatus.Success: tức là đăng nhập thành công. Action sẽ trả về URL mà user vừa đứng trước đó.
  • SignInStatus.LockedOut: user này đã bị lock (khóa) (Chúng ta sẽ tìm hiểu cách tùy chỉnh về việc khóa user ở bài viết khác)
  • SignInStatus.RequiresVerification: khi nhảy vào case này tức là chúng ta đã cấu hình yêu cầu user đăng nhập thông qua 2 bước bao gồm bước nhận code từ số điện thoại đăng ký. Tuy nhiên series của mình không hướng dẫn về cách thức này. Các bạn có thể tìm hiểu về nó ở link này nếu muốn ^^ https://www.asp.net/identity/overview/features-api/two-factor-authentication-using-sms-and-email-with-aspnet-identity
  • SignInStatus.Failure hoặc default: xảy ra 1 lỗi khi đăng nhập. Có thể do user nhập thông tin không tồn tại trong CSDL hoặc do 1 lỗi bất thường nào đó ở server mà ta không biết trước. Các bạn có thể thay chuỗi "Invalid login attempt" thành chuỗi tiếng Việt như "Email hoặc mật khẩu không chính xác". Nếu nhảy vào case này thì action sẽ trả về view Login kèm theo model để hiển thị lỗi ra view này.
Tóm lại, nội dung trong action này cũng rất logic và dễ hiểu các bạn nhỉ?
Vậy là mình đã giải thích đầy đủ cơ chế chức năng đăng nhập trong ASP.NET Identity. Bây giờ bạn hãy build lại website và thử log in nhé.

5. Chức năng đăng xuất (Log off)

Cơ chế xử lý của chức năng này thì cực kỳ đơn giản . Khi các bạn click vào link Log off thì server sẽ gọi tới action LogOff (dòng 391) trong AccountController. Action này gọi tới phương thức SignOut trong class AuthenticationManager kèm theo cookie của ứng dụng để ASP.NET Identity remove những cookie đã lưu khi user đăng nhập, cuối cùng thì redirect về trang chủ.

6. Sự hiển thị của menu

Nhiều bạn sẽ thắc mắc vì sao menu hiển thị 2 link Register và Login khi user chưa đăng nhập và khi đã đăng nhập thì hiển thị link Hello và Log off ? Để hiểu điều này thì các bạn mở view _Layout.cshtml ở thư mục Views/Shared lên sẽ thấy đoạn lệnh @Html.Partial("_LoginPartial") sau thẻ ul của menu. Tiếp tục mở view _LoginPartial ở thư mục Shared luôn thì sẽ thấy đoạn lệnh sau:


Đầu tiên thì lệnh @using là cần thiết để sử dụng phương thức GetUserName() trong namespace Microsoft.AspNet.Identity. Bên dưới là đoạn lệnh kiểm tra xem user đã đăng nhập hay chưa, nếu user đã đăng nhập thì hiển thị form LogOff cùng với 2 link HelloLog off còn nếu user chưa đăng nhập thì hiển thị 2 link RegisterLog in.

7. Xác thực email khi đăng ký thành viên

Một chức năng phổ biến thường gặp ở các website đó là xác thực tài khoản thông qua email khi đăng ký. ASP.NET Identity hỗ trợ rất tốt developer cho việc cấu hình chức năng này. Tuy nhiên chức năng này "không bắt buộc" phải có. Nếu các bạn không có nhu cầu sử dụng thì có thể bỏ qua phần này. Sau đây là các bước cấu hình để gửi mail thông qua tài khoản gmail:
Bước 1: Mở file IdentityConfig.cs trong folder App_Start. Tiếp theo bạn using thêm 2 namespace System.Net.Mail và System.Net, sau đó thay thế phương thức SendAsync trong class EmailService thành như sau:


Các bạn thay thế your_email@gmail.com thành địa chỉ gmail của bạn, password thành password gmail của bạn, DisplayName thành tên muốn hiển thị mà user sẽ thấy khi nhận được mail. 
Bước 2: Truy cập vào link https://www.google.com/settings/security/lesssecureapps và bật quyền truy cập cho các ứng dụng kém an toàn.
Bước 3: Tìm đến action Register trong AccountController và uncomment đoạn chú thích và thay đổi chút xíu code để hiển thị thông báo cho user biết rằng họ cần vào email để xác thực tài khoản. Nội dung như sau:


Bước 4: Mở view Register và bổ sung đoạn lệnh sau vào sau thẻ <h2>@ViewBag.Title</h2>:




Bước 5: Mở view ConfirmEmail ở file Views/Account/ConfirmEmail.cshtml. Đây là view hiển thị thông báo khi user xác thực email thành công. Các bạn có thể tùy chỉnh thành thông báo tiếng Việt nhé
Bước 6: Build lại website.
Dưới đây là kết quả của mình khi cấu hình email diepanhblog@gmail.com là email gửi link xác nhận và email nguyenaituan@yahoo.com là email đăng ký thành viên.




Chú ý: theo mặc định việc xác thực Email là không bắt buộc đối với user. Nếu họ không xác thực thì họ vẫn có thể đăng nhập vào website bình thường tuy nhiên họ không thể sử dụng chức năng lấy lại mật khẩu khi quên mật khẩu. Điều này là hợp lý bởi vì không phải ai cũng xài email 

8. Tổng kết

Qua bài viết này các bạn có thể thấy được sự lợi hại của ASP.NET Identity trong việc bảo mật website rồi chứ. Còn rất nhiều điều thú vị chờ đợi bạn khám phá. Ở bài viết tiếp theo chúng ta sẽ cùng thực hiện bổ sung các chức năng sau:
  1. Khóa (Lock) user
  2. Đổi mật khẩu
  3. Lấy lại mật khẩu
  4. Bổ sung thông tin của User
Nếu thấy bài viết của mình có ích cho bạn hãy like + comment nhiệt tình vào nhé :)). Đừng quên đăng ký để nhận được những bài viết mới nhất từ Blog Chuyên Lập Trình nhé. Hẹn gặp lại các bạn ở bài viết tiếp theo trong series.

This Is The Oldest Page


EmoticonEmoticon