Bài 11.Yêu cầu xác thực khi truy suất các REST API trong Vert.X

Như vậy hầu hết các loại Rest API trong Vert.X Tui đã hướng dẫn cụ thể. Tuy nhiên bất kỳ ai cũng có thể nhào vào các API này, thật là nguy hiểm đúng không? việc yêu cầu xác thực khi truy suất vào các REST API là rất quan trọng, nó giúp chúng ta bảo vệ dữ liệu, tránh bị hack ở một giới hạn nhất định nào đó.

Có rất nhiều cách để yêu cầu xác thực, dưới đây Tui sẽ hướng dẫn các bạn chi tiết cách dùng Bear Token để tạo lớp bảo vệ cho các REST API.

Dưới đây là các bước chi tiết để xây dựng bức tường xác thực cho các REST API, mở lại dự án “LearnRESTApi” ở bài trước:

Bước 1: Triệu gọi thư viện “vertx-auth”.

Trong Build.Gradle, ta bổ sung:

[code language=”java”]
dependencies {
testCompile group: ‘junit’, name: ‘junit’, version: ‘4.12’
implementation ‘io.vertx:vertx-web:3.9.4’
implementation ‘io.vertx:vertx-auth-jwt:3.9.4’
}
[/code]

Lưu ý vertx-webvertx-auth-jwt phải cùng phiên bản. Ví dụ ở trong bài viết này đang dùng 3.9.4.

JWT là một dạng token đơn giản sử dụng trong lập trình web giúp việc xác thực người dùng trong hệ thống trở nên dễ dàng hơn (đặc biệt trong ứng dụng Single Page Application).

Một số điểm mạnh của JWT:

  • Dễ dàng xác thực token
  • Có thể chứa data dưới dạng JSON -> dễ dàng giao tiếp trong nhiều hệ thống, gọn nhẹ.
  • Completely stateless

Bước 2: Tạo Java keystore files và sao chép vào dự án

JWT trong vertx có 3 cách load keys:

  • Sử dụng secrets (sysmetric keys)
  • OpenSSL (pem format) -> phổ biến là RSA
  • Java keystore files

Bài này Tui chọn cách setup java keystore. Tùy theo thuật toán mình muốn sử dụng mà cần tạo ra keystore tương ứng (RSA, SHA,….). Mặc định vertx sẽ sử dụng HS256.

Dưới đây là cú pháp tạo Java Keystore file:

[code language=”java”]
keytool -genseckey -keystore keystore.jceks -storetype jceks -storepass secret -keyalg HMacSHA256 -keysize 2048 -alias HS256 -keypass secret
[/code]

ví dụ ở trên secret là mật khẩu

file keystore.jceks sẽ được tạo ra, với thuật toán HS256

Cụ thể ta mở command line:

nhấn tổ hợp phím Windows + R để mở cửa sổ Run:

gõ lệnh cmd rồi nhấn OK, màn hình command line hiển thị ra như dưới đây:

Sao chép lệnh tạo Keystore ở trên vào command line:

Nhấn Enter:

như vậy file keystore.jceks được tạo ra trong C:\Users\<your user>

máy tính của bạn đang dùng account nào thì nó lưu vào account đó, ví dụ trong máy của Tui thì:

ở trên các bạn thấy keystore.jceks được tạo ra.

tạo thư mục keys trong dự án, sao chép file này vào thư mục keys (muốn tạo thư mục thì bấm chuột phải vào dự án chọn New/chọn Directory):

Bước 3: Cài yêu cầu xác thực Authetication cho các API

Để tránh không bị rối, không bị hư coding cũ, Tui tạo thêm 1 verticle tên là “EmployeeAuthenVerticle“, Verticle này chỉ đơn thuần là có 1 API lấy danh sách Employee. Nhưng phải có xác thực thì mới lấy được. Khi nào các bạn rành rồi thì tự cấu hình cho các API còn lại:

Nội dung mã nguồn như sau(sao chép từ EmployeeVerticle qua và chỉ giữ lại API lấy toàn bộ Employee):

[code language=”java”]
package tranduythanh.com.verticle;

import io.vertx.core.AbstractVerticle;
import io.vertx.core.Promise;
import io.vertx.core.http.HttpServerResponse;
import io.vertx.core.json.Json;
import io.vertx.ext.web.Router;
import io.vertx.ext.web.RoutingContext;
import tranduythanh.com.model.Employee;

import java.util.HashMap;

