الإعداد والملفّات الشخصية وActuator

الأسرار ومتغيرات البيئة

18 دقيقة الدرس 5 من 13

الأسرار ومتغيرات البيئة

كل تطبيق إنتاجي يمتلك أسرارًا: كلمات مرور قواعد البيانات، ومفاتيح API، ومفاتيح توقيع JWT، ورموز خدمات الطرف الثالث. طريقة تعاملك مع هذه الأسرار هي إحدى أهم القرارات الأمنية التي تتخذها بوصفك مطورًا. يغطّي هذا الدرس الطيف الكامل — من الأخطاء الأكثر شيوعًا إلى الأنماط التي تعتمدها الفرق المحترفة عند نشر تطبيقات Spring Boot 3.

المشكلة الجوهرية: الأسرار داخل كود المصدر

أكثر الأخطاء شيوعًا هو ترميز بيانات الاعتماد مباشرةً في application.properties وإيداع ذلك الملف في نظام التحكم بالإصدارات. بمجرد أن يصل سر ما إلى مستودع Git، يصبح عمليًا عامًا — حتى في المستودعات الخاصة، لأن السجل يبقى، ويمكن أن يُسرَّب عبر حسابات مخترقة أو موظفين سابقين أو تعريض غير مقصود.

لا تودع أسرارًا حقيقية في Git أبدًا. تمتلك GitHub وGitLab وBitbucket روبوتات تفحص الأسرار تلقائيًا. تشهد شركات السحابة باستمرار استخدام مفاتيح AWS الملتزَمة خلال دقائق. عامل أي سر وصل إلى مستودع عام باعتباره مخترقًا نهائيًا — أبطله واستبدله فورًا.

إليك النمط الذي يجب أن تتجنبه تمامًا في أي بيئة تتجاوز النماذج المحلية:

# application.properties — لا تفعل هذا في الإنتاج أبدًا spring.datasource.password=mySuperSecretPassword123 stripe.api-key=sk_live_abc123xyz

متغيرات البيئة: أساس منهجية الاثني عشر عاملًا

تحدّد منهجية Twelve-Factor App الحلَّ القياسي: خزّن الإعدادات التي تتغير بين البيئات (dev وstaging وprod) في متغيرات البيئة. يقرأ Spring Boot متغيرات بيئة نظام التشغيل تلقائيًا ويربطها بمفاتيح الخصائص وفق قاعدة ربط مرنة: يُعيَّن SPRING_DATASOURCE_PASSWORD إلى spring.datasource.password، ويُعيَّن STRIPE_API_KEY إلى stripe.api-key.

الربط المرن: يُطبّع Spring Boot أسماء الخصائص بتحويلها إلى حروف صغيرة واستبدال الشرطات السفلية والشرطات والنقاط. لذا فإن MY_APP_API_KEY وmy.app.api-key وmyApp.apiKey كلها تُعيَّن إلى الخاصية ذاتها. يُفضَّل استخدام SCREAMING_SNAKE_CASE لمتغيرات البيئة — فهو اتفاقية Unix ولا يكتنفه أي غموض في السكريبتات.

على خادم Linux أو macOS تُصدر المتغيرات قبل تشغيل JVM:

export DB_PASSWORD="vault:retrieved-at-deploy" export STRIPE_API_KEY="sk_live_abc123xyz" java -jar app.jar

في application.properties تُشير إليها بعناصر نائبة ${}:

spring.datasource.url=jdbc:postgresql://${DB_HOST:localhost}:${DB_PORT:5432}/${DB_NAME:mydb} spring.datasource.username=${DB_USER} spring.datasource.password=${DB_PASSWORD} stripe.api-key=${STRIPE_API_KEY}

القيمة المفصولة بنقطتين (${DB_HOST:localhost}) هي القيمة الافتراضية عند غياب المتغير. استخدم القيم الافتراضية للقيم غير الحساسة كأسماء المضيفين في بيئة التطوير، لكن لا توفّر قيمة افتراضية لأي سر أبدًا — غياب السر يجب أن يُفشل التطبيق عند بدء التشغيل حتى تكتشف المشكلة فورًا لا أن تستمر بقيمة خاطئة بصمت.

ملف .env وملفات تعريف Spring

في بيئة التطوير المحلي، كتابة أوامر export قبل كل تشغيل أمر مرهق. الحل المعتاد هو ملف .env في جذر المشروع:

# .env — للتطوير المحلي فقط — لا تودع هذا الملف أبدًا DB_PASSWORD=localdevpassword STRIPE_API_KEY=sk_test_... JWT_SECRET=dev-only-secret-32-chars-minimum

أضف .env فورًا إلى .gitignore. لا يقرأ Spring Boot ملف .env أصلًا، لكن عدة مقاربات تعالج ذلك:

  • إعدادات تشغيل IDE — تتيح IntelliJ IDEA وVS Code تحديد ملف env في إعدادات التشغيل. وهذا أنظف مقاربة محلية.
  • dotenv-spring-boot — مكتبة خفيفة (me.paulschwarz:spring-dotenv) تحمّل .env تلقائيًا في Environment الخاصة بـ Spring.
  • تحميل الملف في الشِّلset -a; source .env; set +a; ./mvnw spring-boot:run يُصدر كل سطر قبل استدعاء Maven.
