Xác thực người dùng (authentication) và cấp quyền truy cập trong phạm vi cho phép (authorization) là những yêu cầu bắt buộc trong lập trình server. Xác thực người dùng thông thường là đối chiếu thông tin tài khoản (tên và mật khẩu) người dùng gửi lên và thông tin tài khoản server đã lưu trước đó. Cấp quyền là công việc cho phép client tiếp cận những tài nguyên phù hợp với vai trò của người dùng đã xác thực trước đó. Các công việc này khá phức tạp bởi đây là vấn đề bảo mật hệ thống.
Tài khoản người dùng và xác thực tài khoản
Tài khoản người dùng chứa các thông tin của người dùng trên server nhưng quan trọng nhất là thông tin để xác thực người dùng. Xác thực là công việc xác minh đúng người dùng đang sử dụng dịch vụ. Có nhiều cách xác thực khác nhau như dựa trên tri thức (nhớ đoạn PIN, mật khẩu riêng, v.v...), dựa trên sở hữu (thông tin trên thẻ cấp cho từng người, tin nhắn SMS chứa mật khẩu, v.v...) hoặc dựa trên sinh trắc học (vân tay, võng mạc, v.v...). Tùy theo mức độ bảo mật và sự thuận tiện khi sử dụng mà dịch vụ sử dụng cách xác thực phù hợp hoặc kết hợp nhiều cách xác thực cùng lúc. Thông thường các dịch vụ web thường dùng cách xác thực dựa trên tri thức bởi nó dễ thực hiện. Khi cần giao dịch bảo mật hơn, cách xác thực dựa trên sở hữu được áp dụng kèm.
Trong phương thức xác thực dựa trên tri thức, dịch vụ sẽ yêu cầu người dùng chọn một tên riêng biệt, không trùng với tên người dùng khác trong dịch vụ đã chọn trước đó và một mật khẩu tùy người dùng chọn nhưng theo một số quy định để đảm bảo mật khẩu đủ mạnh tránh bị dò ra. Sau khi có thông tin, dịch vụ sẽ xây dựng hệ thống thông tin tài khoản cũng như quyền truy cập gắn liền với tên duy nhất này(1). Quá trình này được gọi là quá trình đăng ký.
Bất kỳ khi nào muốn sử dụng dịch vụ, người dùng khai báo tên và cung cấp mật khẩu tương ứng. Client (trình duyệt hoặc ứng dụng di động) gửi cặp thông tin này cho server. Server so sánh cặp thông tin này với cặp thông tin đã lưu trong hệ thống, nếu khớp nhau người dùng được xác thực và client có quyền tiếp cận mọi thông tin liên quan đến tài khoản gắn với tên đó. Đây là quá trình đăng nhập dịch vụ.
Xác thực tài khoản sau đăng nhập
Ở trên chúng ta có bàn đến các bước xác thực người dùng trong quá trình đăng nhập nhưng đặc điểm của giao thức HTTP là không lưu trạng thái nên phải chăng mỗi lần gửi yêu cầu thì client đều phải gửi thông tin tài khoản để xác thực? Làm vậy cũng được nhưng rất dở vì client và server tốn chi phí gửi và xác thực. Việc lộ mật khẩu cũng rất dễ xảy ra khi mà nó được trao đổi nhiều lần như vậy trong môi trường mạng. Lúc này người ta nghĩ đến cơ chế khác để thực hiện xác thực mà không cần đến thông tin tài khoản do client gửi lên nữa. Đó là mã xác thực. Nó là chuỗi do server tạo ra đại diện cho người dùng và client dùng nó để xác thực người dùng sau khi đăng nhập thành công.
Có 2 phương pháp phổ biến để xác thực tài khoản sau đăng nhập bằng mã xác thực:
- Dùng phiên (session).
- Dùng thẻ bài (token)
Xác thực với session và cookie
Đây là hai khái niệm cực kỳ quen thuộc mà hầu như mọi lập trình viên web đều biết nhưng mọi người thường hay nhầm lẫn chúng với nhau. Cookie lưu thông tin của dịch vụ web trong đó có mã xác thực tại client (trình duyệt). Ngược lại, session lưu thông tin trao đổi giữa client và server tại server.
Quá trình xác thực với session và cookie như sau:
Quá trình xác thực với session và cookie như sau:
- Mỗi khi người dùng đăng nhập bằng thông tin tài khoản thành công, server sẽ tạo ra một session với một định danh session ID duy nhất trong dịch vụ.
- Session ID này sẽ được gửi về cho client và client mà cụ thể ở đây là trình duyệt sẽ sinh ra một cookie chứa session ID này.
- Với các yêu cầu tiếp theo sau đó, trình duyệt chỉ cần gửi session ID kèm trong trường Cookie ở phần đầu gói yêu cầu HTTP. Server nhận session ID này và so sánh với các session ID đã lưu. Nếu bằng nhau thì việc xác thực xem như hoàn tất. Như vậy session ID đóng vai trò mã xác thực thay cho thông tin đăng nhập tài khoản.
Ngoài thông tin session ID, cookie còn chứa các thông tin khác như URL dịch vụ và thời hạn sử dụng cookie. Nếu không có thời hạn sử dụng, khi người dùng đóng trình duyệt, thông tin cookie cũng sẽ mất đi. Sau này truy cập dịch vụ buộc phải đăng nhập lại. Thời hạn sử dụng sẽ giúp cho việc xác thực được duy trì mãi cho đến khi hết hạn hoặc server dịch vụ vì lý do gì đó mất thông tin session tương ứng. Để tránh việc người dùng phải đăng nhập nhiều lần, đôi khi cookie lưu cả thông tin tài khoản của người dùng để khi cần phải đăng nhập thì sử dụng luôn thay vì yêu cầu người dùng nhập lại. Việc này là vô cùng nguy hiểm vì có nhiều cách đọc được thông tin cookie trên trình duyệt dẫn đến lộ thông tin đăng nhập tạo ra lỗi bảo mật nghiêm trọng.
Thông thường server sẽ lưu session trong một bảng băm (hash) với khóa là session ID. Giá trị của nó thường chứa thông tin tài khoản cần xác thực. Mỗi khi đăng nhập thành công, server chưa vội tạo ngay session mới mà tìm xem trong bảng băm đã lưu có session nào chứa thông tin tương ứng chưa. Nếu có rồi thì chỉ việc trả session ID này cho client. Khi không có, server mới tạo session mới.
Để gửi session ID về cho client, server sẽ gửi trong trường Cookie của gói phản hồi HTTP. Package net/http cung cấp hàm SetCookie để thực hiện việc này như sau:
http.SetCookie(w ResponseWriter, cookie *Cookie)
Tham số thứ 2 là con trỏ đối tượng struct Cookie:
type Cookie struct {
Name string
Value string
Path string
Domain string
Expires time.Time
RawExpires string
MaxAge int
Secure bool
HttpOnly bool
Raw string
Unparsed []string
}
Ví dụ việc gửi cookie như sau:
expiration := time.Now().Add(365 * 24 * time.Hour) // Tồn tại trong 1 năm
cookie := http.Cookie{Name: "sessionid", Value: "58048e705babd15cd91c4f9f", Expires: expiration}
http.SetCookie(w, &cookie)
Việc lưu trữ ở cookie gây ra nhiều lỗi bảo mật nên một số trình duyệt cho phép người dụng chặn việc sử dụng cookie. Trong tình huống này thì session ID có thể được trao đổi bằng cách đưa nó vào tham số trong địa chỉ URL. Tuy nhiên việc này cũng nguy hiểm khi mà session ID lại để lộ trên địa chỉ trình duyệt, rất dễ bị đối tượng xấu sử dụng để xác thực tài khoản người dùng.
Ngay cả khi truyền session ID qua cookie thì việc lộ session ID cũng có thể xảy ra nếu không sử dụng kết nối bảo mật HTTPS. Kẻ xấu có thể lợi dụng session ID này để xác thực. Trong tình huống này chúng ta có 2 cách sau để tránh bị tấn công kiểu này:
- Dùng giá trị bí mật: giá trị này nếu tồn tại sẽ cho biết client đã được đăng nhập thành công trước đó. Dựa trên giá trị này, dịch vụ sẽ xác thực cho client. Bên khác muốn xác thực với session ID đúng nhưng không có giá trị này cũng sẽ bị yêu cầu đăng nhập lại.
- Làm mới session ID: Giá trị session ID được làm mới trong khoản thời gian ngắn và gửi lại cho client dùng để xác thực các lần tới. Làm như vậy nhiều khả năng bên có được session ID chưa kịp sử dụng thì nó đã cũ mất rồi.
Xác thực với token
Xác thực với session và cookie gặp nhiều bất tiện như việc client chặn cookie hay việc duy trì lưu thông tin client trên server khiến cho server không còn phi trạng thái và khó mở rộng khi phục vụ số lượng client lớn cũng như khó khăn khi client truy cập server phân tán khác qua ajax do ràng buộc cấm truy cập khác tên miền (CORS) giữa client và server khi sử dụng cookie.
Phương pháp xác thực với token ra đời giúp giải quyết vấn đề trên. Theo hướng này, sau khi đăng nhập thành công, server sẽ tạo ra một chuỗi (gọi là token) duy nhất gửi về cho client. Những yêu cầu sau đó từ client, chuỗi token này đều được để trong trường Authorization. Server dùng nó để xác thực người dùng. Mới nghe thấy nó tương tự như session ID thôi chứ đâu có gì khác biệt nhỉ? Đúng, vì chúng cùng là mã xác thực cả. Điểm đặc biệt ở đây là chuỗi token này server không có lưu, chỉ client là phải lưu. Mỗi khi nhận được, server sẽ giải mã chuỗi và từ đó xác định được tài khoản người dùng tương ứng.
Như vậy xác thực với token đảm bảo server phi trạng thái vì không lưu bất kỳ thông tin kết nối gì của client cả. Nó cũng giúp cho việc xác thực dễ dàng khi server là một hệ thống phân tán bởi lẽ mỗi server trong hệ thống phân tán tự xác thực được mà không cần lưu thông tin hay hỏi lại server đã xác thực trước đó cho client này. Trong bối cảnh ứng dụng di động phát triển và dịch vụ cung cấp API ngày càng phát triển thì phương pháp này càng tỏ ra hữu dụng. Trong phần tiếp theo chúng ta sẽ tìm hiểu một phương pháp xác thực với token là JWT.
Xác thực với Json Web Token (JWT)
Sau khi người dùng đăng nhập thành công, server sẽ sinh ra một chuỗi JWT rồi gửi về cho client. Client nhận được và lưu chuỗi này lại trong cookie hoặc bộ nhớ cục bộ của trình duyệt hoặc vùng nhớ ứng dụng. Mỗi khi có yêu cầu lên server cần xác thực, client lại đóng gói JWT vào trường Authorization phần đầu gói yêu cầu HTTP với nội dung: Bearer <nội dung chuỗi JWT>. Server nhận được tự phân giải để duy trì xác thực người dùng.
Cấu trúc JWT gồm 3 phần có dạng như sau: xxxx.yyyy.zzzz
- Phần đầu (header): là một cấu trúc JSON gồm 2 trường là loại token (typ) và loại thuật toán mã hóa (alg). Ví dụ:
{
"alg": "HS256",
"typ": "JWT"
}
Cấu trúc này sau đó được mã hóa với thuật toán base64 phù hợp chuyển qua URL(base64UrlEncode). Thuật toán này y như base64, chỉ là đổi vài chỗ cho phù hợp khi dùng trong URL: không thêm dấu = và thay + bằng -, thay / bằng _. Sau khi mã hóa ta sẽ có phần đầu của JWT, tức xxxx trong chuỗi mô tả ở trên.
- Phần nội dung (payload): nội dung chứa các trường thông tin (claim) gồm 3 nhóm:
+ Reserved: những trường được quy định sẵn tại danh mục IANA JSON Web Token Claims.
- iss (issuer): tổ chức phát hành token.
- sub (subject): chủ đề của token.
- aud (audience): đối tượng sử dụng token.
- exp (expired time): thời điểm token sẽ hết hạn, giá trị giờ Unix (tổng số giây tính từ bắt đầy ngày 1/1/1970).
- nbf (not before time): token sẽ chưa hợp lệ trước thời điểm này, giá trị giờ Unix.
- iat (issued at): thời điểm token được phát hành, giá trị giờ Unix.
- jti: định danh (ID) JWT.
+ Public: là những trường được công nhận và sử dụng rộng rãi hoặc URI không bị trùng.
+ Private: trường quy định riêng của từng dịch vụ, không trùng với trường thuộc 2 nhóm trên.
Ví dụ một payload như sau:
{
“exp”: 1444478400,
“user”: “root”,
“role”: "admin"
}
Cấu trúc JSON phần nội dung cũng sẽ được mã hóa base64UrlEncode. Đó là phần yyyy trong JWT.
- Phần chữ ký (Signature): là chuỗi được tạo thành từ phần đầu và phần nội dung. Các phần này ngăn cách nhau bởi dấu chấm (.). Sau đó chuỗi này được mã hóa với thuật toán đã nêu ở phần đầu, ta có chuỗi zzzz:
HS256(base64UrlEncode(header) + "." + base64UrlEncode(payload), <khóa bí mật>)
Chi tiết hơn về JWT, các bạn có thể tham khảo tại đây. Bây giờ chúng ta sẽ tìm hiểu cách tạo ra JWT và dùng nó xác thực người dùng trong Go. Có nhiều package hỗ trợ JWT trong Go. Ở đây tôi sử dụng dgrijalva/jwt-go:
- Khai báo package sử dụng và cấu trúc claim:
+ Package sử dụng: để tiện sử dụng tôi đổi tên package lại thành jwt:
import (+ Cấu trúc mô tả các claim phần nội dung:
"fmt"
"log"
"time"
jwt "github.com/dgrijalva/jwt-go"
)
type MyClaims struct {
Username string `json:"username"`
Role string `json:"role"`
jwt.StandardClaims
}
Phần Reserved được mô tả trong jwt.StandardClaims, được nhúng thẳng vào struct.
+ Tạo một JWT:
secret := []byte("secrethmac256jwt")
claims := MyClaims{
"root",
"admin",
jwt.StandardClaims{
Issuer: "pilo",
ExpiresAt: time.Date(2017, 1, 1, 0, 0, 0, 0, time.UTC).Unix(),
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
fmt.Println(token.Header)
fmt.Println(token.Claims)
tokenString, err := token.SignedString(secret)
if err != nil {
log.Fatal("Lỗi tạo JWT:", err.Error())
}
fmt.Println(tokenString)
Kết quả được in ra như sau:
map[typ:JWT alg:HS256]
{root admin { 1483228800 0 pilo 0 }}
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6InJvb3QiLCJyb2xlIjoiYWRtaW4iLCJleHAiOjE0ODMyMjg4MDAsImlzcyI6InBpbG8ifQ.YEzl9owT6Rm-Ul8Zp-DJn7aPBws6pX6G-1bmarYyvHU
- Gửi JWT về cho client sau khi đăng nhập thành công:
+ Cấu trúc JWT:
type JWT struct {
Token string `json:"token"`
}
+ Đóng gói jwt gửi về client ở phần thân gói phản hồi:
tokenstr := JWT {tokenString}
json, err := json.Marshal(tokenstr)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
w.Header().Set("Content-Type", "application/json")
w.Write(json)
với w là tham số kiểu http.ResponseWriter.
- Client gửi token JWT xác thực:
req, err := http.NewRequest("GET", "http://<địa chỉ server>:<cổng kết nối>/<đường dẫn tài nguyên yêu cầu>", nil)
if err != nil {
log.Fatal("Lỗi kết nối:", err.Error())
}
req.Header.Set("Authorization", fmt.Sprintf("Bearer %v", tokenString))
res, err := http.DefaultClient.Do(req)
...
Hàm NewRequest thuộc package net/http giúp tạo yêu cầu HTTP:
func NewRequest(method, urlStr string, body io.Reader) (*Request, error)
với tham số đầu là phương thức kết nối GET/POST/PUT/DELETE, tham số thứ 2 URL đến tài nguyên cần yêu cầu, tham số thứ 3 tùy chọn, thường là nil. Giá trị trả về đối tượng http.Request phù hợp làm tham số cho http.Client.Do() để thực thi yêu cầu. Ta sử dụng biến http.Request để bổ sung phần đầu yêu cầu như thêm giá trị token vào trường Authorization như bên trên.
- Xác thực người dùng với JWT từ client:
token, err = jwt.ParseWithClaims(tokenString, &MyClaims{}, func(token *jwt.Token) (interface{}, error) {
return secret, nil
})
if claims, ok := token.Claims.(*MyClaims); ok && token.Valid {
fmt.Printf("%v %v %v", claims.Username, claims.Role, claims.StandardClaims.ExpiresAt)} else {
fmt.Println(err)
}
Hàm jwt.ParseWithClaims sẽ phân giải chuỗi JWT nhận được thành đối tượng struct Token:
func ParseWithClaims(tokenString string, claims Claims, keyFunc Keyfunc) (*Token, error)Keyfunc là kiểu hàm cung cấp khóa bí mật giúp cho việc phân tích:
type Token struct {
Raw string // Raw token, được tạo ra khi phân tích token
Method SigningMethod // Thuật toán mã hóa
Header map[string]interface{} // Phần đầu của token
Claims Claims // Phần thông tin
Signature string // Phần chữ ký
Valid bool // True khi token hợp lệ
}
Ngoài chức năng xác thực thì JWT còn có thể dùng để trao đổi dữ liệu mã hóa giữa các bên với nhau. Tuy nhiên do mã hóa base64 là mã hóa đối xứng dễ dàng giải mã nên JWT chỉ dùng để chuyển thông tin không quá bảo mật nhưng cần đảm bảo nội dung không bị sai lệch, thay đổi trong lúc chuyển. Tính năng này có được nhờ phần chữ ký của JWT. Nó giúp kiểm tra tính đúng đắn của dữ liệu truyền.type Keyfunc func(*Token) (interface{}, error)
Trong bài tới chúng ta sẽ tìm hiểu về cấp quyền người dùng với OAuth 2.0 trong Go.
Tóm tắt:
- Tài khoản người dùng chứa thông tin người dùng, đặc biệt là thông tin xác thực. Dịch vụ web sử dụng cặp ID và mật khẩu là thông tin xác thực.
- Sau khi đăng nhập thành công, client không gửi thông tin đăng nhập lên server nữa mà gửi mã xác thực theo phương pháp session-cookie hoặc token.
- Cookie lưu session ID do server gửi về sau khi đăng nhập thành công vào trình duyệt và gửi lại cho server ở trường Cookie khi cần xác thực. Session được server tạo ra và lưu trữ sau khi client đăng nhập thành công. Server so sánh session ID client gửi và cái lưu trong session để có cơ sở xác thực.
- Server sinh token rồi trả về client lưu chứ server không lưu. Khi nhận lại token, server tự giải mã nó để xác định thông tin người dùng tương ứng.
- JWT là loại token được tạo thành dựa trên 3 thành phần là các chuỗi JSON đã mã hóa. JWT được dùng phổ biến làm mã xác thực trong phương pháp xác thực với token.
- JWT đảm bảo sự toàn vẹn dữ liệu nên ngoài việc được dùng cho xác thực thì còn được dùng để trao đổi dữ liệu.
No comments:
Post a Comment