简介
本专题将带你使用 axum 和 gRPC 构建一个分布式的博客系统数据结构与Protobuf
本章对我们项目的数据结构和proto进行定义实现分类服务
本章我们实现分类服务,即 `category-srv`实现文章服务
本章将带你实现文章的 gPRC 服务。实现前台web服务
本章将通过使用 axum 调用分类和文章的 gRPC 服务,来实现博客前台Web服务实现管理员服务
本章我们将实现管理员服务实现后台管理web服务
本章将使用 axum 调用 gRPC 服务来实现后台管理的 web 服务安全与鉴权
本章将讨论使用jwt进行鉴权服务扩容、注册、发现和编排
本章将讨论服务管理相关的话题配置中心服务
本章讨论配置中心的实现总结
本专题试图通过一个分布式博客的案例来探讨使用 rust 实现 gRPC 微服务架构的可行性
安全与鉴权
- 73706
- 2022-09-23 22:33:25
本章涉及的 crate 有:
blog-auth
:jwt实现blog-backend
:上一章的后台管理web服务
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))
}
}
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();
}
特别地,监听地址也变成了从环境变量中读取
作业
将分类服务、文章服务和管理员服务的地址从硬编码改成从环境变量读取。
gRPC 服务的安全与鉴权
我们的后台管理WEB服务增加了jwt token的鉴权,但 gRPC 还没有加任何鉴权。
对于本专题而言,gPRC 不是必须的,因为所有 gRPC 都监听 127.0.0.1
,同时,能假设确保所有服务都是自己开发的,知道哪些是后台管理才可以调用的。
也正是因为以上假设,所以 gPRC 安全问题暴露了。
本专题中所有服务的通讯都是使用非安全的协议,为了数据传输的安全,你可能需要 TLS
本专题中,gRPC 没有任何鉴权,都是建立在web开发者知道哪些 gRPC 可以公开调用,哪些需要后台验证才能调用。试想,如果有程序员在前台WEB中调用 gPRC 创建管理员,结果将是灾难性的。
gRPC 提供了拦截器和metadata,利用它们,可以将web 服务的 jwt token 传递到 gPRC 服务,实现 gRPC 鉴权。