简介
本专题将带你使用 axum 和 gRPC 构建一个分布式的博客系统数据结构与Protobuf
本章对我们项目的数据结构和proto进行定义实现分类服务
本章我们实现分类服务,即 `category-srv`实现文章服务
本章将带你实现文章的 gPRC 服务。实现前台web服务
本章将通过使用 axum 调用分类和文章的 gRPC 服务,来实现博客前台Web服务实现管理员服务
本章我们将实现管理员服务实现后台管理web服务
本章将使用 axum 调用 gRPC 服务来实现后台管理的 web 服务安全与鉴权
本章将讨论使用jwt进行鉴权服务扩容、注册、发现和编排
本章将讨论服务管理相关的话题配置中心服务
本章讨论配置中心的实现总结
本专题试图通过一个分布式博客的案例来探讨使用 rust 实现 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()
函数也需要增加对其的初始化
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
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 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代码
/// 重定向
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)
}
handler::list_cate
分类列表
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
添加分类
pub async fn add_cate(
Extension(state): Extension<Arc<AppState>>,
Form(frm): Form<form::AddCatetory>,
) -> Result<(StatusCode, HeaderMap), String> {
let mut cate = state.cate.clone();
let resp = cate
.create_category(tonic::Request::new(blog_proto::CreateCategoryRequest {
name: frm.name,
}))
.await
.map_err(|err| err.to_string())?;
let repl = resp.into_inner();
let url = format!("/m/cate?msg=分类(ID为{})添加成功", repl.id);
Ok(redirect(&url))
}
作业
本章的代码只实现登录、注册登录、分类列表和添加分类。请结合之前章节和源码里的导航菜单,将其余功能实现完整。
本章代码位于06/后台管理分支。