قدّم ملف .env.example في المستودع. يسرد هذا الملف كل متغير مطلوب بقيمة عنصر نائب وتعليق يشرح الغرض منه. ينسخه أعضاء الفريق الجدد إلى .env ويملؤونه. يوثّق هذا المتطلبات دون تسريب الأسرار.
# .env.example — أودع هذا الملف DB_HOST=localhost DB_PORT=5432 DB_NAME=myapp DB_USER=postgres DB_PASSWORD= # ضع كلمة مرور Postgres المحلية هنا STRIPE_API_KEY= # احصل عليها من https://dashboard.stripe.com/test/apikeys JWT_SECRET= # أنشئها: openssl rand -hex 32

قراءة الأسرار برمجيًا مع @Value و@ConfigurationProperties

بمجرد أن يكون المتغير في البيئة (أو في application.properties عبر عنصر نائب)، يحلّه Spring تلقائيًا. يمكنك حقنه بـ @Value:

import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; @Service public class StripePaymentService { private final String apiKey; public StripePaymentService(@Value("${stripe.api-key}") String apiKey) { this.apiKey = apiKey; } public void charge(long amountCents, String token) { // استخدم this.apiKey — لا تسجّله في السجلات أبدًا } }

أو اربط مجموعة من الأسرار المترابطة بفئة @ConfigurationProperties (تمت تغطيتها بعمق في الدرس الثاني، لكنها تستحق التذكير هنا):

import org.springframework.boot.context.properties.ConfigurationProperties; @ConfigurationProperties(prefix = "stripe") public record StripeProperties(String apiKey, String webhookSecret) {}
لا تسجّل الأسرار في سجلات التطبيق — ولو جزئيًا. أُطر التسجيل وأنظمة التتبع الموزع وأدوات الإبلاغ عن الأخطاء (Sentry وDatadog) كلها تلتقط سطور السجل. جملة مثل log.debug("API key: {}", apiKey) في خدمة تُستدعى مع كل طلب ستبثّ سرّك في كل مجمّع سجلات تستخدمه مؤسستك.

الأسرار في البيئات الحاوية (Docker وKubernetes)

عند التشغيل في Docker، مرّر متغيرات البيئة عند بدء تشغيل الحاوية:

docker run \ -e DB_PASSWORD="$(vault kv get -field=password secret/myapp/db)" \ -e STRIPE_API_KEY="sk_live_..." \ -p 8080:8080 \ myapp:latest

في Docker Compose للتطوير، استخدم توجيه env_file:

# docker-compose.yml services: app: image: myapp:latest env_file: - .env # يقرأ ملف .env المحلي، غير مُودَع في Git ports: - "8080:8080"

في Kubernetes، الآلية الاصطلاحية هي كائن Secret يُثبَّت كمتغيرات بيئة أو كمجلد. لا تدمج الأسرار أبدًا في صورة Docker أو في ConfigMap (الذي لا يُشفَّر في حالة الراحة).

# Kubernetes Secret (قيم مشفرة بـ base64) apiVersion: v1 kind: Secret metadata: name: myapp-secrets type: Opaque data: db-password: bXlwYXNzd29yZA== # echo -n 'mypassword' | base64 --- # مقتطف من deployment env: - name: DB_PASSWORD valueFrom: secretKeyRef: name: myapp-secrets key: db-password

مديرو الأسرار الخارجيون

للفرق التي تحتاج إلى سجلات تدقيق وتدوير تلقائي وتحكم دقيق في الوصول، يكون مدير الأسرار المخصص هو الأداة الصحيحة. الخيارات الرئيسية تتكامل مع Spring Boot عبر Spring Cloud:

  • HashiCorp Vault — محلي أو سحابي؛ يقرأ spring-cloud-vault الأسرار عند بدء التشغيل ويحقنها خصائصَ.
  • AWS Secrets Manager / Parameter Store — يحلّ spring-cloud-aws خصائص aws.secretsmanager.secret-name تلقائيًا.
  • Azure Key Vault — يعيّن azure-spring-cloud-starter-keyvault-secrets مدخلات الخزنة إلى خصائص Spring.

مع Spring Cloud Vault يصبح ملف bootstrap.yml (أو application.yml بالاستيراد الصحيح) بسيطًا كما يلي:

spring: cloud: vault: host: vault.internal.example.com port: 8200 authentication: APPROLE app-role: role-id: ${VAULT_ROLE_ID} secret-id: ${VAULT_SECRET_ID} kv: enabled: true default-context: myapp

عندها يحلّ Spring Boot ${stripe.api-key} بجلب مسار السر myapp من Vault عند بدء التشغيل — لا يلمس السر نظام الملفات أو ملف خصائص قط.

الخلاصة: قائمة التحقق من معالجة الأسرار

  1. أضف .env وأي ملفات *-secrets.properties إلى .gitignore قبل أول إيداع.
  2. استخدم عناصر نائبة ${ENV_VAR} في ملفات application.properties المُودَعة — لا قيم مرمّزة.
  3. قدّم .env.example بتعليقات دون قيم حقيقية.
  4. لا توفّر قيمة افتراضية لعنصر نائب يمثّل سرًّا (أفشل بصوت عالٍ عند غيابه).
  5. لا تسجّل قيم الأسرار — حتى في وضع DEBUG.
  6. في الإنتاج، احقن عبر متغيرات بيئة نظام التشغيل أو أسرار Docker أو أسرار Kubernetes أو مدير أسرار متخصص.
  7. دوّر أي سر تعرّض للكشف — عامله باعتباره مخترقًا نهائيًا.

اتباع هذه القواعد لا يكلّف جهدًا إضافيًا يُذكر ويمنع أكثر فئات حوادث الأمان الإنتاجية شيوعًا. يبني الدرس التالي على هذا الأساس ليغطي بنية إعدادات YAML الكاملة التي تجعل إعدادات Spring Boot المعقدة قابلة للقراءة والصيانة.