Ruby中使用HTTParty集成外部API时存储JWT的最佳实践
Great question! When working with JWTs in Ruby—especially when integrating external APIs with HTTParty—there are several battle-tested practices for storing tokens, depending on your app’s architecture and token lifecycle. Let’s walk through the most common ones:
1. Environment Variables (Ideal for Long-Lived, Server-Side Tokens)
This is the go-to for production apps where the JWT is tied to your server’s identity (not individual users). Use the dotenv gem to keep tokens out of your codebase:
First, add dotenv-rails (or just dotenv for non-Rails Ruby apps) to your Gemfile, then create a .env file:
# .env (add this to .gitignore!) EXTERNAL_API_JWT="your-valid-jwt-token-here" CLIENT_ID="your-api-client-id" CLIENT_SECRET="your-api-client-secret"
Then reference it in your HTTParty service class:
class ExternalAPIService include HTTParty base_uri "https://api.your-external-service.com" def initialize @auth_header = { "Authorization" => "Bearer #{ENV['EXTERNAL_API_JWT']}" } end def fetch_resource(resource_id) self.class.get("/resources/#{resource_id}", headers: @auth_header) end end
Pro tip: Always keep .env out of version control to prevent accidental token leaks.
2. In-Memory Storage (For Short-Lived Tokens in Single-Process Apps)
If your JWT expires quickly (e.g., 15-30 minutes) and you’re running a single-process app (like a small Sinatra service), store the token in a class variable to avoid re-authenticating on every request. Add logic to check for expiration too:
require 'jwt' class ExternalAPIService include HTTParty base_uri "https://api.your-external-service.com" @@cached_jwt = nil def self.get_valid_jwt # Return cached token if it exists and isn't expired return @@cached_jwt if @@cached_jwt && !token_expired?(@@cached_jwt) # Fetch a new token if cache is empty or expired auth_response = post("/auth", body: { client_id: ENV['CLIENT_ID'], client_secret: ENV['CLIENT_SECRET'] }) @@cached_jwt = auth_response.parsed_response['access_token'] @@cached_jwt end private def self.token_expired?(token) decoded_payload = JWT.decode(token, nil, false)[0] Time.now.to_i > decoded_payload['exp'] rescue JWT::DecodeError true # Treat invalid tokens as expired end # Example request using the valid JWT def fetch_data headers = { "Authorization" => "Bearer #{self.class.get_valid_jwt}" } self.class.get("/data", headers: headers) end end
Note: This won’t work well in multi-process setups (like Puma in cluster mode) since each process maintains its own memory cache. For those cases, use a distributed cache.
3. Distributed Cache (For Multi-Process/Distributed Systems)
If you’re running multiple app servers or processes, use a shared cache like Redis or Memcached to store the JWT. This ensures all instances use the same token until it expires:
Using Redis with the redis-rb gem:
require 'redis' require 'jwt' class ExternalAPIService include HTTParty base_uri "https://api.your-external-service.com" REDIS_CLIENT = Redis.new(host: ENV['REDIS_HOST'], port: ENV['REDIS_PORT']) CACHE_KEY = "external_api_jwt" def self.get_valid_jwt cached_token = REDIS_CLIENT.get(CACHE_KEY) return cached_token if cached_token && !token_expired?(cached_token) # Fetch new token and cache it with matching expiration auth_response = post("/auth", body: { client_id: ENV['CLIENT_ID'], client_secret: ENV['CLIENT_SECRET'] }) new_token = auth_response.parsed_response['access_token'] decoded_payload = JWT.decode(new_token, nil, false)[0] expires_in_seconds = decoded_payload['exp'] - Time.now.to_i REDIS_CLIENT.setex(CACHE_KEY, expires_in_seconds, new_token) new_token end # ... token_expired? method same as before end
This keeps token fetching efficient across all your app instances.
4. Rails-Specific Options
If you’re building a Rails app, you have a couple of tailored choices:
- Rails Cache: Use the built-in
Rails.cache(configured to use Redis/Memcached in production) for a simpler approach:def self.get_valid_jwt Rails.cache.fetch(CACHE_KEY, expires_in: expires_in_seconds) do # Fetch new token logic here end end - Session/Cookies (User-Specific Tokens): If each user has their own JWT (e.g., after OAuth2 login), store it in the encrypted Rails session or secure cookies:
Rails sessions are encrypted by default, so this is safe for user-specific tokens.# In your controller after user auth session[:user_external_jwt] = auth_response['access_token'] # In your service class def initialize(user_jwt) @headers = { "Authorization" => "Bearer #{user_jwt}" } end
Critical Security Best Practices
- Never hardcode tokens: Keep secrets out of your codebase and version control.
- Encrypt sensitive storage: If you need to store tokens in a database (e.g., user-specific tokens), use a gem like
attr_encryptedto encrypt the value. - Validate expiration: Always check if a token is expired before using it to avoid unnecessary failed requests.
- Limit token scope: Request only the permissions your app needs when fetching the JWT—this minimizes damage if the token is compromised.
内容的提问来源于stack exchange,提问作者Brandon Teixeira