public class EmployeeAuthenVerticle extends AbstractVerticle {
private HashMap employees=new HashMap();
public void createExampleData()
{
employees.put(1,new Employee(1,”Mr Obama”,”Obama@gmail.com”));
employees.put(2,new Employee(2,”Mr Donald Trump”,”Trump@gmail.com”));
employees.put(3,new Employee(3,”Mr Putin”,”Putin@gmail.com”));
}
private void getAllEmployees(RoutingContext routingContext) {
HttpServerResponse response=routingContext.response();
response.putHeader(“content-type”,”application/json;charset=UTF-8″);
response.end(Json.encodePrettily(employees.values()));
}
@Override
public void start(Promise startPromise) throws Exception {
createExampleData();
Router router=Router.router(vertx);
router.get(“/api/employees”).handler(this::getAllEmployees);
vertx
.createHttpServer()
.requestHandler(router::accept)
.listen(config().getInteger(“http.port”, 8080),
result -> {
if (result.succeeded()) {
startPromise.complete();
} else {
startPromise.fail(result.cause());
}
}
);
}
}
[/code]

Tiếp tới cài đặt Authentication JWT:

[code language=”java”]
JWTAuth jwt=JWTAuth.create(vertx,
new JsonObject()
.put(“keyStore”,
new JsonObject()
.put(“type”,”jceks”)
.put(“path”,”keys\\keystore.jceks”)
.put(“password”,”secret”)));
[/code]

Sau đó setup Route các API yêu cầu phải xác thực, ví dụ lệnh dưới đây khi ai đó truy cấp API api/employees sẽ bị báo yêu cầu phải xác thực:

[code language=”java”]
router.route(“/api/*”).handler(authHandler(jwt));
router.get(“/api/employees”).handler(this::getAllEmployees);
[/code]

Cú pháp api/* ý nói mọi API được khai báo đằng sau phải xác thực.

hàm authHandler:

[code language=”java”]
public JWTAuthHandler authHandler(JWTAuth jwtAuth)
{
return JWTAuthHandler.create(jwtAuth,”/api/login”);
}
[/code]

Dưới đây là coding đầy đủ của EmployeeAuthenVerticle:

[code language=”java”]
package tranduythanh.com.verticle;

import io.vertx.core.AbstractVerticle;
import io.vertx.core.Promise;
import io.vertx.core.http.HttpServerResponse;
import io.vertx.core.json.Json;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.auth.jwt.JWTAuth;
import io.vertx.ext.web.Router;
import io.vertx.ext.web.RoutingContext;
import io.vertx.ext.web.handler.JWTAuthHandler;
import tranduythanh.com.model.Employee;

import java.util.HashMap;

public class EmployeeAuthenVerticle extends AbstractVerticle {
private HashMap employees=new HashMap();
public void createExampleData()
{
employees.put(1,new Employee(1,”Mr Obama”,”Obama@gmail.com”));
employees.put(2,new Employee(2,”Mr Donald Trump”,”Trump@gmail.com”));
employees.put(3,new Employee(3,”Mr Putin”,”Putin@gmail.com”));
}
private void getAllEmployees(RoutingContext routingContext) {
HttpServerResponse response=routingContext.response();
response.putHeader(“content-type”,”application/json;charset=UTF-8″);
response.end(Json.encodePrettily(employees.values()));
}
@Override
public void start(Promise startPromise) throws Exception {
createExampleData();
Router router=Router.router(vertx);
JWTAuth jwt=JWTAuth.create(vertx,
new JsonObject()
.put(“keyStore”,
new JsonObject()
.put(“type”,”jceks”)
.put(“path”,”keys\\keystore.jceks”)
.put(“password”,”secret”)));
router.route(“/api/*”).handler(authHandler(jwt));
router.get(“/api/employees”).handler(this::getAllEmployees);
vertx
.createHttpServer()
.requestHandler(router::accept)
.listen(config().getInteger(“http.port”, 8080),
result -> {
if (result.succeeded()) {
startPromise.complete();
} else {
startPromise.fail(result.cause());
}
}
);
}
public JWTAuthHandler authHandler(JWTAuth jwtAuth)
{
return JWTAuthHandler.create(jwtAuth,”/api/login”);
}
}
[/code]

Bây giờ ta tạo 1 UniversalAuthen để test EmployeeAuthenVerticle:

Coding chi tiết của UniversalAuthen:

[code language=”java”]
package tranduythanh.com;

import io.vertx.core.Vertx;
import tranduythanh.com.verticle.EmployeeAuthenVerticle;

public class UniversalAuthen {
public static void main(String[]args)
{
Vertx vertx=Vertx.vertx();
vertx.deployVerticle(new EmployeeAuthenVerticle());
}
}
[/code]

Chạy UniversalAuthen để test api employees, từ PostMan ta test:

Ở trên các Em thấy nó báo Unauthorized đó. Như vậy ta đã tạo được bức tường bảo vệ API lấy danh sách Employee.

Bước 4: Đăng nhập lấy Token để truy suất các API

Trong hàm start bổ sung 2 lệnh sau:

[code language=”java”]
router.route(“/api*”).handler(BodyHandler.create());
router.post(“/api/login”).handler(ctx->{login(ctx,jwt);});
[/code]

Code trên là để tạo ra api login, yêu cầu đăng nhập (trường hợp này Tui sẽ dùng JsonObject để truyền user+password lên server, trong hàm đăng nhập sẽ nói kỹ).

Coding chi tiết hàm hàm start:

[code language=”java”]
public void start(Promise startPromise) throws Exception {
createExampleData();
Router router=Router.router(vertx);
JWTAuth jwt=JWTAuth.create(vertx,
new JsonObject()
.put(“keyStore”,
new JsonObject()
.put(“type”,”jceks”)
.put(“path”,”keys\\keystore.jceks”)
.put(“password”,”secret”)));
router.route(“/api/*”).handler(authHandler(jwt));
router.get(“/api/employees”).handler(this::getAllEmployees);
router.route(“/api*”).handler(BodyHandler.create());
router.post(“/api/login”).handler(ctx->{login(ctx,jwt);});
vertx
.createHttpServer()
.requestHandler(router::accept)
.listen(config().getInteger(“http.port”, 8080),
result -> {
if (result.succeeded()) {
startPromise.complete();
} else {
startPromise.fail(result.cause());
}
}
);
}
[/code]

Còn dưới đây là mã lệnh chi tiết hàm API login:

[code language=”java”]
public void login(RoutingContext context,JWTAuth jwtAuth)
{
try
{
//lấy dữ liệu là 1 JsonObject ở client truyền lên
JsonObject data = context.getBodyAsJson();
//Tui giả sử username: admin và password là: 123
/* cú pháp minh họa từ client gửi lên:
{
“user”: “admin”,
“pass”: “123”
}
dĩ nhiên khi làm MongoDB thì mình sẽ truy vấn account thực tế, chỉ cần đổi chỗ này thôi, rất dễ
*/
if(!(data.getString(“user”).equals(“admin”) && data.getString(“pass”).equals(“123”)))
{
return;
}
//tạo token, hạn dùng 60 giây
String token=jwtAuth.generateToken(new JsonObject(),
new JWTOptions().setExpiresInSeconds(60));
//dùng cookie lưu xuống phía client, trong vòng 60 giây truy cấp được các API thoải mái
Cookie cookie=Cookie.cookie(“auth”,token);
cookie.setHttpOnly(true).setPath(“/”).encode();
context.addCookie(cookie).response()
.putHeader(“content-type”,”text/plain”)
.putHeader(“Authorization”,token)
.end(token);
}
catch (Exception ex)
{
context.response().setStatusCode(401)
.putHeader(“content-type”,”application/json”)
.end(ex.getMessage());
}
}
[/code]

