本章将讨论使用jwt进行鉴权。
blog-auth
:jwt实现blog-backend
:上一章的后台管理web服务
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 相关的配置,它们都是从环境变量中读取的。
特别地,监听地址也变成了从环境变量中读取
作业
gRPC 服务的安全与鉴权
我们的后台管理WEB服务增加了jwt token的鉴权,但 gRPC 还没有加任何鉴权。
对于本专题而言,gPRC 不是必须的,因为所有 gRPC 都监听 127.0.0.1
,同时,能假设确保所有服务都是自己开发的,知道哪些是后台管理才可以调用的。
也正是因为以上假设,所以 gPRC 安全问题暴露了。
安全
本专题中所有服务的通讯都是使用非安全的协议,为了数据传输的安全,你可能需要 TLS
鉴权
本专题中,gRPC 没有任何鉴权,都是建立在web开发者知道哪些 gRPC 可以公开调用,哪些需要后台验证才能调用。试想,如果有程序员在前台WEB中调用 gPRC 创建管理员,结果将是灾难性的。