如何从shinyapps.io应用认证Plumber API调用?密钥认证是否安全?
Great question! Let's break this down clearly since you're building a private Shiny app on shinyapps.io that talks to a Plumber API—security here is critical to keep your data safe. I'll cover both your authentication implementation question and the security of using .Renviron keys in headers.
You have two solid approaches here, depending on whether you want to validate just the app itself or also individual users:
1. API Key Authentication (Your Proposed Method)
This is the simplest way to ensure only your Shiny app can call the Plumber API. Here's how to set it up:
Shiny App Side (shinyapps.io)
- Store your API key in a
.Renvironfile locally, but don't include this file in your deployment bundle. Instead, add the key directly in the shinyapps.io dashboard under Settings > Environment Variables (shinyapps.io encrypts these at rest). - When making API calls, pull the key with
Sys.getenv("PLUMBER_API_KEY")and include it in the request headers. Usehttr2(recommended overhttrfor modern workflows) to build authenticated requests:library(shiny) library(httr2) server <- function(input, output, session) { # Fetch the API key from environment variables api_key <- Sys.getenv("PLUMBER_API_KEY") # Example authenticated API call get_data <- reactive({ req <- request("https://your-plumber-api-url.com/data-endpoint") %>% req_headers(`X-API-KEY` = api_key) %>% # Custom header for API key req_perform() %>% resp_body_json() }) # Use the data in your app... }
Plumber API Side
Add a filter to validate the API key on every incoming request. This runs before any endpoint logic:
library(plumber) # Initialize your Plumber API pr <- plumb("your-plumber-file.R") # Add a validation filter pr$filter("validate_api_key", function(req, res) { # Fetch the expected key from your cloud platform's environment variables expected_key <- Sys.getenv("PLUMBER_API_KEY") # Get the key provided in the request header provided_key <- req$HTTP_X_API_KEY # Check if the key is missing or invalid if (is.null(provided_key) || provided_key != expected_key) { res$status <- 401 # Unauthorized return(list(error = "Invalid or missing API key")) } # If valid, proceed to the endpoint forward() }) # Run the API pr$run(host = "0.0.0.0", port = 8000)
2. User-Level Authentication (Leveraging shinyapps.io's Auth)
Since your Shiny app is private (users log in via shinyapps.io), you can extend this auth to the Plumber API to restrict access per user:
Shiny App Side
Get the logged-in user's details (shinyapps.io automatically sets this) and pass it to the API alongside the app's API key:
server <- function(input, output, session) { api_key <- Sys.getenv("PLUMBER_API_KEY") # Get the current user's email from shinyapps.io auth current_user <- shiny::getShinyOption("user")$email get_user_data <- reactive({ req <- request("https://your-plumber-api-url.com/user-specific-data") %>% req_headers( `X-API-KEY` = api_key, `X-USER-EMAIL` = current_user ) %>% req_perform() %>% resp_body_json() }) }
Plumber API Side
Add a filter that validates both the API key and the user's authorization (e.g., check if the user is in an approved list):
pr$filter("validate_user", function(req, res) { # First validate the app's API key expected_api_key <- Sys.getenv("PLUMBER_API_KEY") provided_key <- req$HTTP_X_API_KEY if (is.null(provided_key) || provided_key != expected_api_key) { res$status <- 401 return(list(error = "Invalid API key")) } # Then validate the user is allowed user_email <- req$HTTP_X_USER_EMAIL allowed_users <- strsplit(Sys.getenv("ALLOWED_USERS"), ",")[[1]] # e.g., "user1@example.com,user2@example.com" if (!user_email %in% allowed_users) { res$status <- 403 # Forbidden return(list(error = "User not authorized to access this resource")) } forward() })
This approach is safe for most use cases if you follow best practices, but there are important caveats to consider:
Pros
- No hardcoding: Keys aren't stored in your codebase, so they won't leak if you push code to version control (like GitHub).
- shinyapps.io security: Environment variables on shinyapps.io are encrypted at rest, so they can't be accessed via your app's code or deployment logs.
- Simple to implement: No complex OAuth flows needed for small private apps.
Risks & Mitigations
- MITM attacks: If your Plumber API isn't served over HTTPS, the API key in headers can be intercepted in plaintext. Fix: Always use HTTPS for both your Shiny app (shinyapps.io uses HTTPS by default) and your Plumber API.
- Log exposure: Accidental logging of API requests in your Shiny app or Plumber API could leak the key. Fix: Disable verbose request logging, and never print the key in error messages or debug output.
- Static key limitations: If the key is compromised, anyone can access your API until you rotate it. Fix: Implement regular key rotation, and consider using short-lived keys (e.g., JWT tokens) if you need higher security.
- Scope: This method only validates that the request comes from your Shiny app—not which user is making it. If you need per-user permissions, combine it with the user-level auth approach above.
Final Verdict
For a private, small-to-medium scale app, using an environment-stored API key in HTTPS headers is a secure and practical choice. For enterprise-level security, you might want to upgrade to OAuth2 client credentials flow, but the API key method is a strong starting point.
内容的提问来源于stack exchange,提问作者vwrobel




