Chia sẻ kinh nghiệm làm dự án web REST API

05 tháng 10, 2021 - 5648 lượt xem

Bạn chuẩn bị phát triển dự án web REST API mà chia thành 2 team. Team frontend thì tập trung làm giao diện, gọi vào REST API để lấy ra dữ liệu. Team backend thì làm REST API, truy vấn cơ sở dữ liệu để trả ra dữ liệu cho team frontend.

Có 3 cách để 2 team phối hợp với nhau:

  • Một là team frontend clone project của team backend về và chạy project của team backend song song
  • Hai là team backend sử dụng ngrok để chuyển đổi localhost sang một domain public mà team frontend có thể dễ dàng gọi vào. (Có thể tìm hiểu ngrok tại đây)
  • Cách thứ 3 là team backend xây dựng một API online đẩy lên Internet để team frontend ở bất cứ đâu có thể gọi trực tiếp vào đó.

Ở cách thứ 1 có thể đảm bảo 2 bên kết nối nhau an toàn mà không dính đến các vấn đề như cross-site request vì cùng dùng chung một máy localhost. Tuy nhiên cách này khiến cho một bên phụ thuộc vào bên kia, team frontend clone project về mỗi lần bật máy lên phải khởi chạy project của team backend. Giả sử team frontend là những người không biết gì về backend, không biết cách chạy thì team backend phải hướng dẫn từng bước cho team frontend chạy dự án của mình. Ngoài ra nếu mỗi bên làm việc ở xa việc hướng dẫn sẽ càng khó khăn.

Cách thứ 2 và 3 thì đều dính đến vấn đề cross-site request, vì team frontend gọi API ở máy local tới một domain hoàn toàn khác. Điều này khiến team backend phải tạm thời nới lỏng bảo mật trong quá trình develop để team fronend có thể gọi API và chọc được vào session cookie.

Ở cách 2 ưu điểm là team backend mỗi lần cập nhật và chạy code team frontend có thể nhận được ngay. Tuy nhiên điều này phụ thuộc team backend phải bật ngrok 24/24. Nếu tắt ngrok đi thì domain public đang gọi vào sẽ biến mất. Ngoài ra sau khi bật lại ngrok sẽ lại sinh ra domain mới hoàn toàn. Điều đó khiến cho team frontend phải chỉnh sửa lại toàn bộ domain ở từng API một.

Cách thứ 3 thì tiện lợi, team frontend có thể kết nối API bất cứ lúc nào mà không phải chỉnh lại nhiều. Tuy nhiên không như cách 2, chỉ khi team backend đẩy code mình lên thì team frontend mới gọi được thêm các API mới. Ngoài ra khi tạo ra một API online trên Internet trong trạng thái nới lỏng bảo mật có thể dễ dàng bị tấn công.

1. Nên chọn cách nào

Không có cách nào không có rủi ro, không có những bất lợi riêng. Ta cần cân nhắc, xét đến nhiều yếu tố và làm sao 2 team càng ít phụ thuộc vào nhau càng tốt. Dưới đây quan điểm của mình:

  • Nếu tất cả đều là lập trình viên fullstack, có thể clone project chạy bình thường mà không gặp phải vấn đề khó khăn gì thì cách 1 là điều khuyến khích vì tính bảo mật của nó.
  • Trường hợp khác nếu hai bên luôn làm việc cùng một thời gian, thời điểm như nhau thì cách 2 là điều khuyến khích. Vì 2 bên có thể kết nối trực tiếp, dễ dàng, nhanh chóng và khả năng bị tấn công cũng rất thấp nhờ domain sinh ngẫu nhiên.
  • Trường hợp hai bên làm việc ở xa như đang đại dịch ngồi nhà làm, mỗi bên có chuyên môn riêng, thời gian làm việc ít bị quản thúc. Khi đó cách 3 sẽ là tiện nhất.

Trên là quan điểm của mình trong quá trình làm dự án. Tất nhiên những người kinh nghiệm lâu năm sẽ có nhiều cách và giải pháp hơn. Sau đây mình xin chia sẻ tiếp về bảo mật khi làm dự án.

2. Vấn đề về cross-site request khi làm dự án

Cross-site là khi bạn gọi request từ site của bạn tới một website khác domain.

2.1 Cors

Nếu bạn không thiết lập gì trong code thì khi request gọi từ một domain khác sẽ sinh ra lỗi CORS.

cors

Để domain A có thể gọi sang domain B, domain B phải cấp quyền cho domain A. Khi domain B cấp quyền thì tại response header ta sẽ thấy thuộc tính sau:

header

Ngoài Access-Control-Allow-Origin ra còn có Access-Control-Allow-Methods để cho phép các method khi goi API như GET, POST,...

2.2 Cookies

Khi đã truy cập vào request đôi lúc ta cũng gặp một chướng ngại nữa, đó là chọc vào cookie để set cookie và lấy ra dữ liệu đăng nhập cookie. Cookie cũng có cơ chế chặn cross-site. Vì vậy để có thể cho domain khác chọc được vào ta cần phải set các thuộc tính (attributes) cho cookie. Các thuộc tính của cookie sau khi set hiện ra như sau:

cookie

