如何避免Plumber过滤器在返回404前拦截不存在的路由?
我用Plumber开发内部API,该API由负责所有认证的公共API接入。我们通过请求头获取用户角色:100=普通用户、101=管理员、102=超级管理员。
API里有多个含敏感数据的端点,部分仅限管理员/超级管理员访问,甚至有些只对超级管理员开放。目前用过滤器和前置过滤器(preempt filter)控制端点访问权限。
当前问题:调用不存在的端点时,只有超级管理员的请求能返回404(因为其请求不会被过滤器前置拦截),普通用户和管理员的请求会被过滤器拦截,无法触发notFoundStep返回404。我们期望所有用户调用不存在的端点时都返回404。
我原本想在其他过滤器前加一个检查端点是否存在的过滤器,但过滤器里无法访问实际路由器,这个思路不可行。
请问有没有解决办法?这种多级权限控制的方式是否最优?我完全可以根据需求调整整体架构。
以下是复现问题的极简代码:
filter_authentication <- function(req, res) { if (is.na(req$HEADERS[["role"]])) { res$status <- 401 return("You are unauthenticated") } plumber::forward() } filter_admin <- function(req, res) { if (req$HEADERS[["role"]] < 101) { res$status <- 403 return("You lack permissions to view this") } plumber::forward() } filter_super_admin <- function(req, res) { if (req$HEADERS[["role"]] < 102) { res$status <- 403 return("You lack permissions to view this") } plumber::forward() } #' Creates the Plumber router #' #' @param pr A Plumber router make_router <- function(pr) { router <- pr |> plumber::pr_filter("filter_authentication", filter_authentication) |> plumber::pr_filter("filter_admin", filter_admin) |> plumber::pr_filter("filter_super_admin", filter_super_admin) |> plumber::pr_get( path = "/open_data", handler = function() { return("This is accessible to all users") }, preempt = "filter_admin" ) |> plumber::pr_get( path = "/admin_data", handler = function() { return("This is accessible to all admins") }, preempt = "filter_super_admin" ) |> plumber::pr_get( path = "/super_admin_data", handler = function() { return("This is accessible to all super admins") } ) return(router) } make_req <- function(verb, path, qs = "", body = "", headers = "", args = c()) { req <- new.env() req$REQUEST_METHOD <- toupper(verb) req$PATH_INFO <- path req$QUERY_STRING <- qs req$rook.input <- list( read_lines = function() { body }, read = function() { charToRaw(body) }, rewind = function() { length(charToRaw(body)) } ) req$HEADERS <- headers req$args <- args return(req) } # User pr <- make_router(plumber::pr()) req <- make_req("GET", "/admin_data", headers = c(role = 100)) res <- plumber:::PlumberResponse$new() pr$serve(req, res) # Admin req <- make_req("GET", "/super_admin_data", headers = c(role = 101)) res <- plumber:::PlumberResponse$new() pr$serve(req, res) # Super admin req <- make_req("GET", "/super_admin_data", headers = c(role = 102)) res <- plumber:::PlumberResponse$new() pr$serve(req, res) # Non-existing endpoint req <- make_req("GET", "/does_not_exist", headers = c(role = 102)) res <- plumber:::PlumberResponse$new() pr$serve(req, res) req <- make_req("GET", "/does_not_exist", headers = c(role = 101)) res <- plumber:::PlumberResponse$new() # This should also be 404 pr$serve(req, res)
一、解决404返回问题
核心思路是先判断端点是否存在,再执行权限校验,以下是两种可行实现方式:
方法1:在全局认证过滤器中先判断路由是否存在
修改认证过滤器,先通过路由器的match方法检查请求是否匹配现有路由,不匹配直接返回404:
filter_authentication <- function(req, res) { # 获取当前路由器实例 pr <- parent.frame()$pr # 匹配路由,判断是否存在对应端点 match_result <- pr$match(req) if (is.null(match_result$route)) { res$status <- 404 return("Endpoint not found") } # 原有认证逻辑 if (is.na(req$HEADERS[["role"]])) { res$status <- 401 return("You are unauthenticated") } plumber::forward() }
这种方式下,所有请求会先经过认证过滤器,不存在的端点直接返回404,不会进入后续权限过滤器。
方法2:改用路由级过滤器替代全局权限过滤器
放弃全局绑定filter_admin和filter_super_admin,改为给需要权限的路由单独绑定对应过滤器,不存在的路由不会触发任何权限校验,直接返回404:
make_router <- function(pr) { router <- pr |> plumber::pr_filter("filter_authentication", filter_authentication) |> plumber::pr_get( path = "/open_data", handler = function() { return("This is accessible to all users") } ) |> plumber::pr_get( path = "/admin_data", handler = function() { return("This is accessible to all admins") }, # 只为该路由绑定admin权限过滤器 filters = "filter_admin" ) |> plumber::pr_get( path = "/super_admin_data", handler = function() { return("This is accessible to all super admins") }, # 为该路由绑定admin+super_admin权限过滤器 filters = c("filter_admin", "filter_super_admin") ) return(router) }
此方式下,不存在的路由只会经过认证过滤器,配合方法1中的路由判断逻辑,即可正常返回404。
二、优化多级权限架构
当前用前置过滤器的方式权限逻辑分散,维护成本高,推荐以下优化方案:
1. 统一权限判断逻辑
创建通用权限检查函数,集中处理角色与权限等级的校验:
check_permission <- function(role, required_level) { role <- as.integer(role) !is.na(role) && role >= required_level }
2. 拆分专用权限过滤器
为不同权限等级创建独立过滤器,逻辑更清晰:
filter_require_admin <- function(req, res) { if (!check_permission(req$HEADERS[["role"]], 101)) { res$status <- 403 return("You lack permissions to view this") } plumber::forward() } filter_require_super_admin <- function(req, res) { if (!check_permission(req$HEADERS[["role"]], 102)) { res$status <- 403 return("You lack permissions to view this") } plumber::forward() }
3. 路由绑定简化
路由只需声明所需权限过滤器即可,无需依赖preempt参数:
make_router <- function(pr) { router <- pr |> plumber::pr_filter("filter_authentication", filter_authentication) |> plumber::pr_get("/open_data", function() "This is accessible to all users") |> plumber::pr_get( "/admin_data", function() "This is accessible to all admins", filters = "filter_require_admin" ) |> plumber::pr_get( "/super_admin_data", function() "This is accessible to all super admins", filters = c("filter_require_admin", "filter_require_super_admin") ) return(router) }
4. 自定义统一404响应
通过pr_set_error自定义404返回格式,让API响应更规范:
pr |> plumber::pr_set_error(404, function(req, res) { res$status <- 404 list(error = "请求的端点不存在", code = 404) })
内容的提问来源于stack exchange,提问作者alethor

