1个不稳定版本
0.0.1 | 2021年4月5日 |
---|
#24 在 #backends
60KB
326 行
概览
设计Web后端的一种方式是将它分为三个部分:API层(用于表示层中可重用的操作),数据层(用于与数据库交互),和表示层(用于处理请求)。这个crate旨在提供认证的API层。示例路由在示例文件夹中提供,因此您也可以获得表示层,尽管您几乎肯定会想要进行一些更改,因此它默认不包含在crate中。
功能
- 简单:通过提供结构和为您完成困难的部分,使将认证添加到您的Web应用程序变得容易
- 灵活:对您的URL结构、UI或数据库的外观没有见解
- 快速:完全异步,除非登录或注册(例如,正常认证请求无需数据库查询即可验证)
- 无状态:此库不需要服务器固定,仅使用密码学来验证会话,因此如果用户偶尔切换后端服务器,这没问题
使用方法
大致来说,使用此库有三个步骤:创建数据层,与Actix-Web集成,创建路由,以及创建前端。您可以在此处查看一个完整的示例 这里。
创建数据层
要创建数据层,您必须创建一个Rust结构来存储您想要与每个用户关联的信息,除了密码哈希存储在单独的位置之外。您必须在此结构中存储电子邮件。以下是一个示例结构
// !!!!!WARNING: anything in your account type is visible to users as it is encoded as a JWT!!!!!
#[derive(Serialize, Deserialize, Debug)]
pub struct ExampleAccount {
pub username: String, //example of custom data to include with one's account type
pub email: String,
}
impl Account for ExampleAccount {
fn email(&self) -> &str {
&self.email
}
}
如您所见,在创建结构体之后,实现了 Account
特性,以便 actix-plus-auth 可以访问电子邮件,并指示此结构体代表一个账户。由于 JWT(Json Web Token)序列化和反序列化,Account
特性需要实现 serde 的 Serialize
和 DeserializeOwned
特性。要实现 DeserializeOwned
,只需创建一个拥有所有其类型(例如,没有引用成员)的结构体,然后派生 Deserialize
。此外,Account
特性还需要 Debug
特性。当然,您可以根据需要实现其他特性。
请注意,由于您的账户类型作为 JWT 编码用于会话存储,账户类型中的任何内容都对用户可见。请勿在账户类型中放入私人信息。
一旦创建账户结构体,就需要一个数据提供者结构体。数据提供者结构体实现了从数据库中获取和存储此账户类型操作的函数。要创建数据提供者结构体,只需实现 DataProvider
特性
#[async_trait]
pub trait DataProvider: Clone {
type AccountType: Account;
///Adds a new account to the database, then returns that account back. You may need to clone the account when implementing this function. If another account exists with this email, then the function should return some sort of error.
async fn insert_account(
&self,
account: Self::AccountType,
password_hash: String,
) -> ResponseResult<Self::AccountType>;
/// Fetches the account with the given email from the database, case-insensitively. Note that a lowercase email should be passed to this function, but the matching email as stored in the database may be in any case.
async fn fetch_account(
&self,
email: &str,
) -> ResponseResult<Option<(Self::AccountType, String)>>;
}
请注意,使用了 async_trait 包来简化此特性中的异步函数,在实现此特性时也必须使用它。另外,请注意,insert_account
和 fetch_account
函数接收 &self
而不是 &mut self
,并且 DataProvider
需要 Clone
。此库中的模型是一个数据存储在多个数据提供者实例之间共享,这些实例的引用类似于正常的数据库连接池(例如,在 sqlx 中)。当您的数据提供者被克隆时,请确保内部引用相同的数据,以便如果对克隆进行了更改,则可以从原始(或其他克隆)中读取,反之亦然。实现此功能的最有效方法是使用已经以这种方式工作的数据库库(例如 sqlx)。
恭喜!您现在已经创建了数据层,这是使用此库所需工作的95%。
由于不同数据库后端的差异很大,这里没有展示示例数据提供者。
与 Actix-Web 集成
在创建数据层之后,您可以继续与 Actix Web 集成。为此,通过 new
函数创建 actix_plus_auth::AuthenticationProvider<T: DataProvider>
的一个实例。为此,您需要一个密钥和您的数据提供者实例
let auth = AuthenticationProvider::new(
MyDataProvider::new(),
"some secret, you should use a real one"
.as_bytes()
.iter()
.map(|u| *u)
.collect(),
);
密钥用于验证 JWT,因此它应该是保密的(如果泄露,则用户可以为您的服务中的任何账户伪造 JWT)且不可更改的(如果更改,现有会话将无效)。如果泄露,您应该更改它,并检查日志以查找任何可能的滥用。《AuthenticationProvider》是通用的,用于提供实现 DataProvider
的结构体,并且它将根据您的 DataProvider
实现推断账户类型,也是通用的。
一旦您有了 AuthenticationProvider
,您只需将其提供给每个路由即可。这是通过 Actix Web 的状态系统 来实现的
HttpServer::new(move || { //move your auth variable into the closure
App::new()
.data(auth.clone()) //clone the closure's copy for each Actix Web worker, this is why clones of a data provider must refer to the same data even when cloned
})
最后,由于该系统依赖于设置了安全标志的cookie,您必须启用TLS/HTTPS才能使其工作。自签名证书对于开发来说已经足够简单。有关启用TLS的详细信息,请参阅Actix Web文档。
创建路由
一旦将AuthenticationProvider
注册到Actix Web中,就必须创建登录、注册和注销的路由。AuthenticationProvider
提供了方便这些操作的大多数重载功能,除了注销需要用户完全实现。
注册
pub async fn register(
&self,
account: DataProviderImpl::AccountType,
password: &str,
) -> ResponseResult<RegistrationOutcome<DataProviderImpl::AccountType>> {
上面是注册函数的签名。简单来说,它接受认证提供者的引用、要注册的密码以及您的账户类型实例。然后返回一个带有RegistrationOutcome
实例的ResponseResult
(来自actix-plus-error crate,用于传播内部服务器错误,例如来自用户提供的底层的数据层错误),其中RegistrationOutcome
有三个变体
///The non-error outcomes of registration. Error outcomes are used when a genuine error takes place — e.g. the database is not reachable (represented by the functions on your DataProvider implementation returning an error).
pub enum RegistrationOutcome<AccountType: Account> {
///The account is now in the database, and is given here.
Successful(AccountType),
///The provided email is not a valid email
InvalidEmail,
//The provided email is already taken
EmailTaken,
}
登录
pub async fn login(
&self,
email: &str,
password: &str,
) -> ResponseResult<LoginOutcome<DataProviderImpl::AccountType>>
上面是登录函数的签名。简单来说,它接受认证提供者的引用、电子邮件和用户名(由用户提供)并返回一个带有LoginOutcome
实例的ResponseResult
(来自actix-plus-error crate,用于传播内部服务器错误,例如来自用户提供的底层的数据层错误),其中LoginOutcome
有两个变体
///The non-error outcomes of logging in. Error outcomes are used when a genuine error takes place — e.g. the database is not reachable (represented by the functions on your DataProvider implementation returning an error).
pub enum LoginOutcome<AccountType> {
///The credentials were correct, so the account and a cookie that should be set in the response to the login route are provided.
Successful(AccountType, Cookie<'static>),
//The provided credentials do not correspond to a valid account.
InvalidEmailOrPassword,
}
如果返回了InvalidEmailOrPassword
,则应按您认为合适的方式在响应中传递此信息。如果返回了Successful
,则应在您的HTTP响应中设置提供的cookie。此外,在成功的场景中,您还可以设置其他cookie以与前端共享数据。请注意,根据基于cookie的令牌存储的最佳实践,令牌cookie仅适用于HTTP,因此JavaScript代码无法访问它。有关示例,请参阅这里。
注销
要注销,只需删除actix-plus-auth-token
cookie以及您在登录中设置的任何其他cookie。您不需要调用库来删除会话或任何内容,因为库是无状态的。
#[post("/logout")]
async fn logout(request: HttpRequest) -> Response {
let mut response = HttpResponse::Ok();
if let Some(mut session_cookie) = request.cookie("actix-plus-auth-token") { //you must delete this cookie
session_cookie.set_path("/"); //this is needed to ensure that the cookie deletion goes through
session_cookie.set_secure(true); //this is needed to ensure that the cookie deletion goes through
response.del_cookie(&session_cookie);
}
if let Some(mut username_cookie) = request.cookie("username") { //delete optional user-added cookie
username_cookie.set_path("/");
username_cookie.set_secure(true);
response.del_cookie(&username_cookie);
}
Ok(response.await?)
}
在其他路由中进行认证
在另一个路由中,您可以直接在AuthenticationProvider
上调用current_user(req: &HttpRequest)
来获取当前用户。此函数返回编码为401未授权(通过actix-plus-error crate)的Err(ResponseError)
,因此如果您使用相同的crate,您可以使用?
简单传播以保持代码简洁。
#[get("/private_page")]
async fn private_page(request: HttpRequest, auth: Data<ExampleAuthProvider>) -> Response {
let account = auth.current_user(&request)?;
Ok(HttpResponse::Ok() //you can do anything here, including more traditional JSON/REST/etc. routes
.body(format!("Hello {}", account.username))
.await?)
}
创建前端
在制作前端时,需要确保HTTP请求中包含cookie。具体来说,凭证应设置为'same-origin'(对于登录、注销和注册的请求,以及一般的已认证请求)。
let response = await fetch('/login', {
method: 'POST',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
email: document.getElementById('email').value,
password: document.getElementById('password').value,
})
});
待办事项
- 电子邮件验证
- SQLx示例
依赖项
~37MB
~839K SLoC