简介
本专题将带你使用 axum 和 gRPC 构建一个分布式的博客系统数据结构与Protobuf
本章对我们项目的数据结构和proto进行定义实现分类服务
本章我们实现分类服务,即 `category-srv`实现文章服务
本章将带你实现文章的 gPRC 服务。实现前台web服务
本章将通过使用 axum 调用分类和文章的 gRPC 服务,来实现博客前台Web服务实现管理员服务
本章我们将实现管理员服务实现后台管理web服务
本章将使用 axum 调用 gRPC 服务来实现后台管理的 web 服务安全与鉴权
本章将讨论使用jwt进行鉴权服务扩容、注册、发现和编排
本章将讨论服务管理相关的话题配置中心服务
本章讨论配置中心的实现总结
本专题试图通过一个分布式博客的案例来探讨使用 rust 实现 gRPC 微服务架构的可行性
安全与鉴权
- 73519
- 2022-09-23 22:33:25
本章将讨论使用jwt进行鉴权。
本章涉及的 crate 有:
JWT
本站的《漫游axum》曾经讨论过 JWT 以及在axum集成JWT。
本站的《漫游axum》曾经讨论过 JWT 以及在axum集成JWT。
#[derive(Debug, Deserialize, Serialize, Default)]
pub struct Claims {
pub id: i32,
pub email: String,
pub iss: String,
pub exp: usize,
}
pub struct Jwt {
pub secret: String,
pub exp: i64,
pub iss: String,
}
impl Jwt {
pub fn new(secret: String, exp: i64, iss: String) -> Self {
Self { secret, exp, iss }
}
pub fn new_claims(&self, id: i32, email: String) -> Claims {
Claims {
id,
email,
iss: self.iss.to_string(),
exp: self.calc_claims_exp(),
}
}
pub fn new_claims_with(&self, claims: Claims) -> Claims {
self.new_claims(claims.id, claims.email.clone())
}
fn calc_claims_exp(&self) -> usize {
(Utc::now() + Duration::seconds(self.exp)).timestamp_millis() as usize
}
fn secret_bytes(&self) -> &[u8] {
(&self.secret).as_bytes()
}
pub fn token(&self, claims: &Claims) -> Result<String, crate::Error> {
encode(
&Header::default(),
claims,
&EncodingKey::from_secret(self.secret_bytes()),
)
.map_err(crate::Error::from)
}
pub fn verify_and_get(&self, token: &str) -> Result<Claims, crate::Error> {
let mut v = Validation::new(jsonwebtoken::Algorithm::HS256);
v.set_issuer(&[self.iss.clone()]);
let token_data = decode(token, &DecodingKey::from_secret(self.secret_bytes()), &v)
.map_err(crate::Error::from)?;
Ok(token_data.claims)
}
}
后台管理 WEB 鉴权
handler::login
登录
pub async fn login(
Extension(state): Extension<Arc<AppState>>,
Form(frm): Form<form::Login>,
) -> Result<(StatusCode, HeaderMap), String> {
let condition = blog_proto::get_admin_request::Condition::ByAuth(ByAuth {
email: frm.email,
password: frm.password,
});
let mut admin = state.admin.clone();
let resp = admin
.get_admin(tonic::Request::new(blog_proto::GetAdminRequest {
condition: Some(condition),
}))
.await
.map_err(|err| err.to_string())?;
let repl = resp.into_inner();
let logined_admin = match repl.admin {
Some(la) => la,
None => return Err("登录失败".to_string()),
};
let claims = state.jwt.new_claims(logined_admin.id, logined_admin.email);
let token = state.jwt.token(&claims).map_err(|err| err.to_string())?;
let cookie = format!("axum_rs_token={}", &token);
Ok(redirect_with_cookie("/m/cate", Some(&cookie)))
}
登录成功后,生成 jwt token,并保存在 cookie 中。
middleware::auth
中间件
pub struct Auth(Claims);
#[async_trait]
impl<B> FromRequest<B> for Auth
where
B: Send,
{
type Rejection = String;
async fn from_request(
req: &mut axum::extract::RequestParts<B>,
) -> Result<Self, Self::Rejection> {
let state = req.extensions().get::<Arc<model::AppState>>().unwrap();
let headers = req.headers();
let claims = match cookie::get(headers, "axum_rs_token") {
Some(token) => state
.jwt
.verify_and_get(&token)
.map_err(|err| err.to_string())?,
None => return Err("请登录".to_string()),
};
Ok(Self(claims))
}
}
- 从 cookie 中获取 jwt token
- 验证此token
main()
#[tokio::main]
async fn main() {
let addr = env::var("ADDR").unwrap_or("0.0.0.0:59527".to_string());
let jwt_secret =
env::var("JWT_SECRET").unwrap_or("PRFw6DQuWfFSQZjuUCnCeLhLXfWetA3r".to_string());
let jwt_iss = env::var("JWT_ISS").unwrap_or("axum.rs".to_string());
let jwt_exp = env::var("JWT_EXP").unwrap_or("120".to_string());
let jwt_exp = jwt_exp.parse().unwrap_or(120);
let cate = CategoryServiceClient::connect("http://127.0.0.1:19527")
.await
.unwrap();
let topic = TopicServiceClient::connect("http://127.0.0.1:29527")
.await
.unwrap();
let admin = AdminServiceClient::connect("http://127.0.0.1:49527")
.await
.unwrap();
let tera = Tera::new("blog-backend/templates/**/*.html").unwrap();
let jwt = Jwt::new(jwt_secret, jwt_exp, jwt_iss);
let m_router = Router::new()
.route("/cate", get(handler::list_cate))
.route(
"/cate/add",
get(handler::add_cate_ui).post(handler::add_cate),
)
.layer(axum::middleware::from_extractor::<middleware::Auth>());
let app = Router::new()
.nest("/m", m_router)
.route("/", get(handler::index))
.route("/login", get(handler::login_ui).post(handler::login))
.route("/logout", get(handler::logout))
.layer(Extension(Arc::new(model::AppState {
tera,
admin,
cate,
topic,
jwt,
})));
axum::Server::bind(&addr.parse().unwrap())
.serve(app.into_make_service())
.await
.unwrap();
}
现在,main()
函数添加了几个 jwt 相关的配置,它们都是从环境变量中读取的。
特别地,监听地址也变成了从环境变量中读取
将分类服务、文章服务和管理员服务的地址从硬编码改成从环境变量读取。
我们的后台管理WEB服务增加了jwt token的鉴权,但 gRPC 还没有加任何鉴权。
对于本专题而言,gPRC 不是必须的,因为所有 gRPC 都监听 127.0.0.1
,同时,能假设确保所有服务都是自己开发的,知道哪些是后台管理才可以调用的。
安全
本专题中所有服务的通讯都是使用非安全的协议,为了数据传输的安全,你可能需要 TLS
鉴权
本专题中,gRPC 没有任何鉴权,都是建立在web开发者知道哪些 gRPC 可以公开调用,哪些需要后台验证才能调用。试想,如果有程序员在前台WEB中调用 gPRC 创建管理员,结果将是灾难性的。