简介
本专题将带你使用 axum 和 gRPC 构建一个分布式的博客系统数据结构与Protobuf
本章对我们项目的数据结构和proto进行定义实现分类服务
本章我们实现分类服务,即 `category-srv`实现文章服务
本章将带你实现文章的 gPRC 服务。实现前台web服务
本章将通过使用 axum 调用分类和文章的 gRPC 服务,来实现博客前台Web服务实现管理员服务
本章我们将实现管理员服务实现后台管理web服务
本章将使用 axum 调用 gRPC 服务来实现后台管理的 web 服务安全与鉴权
本章将讨论使用jwt进行鉴权服务扩容、注册、发现和编排
本章将讨论服务管理相关的话题配置中心服务
本章讨论配置中心的实现总结
本专题试图通过一个分布式博客的案例来探讨使用 rust 实现 gRPC 微服务架构的可行性
实现后台管理web服务
本章将使用 axum 调用 gRPC 服务来实现后台管理的 web 服务。
pub struct AppState {
pub tera: Tera,
pub cate: CategoryServiceClient<tonic::transport::Channel>,
pub topic: TopicServiceClient<tonic::transport::Channel>,
pub admin: AdminServiceClient<tonic::transport::Channel>,
}
后台管理的 AppState
增加了管理员服务的连接。相应地,main()
函数也需要增加对其的初始化
#[tokio::main]
async fn main() {
let addr = "0.0.0.0:59527";
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 m_router = Router::new().route("/cate", get(handler::list_cate)).route(
"/cate/add",
get(handler::add_cate_ui).post(handler::add_cate),
);
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,
})));
axum::Server::bind(&addr.parse().unwrap())
.serve(app.into_make_service())
.await
.unwrap();
}
登录状态和Cookie
为了维护登录状态,我们使用 Cookie
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 cookie = format!("axum_rs_token={}", &logined_admin.email);
Ok(redirect_with_cookie("/m/cate", Some(&cookie)))
}
handler::logout
注销登录
pub async fn logout() -> Result<(StatusCode, HeaderMap), String> {
Ok(redirect_with_cookie("/login", Some("axum_rs_token=")))
}
重定向
这两个函数取自axum-rs代码
这两个函数取自axum-rs代码
/// 重定向
pub fn redirect(url: &str) -> (StatusCode, HeaderMap) {
redirect_with_cookie(url, None)
}
/// 重定向
pub fn redirect_with_cookie(url: &str, cookie: Option<&str>) -> (StatusCode, HeaderMap) {
let mut header = HeaderMap::new();
header.insert(axum::http::header::LOCATION, url.parse().unwrap());
if let Some(cookie) = cookie {
header.insert(axum::http::header::SET_COOKIE, cookie.parse().unwrap());
}
(StatusCode::FOUND, header)
}
pub async fn list_cate(
Extension(state): Extension<Arc<AppState>>,
Query(params): Query<form::CateListFilter>,
) -> Result<Html<String>, String> {
let mut ctx = Context::new();
let msg = params.msg.clone();
if let Some(msg) = msg {
ctx.insert("msg", &msg);
}
let mut cate = state.cate.clone();
let resp = cate.list_category(tonic::Request::new(params.into())).await;
let reply = match resp {
Ok(r) => r.into_inner(),
Err(err) => {
if err.code() == tonic::Code::NotFound {
ListCategoryReply { categories: vec![] }
} else {
return Err(err.to_string());
}
}
};
let mut cate_list = Vec::with_capacity(reply.categories.len());
for c in reply.categories {
let tc: blog_types::Category = c.into();
cate_list.push(tc);
}
ctx.insert("cate_list", &cate_list);
let out = state
.tera
.render("cate/index.html", &ctx)
.map_err(|err| err.to_string())?;
Ok(Html(out))
}
handler::add_cate
添加分类
作业
本章的代码只实现登录、注册登录、分类列表和添加分类。请结合之前章节和源码里的导航菜单,将其余功能实现完整。
本章代码位于06/后台管理分支。