Ở trên ta có thể dễ dàng thấy thuộc tính Max-Age=3600 tức thời gian cookies hết hạn là 3600 giây, expires là thời điểm hết hạn. Domain chính là các domain được phép chọc vào, ở đây domain rỗng nên mọi site khác đều có thể chọc vào request. Nếu để mặc định thì domain=<tên domain website hiện tại> tức chỉ cho phép các site nào cùng domain đó được chọc vào cookie.

Vậy bạn cũng hình dung để từ domain khác gọi vào request chọc được vào cookie ta cần set thuộc tính domain về rỗng.

Cookie cũng có thuộc tính là HttpOnly để ngăn chặn việc chọc vào nó từ request ở javascript. Khi bạn đã set domain rỗng nhưng httponly true thì bạn vẫn chỉ có thể chọc vào cookie ở server-side rendering nhưng không thể ở javascript.

Có thể tham khảo thêm về các thuộc tính cookie tại đây

Như vậy việc nới lỏng bảo mật, mở cửa cho phép cross-site là cần thiết trong giai đoạn development, giúp 2 team frontend và backend có thể dễ dang kết nối với nhau. Tuy nhiên điều đó cũng có thể dễ dàng bị tấn công ở bên ngoài.

Lưu ý: phần này bạn có thể bỏ qua nếu bạn không lập trình bằng ngôn ngữ golang

Hiện trong golang mình dùng sessions lưu vào redis hỗ trợ bởi iris framework.

Ta config session như sau:

    Sess = sessions.New(sessions.Config{
        Cookie:       SESSION_COOKIE,
        AllowReclaim: true,
        Expires:      expires,
        CookieSecureTLS: true,
    })

Đăng ký sess handler làm middleware

app := iris.New()
app.Use(Sess.Handler(... cookieOptions))

Như bạn đã biết để có thể chọc vào cookie khác domain, ta cần set thuộc tính domain về rỗng và nếu chọc vào cookie từ javascript ở site khác cần set httponly = false.

Những tham số cookieOptions trong hàm Sess.Handler() trên có thể giúp bạn set attribute cho cookies ở từng request. Tuy vậy iris framework lại không hỗ trợ set thuộc tính domain về rỗng. Lúc này bạn cần đọc hiểu code trong thư viện để có thể tự tạo thêm options cho mình. Phần này mình sẽ giải thích thật ngắn gọn, dễ hiểu.

Đầu tiên cần hiểu cookieOptions là gì.

// CookieOption is the type of function that is accepted on
// context's methods like `SetCookieKV`, `RemoveCookie` and `SetCookie`
// as their (last) variadic input argument to amend the to-be-sent cookie.
//
// The "op" is the operation code, 0 is GET, 1 is SET and 2 is REMOVE.
type CookieOption func(ctx *Context, c *http.Cookie, op uint8)

Như vậy CookieOption trong Sess.Handler() chính là các tham số kiểu func với các tham số của nó.

Giờ xem thử một hàm cookieoption bất kỳ và xem cách viết, gán attribute cho cookie như thế nào

app.Use(Sess.Handler(iris.CookieExpires()))

Đây là hàm CookieExpires gốc của nó:

// CookieExpires is a `CookieOption`.
// Use it to change the cookie's Expires and MaxAge fields by passing the lifetime of the cookie.
func CookieExpires(durFromNow time.Duration) CookieOption {
    return func(_ *Context, c *http.Cookie, op uint8) {
        if op == OpCookieSet {
            c.Expires = time.Now().Add(durFromNow)
            c.MaxAge = int(durFromNow.Seconds())
        }
    }
}

Bạn có thể thấy đơn giản là khi truyền hàm CookieOption vào Sess.handler(), hàm CookieOption có chỉ có nhiệm vụ gán giá trị các field của tham số *http.Cookie là ta có thể xử lý set được attribute cho cookie. Ví dụ như trên hàm CookieExpires chỉ việc gán giá trị cho field Expires và MaxAge của http.Cookie là ta đã có thể set được 2 attribute expires và max age, rồi thấy rõ ở response header khi bạn gọi vào request Set Cookie.

Dựa vào func trên bạn có thể làm tương tự khi Set attribute Domain cho cookie. Mình đã làm như sau:

func CookieDomain(domain string) iris.CookieOption {
    return func(_ *context.Context, c *http.Cookie, op uint8) {
        c.Domain = domain
    }
}

Sau đấy ta chỉ cần truyền hàm CookieDomain trên vào Sess.Handler() và đặt tham số domain là rỗng

app.Use(Sess.Handler(CookieDomain("")))

Như vậy mình đã set thành công attribute Domain của cookie về rỗng và lúc này domain khác có thể chọc được vào cookie.

4. Kết

Trên là những kinh nghiệm của mình trong quá trình phát triển dự án web REST API sử dụng ngôn ngữ lập trình Golang. Tuy vậy những kinh nghiệm trên không dành có thể dành cho bất cứ dự án web nào không riêng gì golang. Mong rằng những kinh nghiệm sẽ giúp ích cho các bạn mỗi khi vào làm dự án Rest API, giúp bạn cân nhắc hướng giải quyết, cách phối hợp sao cho hiệu quả cao.

Bình luận

avatar
Trịnh Minh Cường 2021-10-05 14:08:07.570599 +0000 UTC

Rất tuyệt vời. Tôi học được vài điều

Avatar
* Vui lòng trước khi bình luận.
Ảnh đại diện
  +3 Thích
+3