Thinking in Play framework
Bài đăng này đã không được cập nhật trong 3 năm
Như đã hứa trong phần trước tiếp tục các loạt bài tìm hiểu về Play framework, chúng ta sẽ cũng nhau tìm hiểu xây dự một hệ thống authentication.
Authenticating users
Trong phần tiếp theo này chúng ta sẽ thử cài đặt một ví dụ nho nhỏ mà hầu hết ai cũng từng làm qua trên bất cứ nền tảng nào: đó là xây dựng một authenticate system.Tất nhiên cũng có những giải pháp có sẵn của bên thứ 3 tuy nhiên chúng ta sẽ xây dựng cơ chế authenticating của chính mình chỉ bằng Play API.Đầu tiên chúng ta cần giải pháp để lưu trữ user information, cùng bắt đầu với việc tìm kiếm một database system được sử dụng trong Scala nhé.
Installing PostgreSQL
Cơ sở dữ liệu quan hệ có lẽ là công cụ phổ biến nhất khi nói đến việc lưu trữ dữ liệu của chúng ta, với lợi thế là khá nhanh chóng đáng tin cậy, được hỗ trỡ bởi hầu hết các ngôn ngữ lập trình.Trong số các Open-Source database thì PostgreSQL nổi nên như một tên tuổi được phát triển rất nhiều.Thông tin hướng dẫn cài đặt tham khảo tại: https://www.postgresql.org/download/linux/ubuntu/.
Chúng ta cần sử dụng 2 gói core server và contrib package.Chúng ta cũng nên cài đặt thêm pgAdmin để sử dụng như là 1 công cụ đồ họa giúp query database và view kết quả một cách trực quan.
$ sudo apt-get install postgresql-9.4 postgresql-contrib-9.4 pgadmin3
Sau khi cài đặt xong, Ubuntu tự động start server.Bước tiếp theo của chúng ta là tạo database chứa các user.Cái khó ở đây là sử dụng postgres Linux user:
$ /usr/bin/sudo -u postgres psql --command "CREATE USER scalauser WITH SUPERUSER\
PASSWORD 'scalapass';"
$ /usr/bin/sudo -u postgres createdb -O scalauser scaladb
Trên đây chúng ta sẽ tạo một user mới là scalauser và một database là scaladb.
Adding ScalikeJDBC
Như chúng ta đã biết tất cả các công cụ cơ sở dữ liệu trong Scala đều sử dụng JDBC (Java Database Connectivity), chúng ta cần thêm PostgreSQL JDBC drivers vào trong list dependencies trong build.sbt:
// ...
libraryDependencies ++= Seq(
// ...
"org.postgresql" % "postgresql" % "9.4.1207.jre7"
)
// …
Vì chúng ta đang sử dụng Java 8 nên chúng ta có thể sử dụng một version xây dựng cho JRE7. Để truy cập cơ sở dữ liệu, chúng ta sẽ sử dụng ScalikeJDBC(tham khảo tạo: http://scalikejdbc.org/).Đó là một thư viện tương đối tốt gần như có mọi thứ mà chúng ta muốn.Nó cung cấp cho chúng ta sự trực quan và an toàn để viết các truy vấn SQL, và việc cấu hình cũng tương đối đơn giản.Sau đây là cách để add ScalikeJDBC library vào trong list của dependencies:
// ...
libraryDependencies ++= Seq(
// ...
"org.scalikejdbc" %% "scalikejdbc" % "2.3.5",
"org.scalikejdbc" %% "scalikejdbc-config" % "2.3.5",
"ch.qos.logback" % "logback-classic" % "1.1.3"
)
// …
Sau khi đã có tất cả các libraries, chúng ta cần edit application.conf file để xác định thuộc tính kết nối của database:
play.crypto.secret = "changeme"
play.i18n.langs = [ "en" ]
play.application.loader = "AppApplicationLoader"
db.default.driver=org.postgresql.Driver
db.default.url="jdbc:postgresql://localhost:5432/scaladb"
db.default.username=scalauser
db.default.password=scalapass
db.default.poolInitialSize=1
db.default.poolMaxSize=5
db.default.ConnectionTimeoutMillis=1000
play.evolutions.autoApply=true
Ở đây chúng ta cũng xác định các cài đặt kết nối.Bằng việc sử dụng Connection pooling, ScalikeJDBC có khả năng tái sử dụng các kết nối thay vì tạo một kết nối mới, giúp tăng hiệu năng cho ứng dụng.
Vì chúng ta sử dụng scalikejdbc-config helper package, việc khởi tạo là tương đối đơn giản.Chúng ta chỉ cần gọi Dbs.setupAll() khi ứng dụng starting và Dbs.closeAll() khi stopping. Rõ ràng nơi tốt nhất để xử lí code là ở trong AppComponents trait:
trait AppComponents extends BuiltInComponents with AhcWSComponents {
// ...
applicationLifecycle.addStopHook { () =>
Logger.info("The app is about to stop")
DBs.closeAll()
Future.successful(Unit)
}
val onStart = {
Logger.info("The app is about to start")
DBs.setupAll()
statsActor ! Ping
}
}
Storing passwords
Trước khi tạo bảng Users, thử nghĩ một chút về các thông tin cần phải lưu trữ.Rõ ràng chúng ta cần login names và user codes, và ID.Thế còn passwords? Kinh nghiệm cho thấy lưu trữ mật khẩu chưa mã hóa thực sự là một ý tưởng không tốt, rõ ràng nếu database của chúng ta bị đánh cắp, tất cả những user phải thay đổi password của mình.Tuy nhiên khi người dùng đăng nhập lại, thực sự thì chúng ta không cần lưu trữ user passwords trong database, lưu trữ 1 hash các password là đủ và chúng chúng ta dễ dàng xác thực được người dùng.Đây là chính là cách thức hoạt động của nó.
- Mỗi user đều lựa chọn một password trong quá trình đăng kí
- Về phía server chúng ta sẽ mã hóa một chiều password này bằng một hash function, nghĩa là việc mã hóa ngược lại là gần như không thể.
- Sau đó chúng ta lưu trữ lưu trũ hash này trong database
- Tiếp theo khi người dùng thử đăng nhập, họ nhập mật khẩu ban đầu một lần nữa.Chúng ta sẽ dùng mật khẩu đó tính toán lại một lần nữa và so sánh xem có match với database không, nếu có thì đã xác thực được người dùng.
Cách tiếp cận này chỉ có một vấn đề nhỏ. Hầu hết các hàm băm rất nổi tiếng và có nhiều bảng (Gọi là bảng cầu vồng) có chứa hàm băm đã được tính toán cho hàng triệu mật khẩu. Sử dụng các bảng này, rất có thể sẽ tìm thấy một mật khẩu ban đầu bằng cách băm của nó. Tuy nhiên, rất dễ dàng để chống lại điều này bằng cách thêm một số text bổ sung (có thể được tạo ra một cách ngẫu nhiên), gọi là salt, đến mật khẩu gốc trước khi băm. Phần quan trọng ở đây là cũng việc lưu trữ salt vì nó cần thiết cho việc xác thực.Thuật toán hoàn chỉnh được miêu tả ở phần dưới đây:
- Một người sử dụng chọn một mật khẩu và cung cấp nó trong quá trình đăng ký. Không có thay đổi ở đây.(phần này không thay đổi so với trên).
- Về phía server, chúng ta lấy mật khẩu này, tạo ra một số salt, trộn chúng với nhau và tính toán băm của hỗn hợp này.
- Sau đó, chúng tôi lưu trữ cả mật khẩu và salt trong cơ sở dữ liệu
- Tiếp theo khi người dùng đăng nhập, chúng ta lấy mật khẩu được cung cấp của họ , tìm salt trong cơ sở dữ liệu, trộn chúng với nhau và và tính ra hash của hỗn hợp này, bằng việc matching nó với những gì đã được lưu trữ chúng ta có thể quyết định có thực người dùng này không
Đối với các hàm băm chính nó, chúng ta sẽ sử dụng một thư viện được gọi jBCrypt54. Nó được viết bằng Java chúng ta có thể sử dụng nó một cách dễ dàng tại Scala. Hãy thêm Maven coordinates55 của thư viện vào danh sách phụ thuộc(dependencies):
// ...
libraryDependencies ++= Seq(
// ...
"de.svenkubiak" % "jBCrypt" % "0.4.1"
)
// …
Trước khi bắt đầu viết code, chúng ta có thể nghĩ đến một thư viện mới, và rất tốt để thử sử dụng trên Scala.Chúng ta có thể dễ dàng chuyển đổi từ interpreter sang Activator.Đơn giản chỉ cần nhấn Ctrl + D để stop server, reload project để update dependencies và gõ trên console:
[scala-web-project] $ reload
[scala-web-project] $ console
[info] Starting scala interpreter...
[info]
Welcome to Scala version 2.11.7.
Type in expressions to have them evaluated.
Type :help for more information.
scala>
Một điểm tuyệt vời về việc starting interpreter theo cách đó là bạn có quyền truy cập vào tất cả các ứng dụng mà bạn đã viết và tất cả các thư viện được liệt kê trong phần phụ thuộc.Sau đó chúng ta thêm jBCrypt vào build.sbt, chúng ta có thể sử dụng như sau:
scala> import org.mindrot.jbcrypt.BCrypt
import org.mindrot.jbcrypt.BCrypt
scala> val hash = BCrypt.hashpw("password123", BCrypt.gensalt())
hash: String = $2a$10$niF.amAexQMHaevqlkganeSjvMHfTq/OdISyj8/5BQy1FHvlbi3Ne
Ở đây chúng ta đang sử dụng bcrypt để băm một mật khẩu đơn giản.Bcrypt cũng tạo ra một số salt và gắn thêm nó vào chuỗi kết quả.Đây là chuỗi chính xác sẽ được lưu trữ trong bảng Users.Nếu chúng ta muốn kiểm tra lại mật khẩu có thể sử dụng checkpw function:
scala> BCrypt.checkpw("password123", hash)
res0: Boolean = true
scala> BCrypt.checkpw("password321", hash)
res1: Boolean = false
Lưu ý rằng checkpw với mật khẩu sai sẽ trả về kết quả là false cho function trên.
Managing database evolutions.
Đây là bước cuối cùng tạo bảng User.Trên lý thuyết chúng ta có thể sử dụng pgAdmin3 hoặc một số tool hỗ trợ khác để thay đổi cơ sở dữ liệu.Tuy nhiên có một cách tiếp cận tốt hơn.Trong trường hợp schema thay đổi một phần mã nguồn thì sẽ được đưa vào hệ thống kiểm soát phiên bản.Hơn nữa các nhà phát triển cũng nhanh chóng nhận ra các cập nhật mà chúng ta đã thay đổi.Để làm được điều đó trên Play framework, chúng ta cần đặt các tập tin SQL tại một nơi cụ thể và thực hiện thêm một số bước.Kể từ khi chúng ta xác định được thuộc tính kết nối tới cơ sở dữ liệu như db.default, chúng ta cần phải đặt các kịch bản tiến hóa trong conf/evaluation/default, vì vậy chúng ta sẽ tạo một file tên là 1.sql với content dưới đây:
# --- !Ups
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE TABLE users
(
user_id UUID DEFAULT uuid_generate_v4() PRIMARY KEY,
user_code VARCHAR(250) NOT NULL,
password VARCHAR(250) NOT NULL
);
INSERT INTO users VALUES (uuid_generate_v4(), 'stest',
'$2a$10$niF.amAexQMHaevqlkganeSjvMHfTq/OdISyj8/5BQy1FHvlbi3Ne');
# --- !Downs
DROP TABLE users;
DROP EXTENSION "uuid-ossp";
Trước tiên chúng ta enable UUID extension như chúng ta muốn để sử dụng UUIDs như một khóa chính.Sau đó, chúng ta sẽ tạo một bảng và cuối cùng là insert một test user.Test user sẽ được login với user_name: “stesst”, password: “passsword123”.Khi jBCrypt gắn thêm salt vào hash chúng ta không cần một cột riêng để lưu trữ salt.Chúng ta cũng thêm evaluation Play module vào danh sách dependencies trong build.sbt:
// ...
libraryDependencies ++= Seq(
jdbc,
cache,
ws,
evolutions,
// ...
)
// …
Và cuối cùng chúng ta phải enable evoluations trong AppCompanents.Lý do ở đây cũng không hẳn là rõ ràng, nó được miêu tả chi tiết step-by-step:
-
Chúng ta cần phải apply evoluations khi ứng dụng start.Trong Play, evoluations được apply khi applicationEvolutions field từ EvolutionComponents được giải quyết.Vì vậy để get được field này chúng ta cần mở rộng EvoluationComponents.
-
EvolutionsComponents được bổ sung thêm một số thành phần trừu tượng, có tên là dynamicEvolutions (of type DynamicEvolutions ) và dbApi (of type DBApi ).
-
Thành phần trừu tượng dynamicEvolutions không phải là vấn đề, chúng ta có thể tạo một field mới một cách đơn giản khỏi tạo nó sử dụng constructor mặc định DynamicEvolutions
-
Thực thể dbAPi có thể được tìm thấy ở trong DBComponents vì thế hãy mở rộng nó hơn.
-
Bây giờ chúng ta đã có dbApi, nhưng DBComponents định nghĩa abstract field khác gọi là connectionPool
-
Thực thể ConnectionPool có thẻ được tìm thấy trong HikariCPComponents, và đặc điểm này không giới thiệu thêm abstract field vì vậy chúng ta thực hiện xong ở đây.
-
Cuối cùng khi applicationEvolutions field được đánh dấu chúng ta cần refer tới code khởi tạo để vấn đề đó được onStart .
Các vấn đề được nói tới tương đối nhiều tuy nhiên phần code sẽ khá ngắn gọn:
trait AppComponents extends BuiltInComponents with AhcWSComponents
with EvolutionsComponents with DBComponents with HikariCPComponents {
// ...
lazy val dynamicEvolutions = new DynamicEvolutions
// ...
val onStart = {
Logger.info("The app is about to start")
applicationEvolutions
DBs.setupAll()
statsActor ! Ping
}
}
Nếu bạn refresh lại trình duyệt, Play sẽ apply các SQL script và một bảng mới được tạo ra.You sẽ không thể tìm thấy dấu hiệu nào về điều đó nhưng bạn có thể thử các command sau trong console:
$ /usr/bin/sudo -u postgres psql -d scaladb --command "SELECT user_code,password\
FROM users;" | more
user_code | password
----------+-------------------------------------------------------------
stest | $2a$10$niF.amAexQMHaevqlkganeSjvMHfTq/OdISyj8/5BQy1FHvlbi3Ne
(1 row)
Ngoài ra bạn có thể sử dụng pgAdmin3 và xem kết quả tương tự.
Authetication roadmap
Như vậy chúng ta đã có một test user ở trong database, hãy phác thảo xem chúng ta sẽ xử lí thế nào với việc cài đặt hệ thống authentication:
- Đầu tiên chúng ta sẽ cần một trang đăng nhập.Đây là trang dành cho người dùng chưa đăng nhập và được điều hướng khi có gắng truy cập vào một resource có yêu cầu authentication.
- Login page sẽ thu thập thông tin người dùng username và password rồi gửi nó cho một login endpoint.
- Các hệ thông login endpoint(thiết bị đăng nhập đầu cuối ) kiểm tra các thông tin cung cấp có hợp lệ hay không.Nếu đó là hợp lệ, chúng tạo ra một phiên cookie kết hợp với người dùng xác thực và thêm nó vào response.Chúng ta cũng sẽ lưu trữ cookie ở trong cache.
- Sử dụng 1 cookie trong một request match với một ở trong cache chúng ta dựa vào đó để tránh việc yêu cầu user phải login lại.
The login page Các trang đăng nhập có thể chỉ cần đơn giản không cần nhiều hiệu ứng từ JS hay Css, ví dụ dưới đây xây dựng một form đăng nhập bằng cách tạo ra một template mới là login.scala.html bao gồm 2 field 1 button:
@()
<html lang="en">
<head>
<title>Login page</title>
<link rel="stylesheet"
href="@routes.Assets.versioned("compiled/styles.css")">
<link rel="shortcut icon" type="image/png"
href="@routes.Assets.versioned("images/favicon.png")">
</head>
<body>
<form method="post" action="/login">
<fieldset>
<legend>Login</legend>
<p>
<label for="username">Username:</label>
<input id="username" type="text" name="username" />
</p>
<p>
<label for="password">Password:</label>
<input id="password" type="password" name="password"
</p>
<button type="submit" class="button--sm">Login</button>
</fieldset>
</form>
</body>
</html>
Controller action trả về một form login đơn giản, vì thế chúng ta sẽ viết như sau:
class Application(sunService: SunService,
//...
def login = Action {
Ok(views.html.login())
}
}
Cuối cùng chúng ta cần thêm login page vào routes:
GET /login controllers.Application.login
Khi đó kết quả sẽ như sau:
Các phần xử lí authentication sẽ được viết tiếp trong bài sau nhé!
All rights reserved