本章将讨论使用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 创建管理员,结果将是灾难性的。