在上一篇文章中,我们研究了如何使用casbin在Go Web Service中实现基于角色的认证。在这篇文章中,我们将对Rust Web Service进行同样的操作。
在这个例子中,我们将定义两个用户角色member 和admin ,为每个角色创建一个用户,并使用基于 casbin 的认证过滤器来处理授权。
为了简单起见,认证机制只是基于用户的名字,使用一个硬编码的用户映射。
为了编写提供这种机制的Web服务,我们将在这里使用warp,但其背后的基本概念应该适用于任何Web框架,只是一些实现细节有不同程度的变化。
让我们开始吧!
免责声明:请不要把下面的示例代码作为生产级应用的模板,代码的重点在于清晰,而不是安全。
例子
我们将从定义一些类型开始。由于这是一个简单的例子,我们将在内存中保存我们的用户和会话。
mod handler;
const BEARER_PREFIX: &str = "Bearer ";
const MODEL_PATH: &str = "./auth/auth_model.conf";
const POLICY_PATH: &str = "./auth/policy.csv";
type UserMap = Arc<RwLock<HashMap<String, User>>>;
type Sessions = Arc<RwLock<HashMap<String, String>>>;
type WebResult<T> = std::result::Result<T, Rejection>;
type Result<T> = std::result::Result<T, Error>;
type SharedEnforcer = Arc<Enforcer>;
#[derive(Clone)]
pub struct UserCtx {
pub user_id: String,
pub token: String,
}
#[derive(Clone)]
pub struct User {
pub user_id: String,
pub name: String,
pub role: String,
}
#[derive(Error, Debug)]
pub enum Error {
#[error("error")]
SomeError(),
#[error("no authorization header found")]
NoAuthHeaderFoundError,
#[error("wrong authorization header format")]
InvalidAuthHeaderFormatError,
#[error("no user found for this token")]
InvalidTokenError,
#[error("error during authorization")]
AuthorizationError,
#[error("user is not unauthorized")]
UnauthorizedError,
#[error("no user found with this name")]
UserNotFoundError,
}
impl warp::reject::Reject for Error {}
好吧--这有很多的类型。我们首先定义casbin 模型和策略文件的路径,然后是用户和会话的内存存储的类型别名。UserMap 是user_id 到User 结构的映射,Sessions 地图是访问令牌到用户 ID 的映射。
我们还定义了一些Result 的别名,这样我们就可以区分来自网络处理程序的结果和内部结果。在类型别名之后,我们定义了User 和UserCtx 的结构。这个用户上下文是我们要传递给认证处理程序的,所以处理程序可以访问用户ID和认证令牌--我们将在后面看到为什么这很有用。
最后,我们定义了一个自定义的Error 类型,它实现了warp的Reject 特质,所以我们可以向用户返回漂亮的错误。
接下来,让我们看一下main 函数,看看应用程序的结构的大致情况。
#[tokio::main]
async fn main() {
let user_map = Arc::new(RwLock::new(create_user_map()));
let sessions: Sessions = Arc::new(RwLock::new(HashMap::new()));
let enforcer = Arc::new(
Enforcer::new(MODEL_PATH, POLICY_PATH)
.await
.expect("can read casbin model and policy files"),
);
let member_route = warp::path!("member")
.and(with_auth(
enforcer.clone(),
user_map.clone(),
sessions.clone(),
))
.and_then(handler::member_handler);
let admin_route = warp::path!("admin")
.and(with_auth(
enforcer.clone(),
user_map.clone(),
sessions.clone(),
))
.and_then(handler::admin_handler);
let login_route = warp::path!("login")
.and(warp::post())
.and(warp::body::json())
.and(with_user_map(user_map.clone()))
.and(with_sessions(sessions.clone()))
.and_then(handler::login_handler);
let logout_route = warp::path!("logout")
.and(with_auth(
enforcer.clone(),
user_map.clone(),
sessions.clone(),
))
.and(with_sessions(sessions.clone()))
.and_then(handler::logout_handler);
let routes = member_route
.or(admin_route)
.or(login_route)
.or(logout_route);
warp::serve(routes).run(([0, 0, 0, 0], 8080)).await;
}
让我们逐一来看看。首先,我们初始化上面提到的sessions 和user_map 。然后,重要的是,我们使用我们的模型和策略文件来初始化casbin::Enforcer 。这是一个基于请求方法、路径和用户ID来执行我们定义的授权规则的东西。我们把这个执行者放入一个Arc ,这样我们就可以把它的引用传递给其他线程。
之后,我们为login 、logout ,并为会员和管理员分别定义了一条路由,以查看授权方案是否有效。
模型和策略文件的定义如下。
// model.conf
[request_definition]
r = sub, obj, act
[policy_definition]
p = sub, obj, act
[policy_effect]
e = some(where (p.eft == allow))
[matchers]
m = r.sub == p.sub && keyMatch(r.obj, p.obj) && (r.act == p.act || p.act == "*")
// policy.csv
p, admin, /*, *
p, anonymous, /login, *
p, member, /logout, *
p, member, /member, *
这些定义是非常简单的。在模型中,我们定义了不同的对象、策略格式和匹配器。在策略中,我们实际上为subject 、object 和action 提供了具体的值--在我们的例子中,这些是user_role 、path 和request method 。
在warp中,我们使用Filter 系统来实现处理传入请求的机制,所以让我们创建一个新的Filter ,它使用casbin 来处理授权。
fn with_auth(
enforcer: SharedEnforcer,
user_map: UserMap,
sessions: Sessions,
) -> impl Filter<Extract = (UserCtx,), Error = Rejection> + Clone {
full()
.and(headers_cloned())
.and(method())
.map(
move |path: FullPath, headers: HeaderMap<HeaderValue>, method: Method| {
(
path,
enforcer.clone(),
headers,
method,
user_map.clone(),
sessions.clone(),
)
},
)
.and_then(user_authentication)
}
这个过滤器使用path::full() 过滤器获得完整的路径、头文件和请求方法。然后我们把这些值,连同执行者、用户映射和会话映射一起传递给user_authentication 函数,它看起来像这样。
async fn user_authentication(
args: (
FullPath,
SharedEnforcer,
HeaderMap<HeaderValue>,
Method,
UserMap,
Sessions,
),
) -> WebResult<UserCtx> {
let (path, enforcer, headers, method, user_map, sessions) = args;
let token = token_from_header(&headers).map_err(|e| warp::reject::custom(e))?;
let user_id = match sessions.read().await.get(&token) {
Some(v) => v.clone(),
None => return Err(warp::reject::custom(Error::InvalidTokenError)),
};
let user = match user_map.read().await.get(&user_id) {
Some(v) => v.clone(),
None => return Err(warp::reject::custom(Error::InvalidTokenError)),
};
match enforcer
.enforce(&[&user.role.as_str(), &path.as_str(), &method.as_str()])
.await
{
Ok(authorized) => {
if authorized {
Ok(UserCtx {
user_id: user.user_id,
token,
})
} else {
Err(warp::reject::custom(Error::UnauthorizedError))
}
}
Err(e) => {
eprintln!("error during authorization: {}", e);
Err(warp::reject::custom(Error::AuthorizationError))
}
}
}
fn token_from_header(headers: &HeaderMap<HeaderValue>) -> Result<String> {
let header = match headers.get(AUTHORIZATION) {
Some(v) => v,
None => return Err(Error::NoAuthHeaderFoundError),
};
let auth_header = match from_utf8(header.as_bytes()) {
Ok(v) => v,
Err(_) => return Err(Error::NoAuthHeaderFoundError),
};
if !auth_header.starts_with(BEARER_PREFIX) {
return Err(Error::InvalidAuthHeaderFormatError);
}
let without_prefix = auth_header.trim_start_matches(BEARER_PREFIX);
Ok(without_prefix.to_owned())
}
好吧,首先我们对参数进行结构化处理。然后,我们从Authorization 头中获取授权令牌,如果没有,或者无效,则返回错误。然后,我们检查是否有一个会话与头中的令牌存在。
如果没有,用户会得到一个错误,如果有,我们再进一步,看看该令牌所属的用户是否仍然存在。如果所有这些都成功了,我们就使用casbinEnforcer ,使用路径、请求方法和用户角色来查看用户是否可以访问请求的资源。如果这些操作中的任何一个失败了,就会返回一个错误,否则就会把令牌和用户ID转发给处理函数。
这基本上是整个授权的神奇之处,我们可以重新使用这个过滤器来保护端点,如上面的路由所示。
为了在处理程序中使用user_map 和sessions map,我们还需要为这些过滤器设置翘曲过滤器。
fn with_user_map(
user_map: UserMap,
) -> impl Filter<Extract = (UserMap,), Error = Infallible> + Clone {
warp::any().map(move || user_map.clone())
}
fn with_sessions(
sessions: Sessions,
) -> impl Filter<Extract = (Sessions,), Error = Infallible> + Clone {
warp::any().map(move || sessions.clone())
}
好吧。让我们创建一个硬编码的用户地图,这样我们就可以在以后测试整个机制。
fn create_user_map() -> HashMap<String, User> {
let mut map = HashMap::new();
map.insert(
String::from("21"),
User {
user_id: String::from("21"),
name: String::from("herbert"),
role: String::from("member"),
},
);
map.insert(
String::from("100"),
User {
user_id: String::from("100"),
name: String::from("sibylle"),
role: String::from("admin"),
},
);
map.insert(
String::from("1"),
User {
user_id: String::from("1"),
name: String::from("gordon"),
role: String::from("anonymous"),
},
);
map
}
有了这些,我们接下来看看处理程序的实现,从login 开始。
#[derive(Deserialize, Debug)]
pub struct LoginRequest {
pub name: String,
}
pub async fn login_handler(
body: LoginRequest,
user_map: UserMap,
sessions: Sessions,
) -> WebResult<impl Reply> {
let name = body.name;
match user_map
.read()
.await
.iter()
.filter(|(_, v)| *v.name == name)
.nth(0)
{
Some(v) => {
let token = Uuid::new_v4().to_string();
sessions
.write()
.await
.insert(token.clone(), String::from(v.0));
Ok(token)
}
None => Err(warp::reject::custom(Error::UserNotFoundError)),
}
}
为了登录一个用户,我们在user_map ,创建一个新的唯一的会话令牌(这里是uuid),并创建一个从该令牌到user_id的会话映射,将该令牌返回给用户。
注销和检查用户是否有会员或管理员权限的端点相当简单。
pub async fn logout_handler(user_ctx: UserCtx, sessions: Sessions) -> WebResult<impl Reply> {
sessions.write().await.remove(&user_ctx.token);
Ok("success")
}
pub async fn member_handler(user_ctx: UserCtx) -> WebResult<impl Reply> {
Ok(format!("Member with id {}", user_ctx.user_id))
}
pub async fn admin_handler(user_ctx: UserCtx) -> WebResult<impl Reply> {
Ok(format!("Admin with id {}", user_ctx.user_id))
}
注销本质上只是删除了会话映射。测试端点只是返回一个字符串,所以我们可以检查它们是否有效。检查授权的整个机制是在上面提到的Filter 层面上处理的。这很好,因为这意味着在处理程序中,我们根本不需要处理授权问题。
这就是了!在使用cargo run 运行应用程序后,我们可以使用cURL来测试它是否工作。
curl -X POST http://localhost:8080/login -d '{ "name": "herbert" }' -H "content-type: application/json"
=> $TOKEN
curl http://localhost:8080/member -H "authorization: Bearer $TOKEN" -H "content-type: application/json"
=> 200
curl http://localhost:8080/admin -H "authorization: Bearer $TOKEN" -H "content-type: application/json"
=> 200
curl -X POST http://localhost:8080/login -d '{ "name": "sibylle" }' -H "content-type: application/json"
=> $TOKEN
curl http://localhost:8080/member -H "authorization: Bearer $TOKEN" -H "content-type: application/json"
=> 200
curl http://localhost:8080/admin -H "authorization: Bearer $TOKEN" -H "content-type: application/json"
=> 401
curl -X POST http://localhost:8080/login -d '{ "name": "gordon" }' -H "content-type: application/json"
=> $TOKEN
curl http://localhost:8080/member -H "authorization: Bearer $TOKEN" -H "content-type: application/json"
=> 401
curl http://localhost:8080/admin -H "authorization: Bearer $TOKEN" -H "content-type: application/json"
=> 401
首先,我们以管理员身份登录,然后检查/member 和/admin 是否工作。然后,以会员身份登录,我们确保只有/member ,而不是/admin 。最后,既不是以管理员身份登录,也不是以会员身份登录,我们要确保/admin 和/member 都不能工作。
完整的示例代码可以在这里找到
总结
这篇文章展示了用于在Rust网络服务中进行授权的神奇的casbin库。casbin的好处是,它的总体概念被大量的语言所支持,并且有许多著名的网络服务器的中间件。这导致了更广泛的采用,在这个过程中,有更多的人在看代码,这就转化为更好的安全性。
casbin背后基于策略的方法既简单又强大,可用于实现许多不同的授权模式。