Dưới đây là coding đầy đủ của EmployeeAuthenVerticle:

[code language=”java”]
package tranduythanh.com.verticle;

import io.vertx.core.AbstractVerticle;
import io.vertx.core.Promise;
import io.vertx.core.http.Cookie;
import io.vertx.core.http.HttpServerResponse;
import io.vertx.core.json.Json;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.auth.JWTOptions;
import io.vertx.ext.auth.jwt.JWTAuth;
import io.vertx.ext.web.Router;
import io.vertx.ext.web.RoutingContext;
import io.vertx.ext.web.handler.BodyHandler;
import io.vertx.ext.web.handler.JWTAuthHandler;
import tranduythanh.com.model.Employee;
import java.util.HashMap;
public class EmployeeAuthenVerticle extends AbstractVerticle {
private HashMap employees=new HashMap();
public void createExampleData()
{
employees.put(1,new Employee(1,”Mr Obama”,”Obama@gmail.com”));
employees.put(2,new Employee(2,”Mr Donald Trump”,”Trump@gmail.com”));
employees.put(3,new Employee(3,”Mr Putin”,”Putin@gmail.com”));
}
private void getAllEmployees(RoutingContext routingContext) {
HttpServerResponse response=routingContext.response();
response.putHeader(“content-type”,”application/json;charset=UTF-8″);
response.end(Json.encodePrettily(employees.values()));
}
@Override
public void start(Promise startPromise) throws Exception {
createExampleData();
Router router=Router.router(vertx);
JWTAuth jwt=JWTAuth.create(vertx,
new JsonObject()
.put(“keyStore”,
new JsonObject()
.put(“type”,”jceks”)
.put(“path”,”keys\\keystore.jceks”)
.put(“password”,”secret”)));
router.route(“/api/*”).handler(authHandler(jwt));
router.get(“/api/employees”).handler(this::getAllEmployees);
router.route(“/api*”).handler(BodyHandler.create());
router.post(“/api/login”).handler(ctx->{login(ctx,jwt);});
vertx
.createHttpServer()
.requestHandler(router::accept)
.listen(config().getInteger(“http.port”, 8080),
result -> {
if (result.succeeded()) {
startPromise.complete();
} else {
startPromise.fail(result.cause());
}
}
);
}
public JWTAuthHandler authHandler(JWTAuth jwtAuth)
{
return JWTAuthHandler.create(jwtAuth,”/api/login”);
}
public void login(RoutingContext context,JWTAuth jwtAuth)
{
try
{
//lấy dữ liệu là 1 JsonObject ở client truyền lên
JsonObject data = context.getBodyAsJson();
//Tui giả sử username: admin và password là: 123
/* cú pháp minh họa từ client gửi lên:
{
“user”: “admin”,
“pass”: “123”
}
dĩ nhiên khi làm MongoDB thì mình sẽ truy vấn account thực tế, chỉ cần đổi chỗ này thôi, rất dễ
*/
if(!(data.getString(“user”).equals(“admin”) && data.getString(“pass”).equals(“123”)))
{
return;
}
//tạo token, hạn dùng 60 giây
String token=jwtAuth.generateToken(new JsonObject(),
new JWTOptions().setExpiresInSeconds(60));
//dùng cookie lưu xuống phía client, trong vòng 60 giây truy cấp được các API thoải mái
Cookie cookie=Cookie.cookie(“auth”,token);
cookie.setHttpOnly(true).setPath(“/”).encode();
context.addCookie(cookie).response()
.putHeader(“content-type”,”text/plain”)
.putHeader(“Authorization”,token)
.end(token);
}
catch (Exception ex)
{
context.response().setStatusCode(401)
.putHeader(“content-type”,”application/json”)
.end(ex.getMessage());
}
}
}
[/code]

