Ở bài viết trước mình đã mô tả về Jwt, tiếp theo đây hãy cùng tạo ra 1 function login đơn giản
1. Gem Jwt
gem "jwt"
Được recommended trên jwt.io, hỗ trợ đầy đủ mọi tính năng để hoàn thiện đoạn mã Jwt:
Cơ bản sẽ focus vào 3 yếu tố:
- payload
- SECRET_KEY_BASE
- algorithm
Đến với payload, gửi lên những thông tin để xác thực user Ex: role, email, login_token (tương tự như session mà user đó đang sử dụng)
def payload
{
user_id: ...,
login_token: ...,
role: ...,
email: ...,
iat: ...,
exp: ...
}
end
Về phần SECRET_KEY_BASE ứng với secret_key của Jwt, chuỗi này sẽ được gen tự động trong Rails app mỗi khi khởi tạo và được sở hữu bởi người dựng app (gồm 1 cặp file master.key và credentials.yml.enc).
Lưu ý:
Tuy nhiên nếu sử dụng function Rails.application.secret_key_base để lấy chuỗi secret_key_base thì lên production sẽ bị lỗi do cặp file nhắc tới trước đó sẽ không tồn tại.
Dựa vào function được định nghĩa trên github, secret_key_base sẽ ưu tiên sử dụng biến ENV trước nên sẽ bớt phức tạp và không cần phải config lại production nếu sử dụng biến ENV["SECRET_KEY_BASE"] ở mọi môi trường.
Cuối cùng là algorithm, tùy vào yêu cầu của mỗi dự án mà chọn 1 algorithm thích hợp Ex: HS256, ES512, ...
def access_token
JWT.encode payload, ENV["SECRET_KEY_BASE"], Settings.jwt.algorithm
end
2. Trích xuất Jwt-Authorization từ request headers
JwtAuthorization, Jwt-Authorization hoặc bất kì cái tên nào bạn nghĩ ra và phù hợp (Hạn chế sử dụng Authorization sẽ gây ra nhầm lẫn với Basic Authen). Trong ví dụ dưới đây, key mình chọn là Jwt-Authorization ( Rails config exception )
def load_jwt_token
header = request.headers["HTTP_JWT_AUTHORIZATION"] # Header được extract sẽ có dạng HTTP_xxx
raise UnauthorizedRequest unless header.is_a?(String)
jwt_authorization = header.split
valid_token_type = jwt_authorization.first == "Bearer" # Token type
jwt_token = jwt_authorization.last # Chuỗi Jwt
raise UnauthorizedRequest unless valid_token_type && jwt_token.present?
jwt_token
end
Tiếp theo là bước verify lại token mình vừa nhận được và load info định danh của request đó:
def decoded_token
@decoded_token ||= JWT.decode token, ENV["SECRET_KEY_BASE"], true, {algorithm: Settings.jwt.algorithm}
end
Trả về của decoded_token sẽ là payload được truyền vào
def load_request_info
@current_user = User.find_by id: decoded_token[0]["user_id"]
@current_session = current_user.user_logins.find_by login_token: decoded_token[0]["login_token"]
# Ứng với trường hợp login nhiều device -> login_token được lưu ở bảng trung gian gọi là user_login
# Bên cạnh đó việc lưu login_token ở bảng trung gian sẽ đơn giản hơn cho việc mở rộng thành login dưới dạng 1 device
# Tương tự với cách vận hành của gem "doorkeeper", khi user login vào app ta sẽ revoke tất cả các sessions trước đó
end
Trong quá trình decode nếu có lỗi xảy ra sẽ được raise lên dưới mã JWT::DecodeError. Trong một vài trường hợp cần phân biệt rõ ràng giữa các lỗi như là Jwt::ExpiredSignature, Jwt::DecodeError thì ta rescue như sau:
def load_request_info
...decoded_token...
rescue Jwt::ExpiredSignature
...
rescue Jwt::DecodeError
...
end
end
Lưu ý:
Không thể đặt Jwt::DecodeError lên đầu vì là class cha của Jwt::ExpiredSignature
3. Test với postman
Call API login với user đã generate từ trước ->
Sau đó mỗi lần client call request thì đính kèm Jwt-Authorization vào header ->
4. Kết luận
Happy coding! Make awesome thing today :v