Như vậy đã đầy đủ mã lệnh, bây giờ chạy UniversalAuthen và mở Postman lên test:

Đầu tiên triệu gọi “http://localhost:8080/api/employees” nó sẽ báo lỗi chưa xác thực:

Bây giờ ta sẽ đăng nhập để lấy token bằng cách gọi api login:

Mục 1: Chọn phương thức POST

Mục 2: chọn API http://localhost:8080/api/login để đăng nhập

Mục 3: Trong body tạo JsonObject để gửi user + password lên:

[code language=”java”]
{
“user”: “admin”,
“pass”: “123”
}
[/code]

admin và 123 là tài khoản minh họa, khi làm MongoDB ta sẽ đăng nhập chính xác account trong CSDL.

Mục 4: bấm send để đăng nhập

Mục 5: Sau khi bấm send, đăng nhập thành công sẽ trả về chuỗi Token như hình.

Token này sẽ được dùng để triệu gọi các API trong hệ thống.

Bây giờ ta triệu gọi API lấy danh sách Employee, từ Postman ta triệu gọi:

Mục 1: Chọn GET

Mục 2: chọn API http://localhost:8080/api/employees

Mục 3: Chọn Authorization, trong Type chọn Bearer Token

Mục 4: Nhập token ở bước đăng nhập mà hệ thống trả về, dán vào mục Token

Mục 5: nhấn Send để truy vập API lấy danh sách Employee

Mục 6: sau khi xác thực thành công thì ta có kết quả như bên dưới, không bị báo là chưa xác thực. Lưu ý là token ta cấu hình chỉ có 60 giây vòng đời, các Em muốn tăng thì cứ tăng lên để test.

Như vậy tới đây Tui đã trình bày một cách đầy đủ và chi tiết từng bước tạo xác thực cho các API. các Bạn dựa vào đây để áp dụng cho các hàm còn lại nha (thực ra đơn giản), ráng suy nghĩ, coi như là 1 bài tập Tui giao.

Coding mẫu đầy đủ của bài này ở đây: https://www.mediafire.com/file/5v8a2hh4969d8rj/LearnRESTApi_Authentication.rar/file

Tới đây coi như các bạn để ổn phần API rồi nhé. Ở các bài sau Tui sẽ trình bày về MongoDB và cách tạo các API lấy và tương tác dữ liệu từ MongoDB.

Chúc các bạn thành công

Leave a Reply