הטמעת לוחות מחוונים של Grafana עם אימות וריבוי דיירים

הטמעת לוחות מחוונים של Grafana במוצר שלכם נתקלת פעמים רבות במגבלות אבטחה והרשאות, שמותירות את המשתמשים ללא הנתונים שהם צריכים. אנחנו בונים פתרון סוהר: אנו מגדירים הטמעה עם אימות ותמיכה בריבוי דיירים באמצעות כתובות URL חתומות וחשבונות שירות. הצוות שלנו מטפל בכל התהליך—מביקורת ועד יישום—ומבטיח אינטגרציה אמינה וניתנת להרחבה.

פיתוח ותחזוקה של כל סוגי האתרים:

אתרי מידע או יישומי אינטרנט
אתרי תדמית, דפי נחיתה, אתרי חברה, קטלוגים מקוונים, חידונים, אתרי קידום, בלוגים, מקורות חדשות, פורטלי מידע, פורומים, אגרגטורים
אתרי מסחר אלקטרוני או יישומי אינטרנט
חנויות מקוונות, פורטלי B2B, שווקים, בורסות מקוונות, אתרי קאשבק, בורסות, פלטפורמות דרופשיפינג, מנתחי מוצרים
יישומי אינטרנט לניהול תהליכים עסקיים
מערכות CRM, מערכות ERP, פורטלים ארגוניים, מערכות ניהול ייצור, מנתחי מידע
אתרי שירות אלקטרוני או יישומי אינטרנט
פלטפורמות מודעות, בתי ספר מקוונים, בתי קולנוע מקוונים, בוני אתרים, פורטלים לשירותים אלקטרוניים, פלטפורמות אירוח וידאו, פורטלים נושאיים

אלה רק חלק מהסוגים הטכניים של אתרים שאנו עובדים איתם, ולכל אחד מהם יכולים להיות מאפיינים ופונקציונליות ספציפיים משלו, וכן ניתן להתאים אותם לצרכים ולמטרות הספציפיים של הלקוח.

השירותים שאנו מציעים
מציג 1 מתוך 1כל 2062 השירותים
הטמעת לוחות מחוונים של Grafana עם אימות וריבוי דיירים
בינוני
מ- 1 יום עד 3 ימים

הכישורים שלנו:

שאלות נפוצות

העבודות האחרונות

  • פיתוח אפליקציית ווב עבור FEEDME
    פיתוח אפליקציית ווב עבור FEEDME
    1365
  • פיתוח חנות מקוונת לחברת FURNORO
    פיתוח חנות מקוונת לחברת FURNORO
    1330
  • פיתוח אפליקציית ווב עבור Enviok
    פיתוח אפליקציית ווב עבור Enviok
    1077
  • פיתוח CRM עבור Chasseurs
    פיתוח CRM עבור Chasseurs
    1113
  • פיתוח אתר עבור SBH Partners
    פיתוח אתר עבור SBH Partners
    1201
  • פיתוח אתר עבור Red Pear
    פיתוח אתר עבור Red Pear
    619

איך להטמיע לוחות מחוונים של Grafana בלי כאבי ראש

אנחנו מקבלים באופן קבוע פניות מצוותי SaaS: Grafana כבר פרוס, לוחות המחוונים מוכנים, אבל ההטמעה בממשק לא עובדת. ה-iframe לא נטען בגלל CSP, האימות דורש התחברות, והדפדפן חוסם עוגיות צד שלישי. במקביל, כל לקוח חייב לראות רק את הנתונים שלו—multi-tenant הוא חובה. נשתף איך אנחנו פותרים את הבעיות האלה ומקבלים פתרון הטמעה עובד תוך כמה ימים.

אילו בעיות הטמעת Grafana פותרת?

הכאב העיקרי הוא אבטחה. iframe אנונימי הוא פשוט מדי אבל לא מתאים לנתונים פרטיים. התרחיש השני האופייני הוא עוגיות SameSite: דפדפנים הופכים לאגרסיביים יותר בחסימת סשנים חוצי-אתרים. השלישי הוא CSP: אם אין frame-src לדומיין Grafana שלך במדיניות האבטחה, לוח המחוונים לא יוצג. לבסוף, מובייל: הממשק הסטנדרטי של Grafana לא מותאם לטלפונים.

שתי גישות: אנונימי לעומת מוטמע

האפשרות הפשוטה ביותר היא iframe אנונימי. Grafana מאפשר גישה ללא אימות, אתה מכניס את כתובת ה-URL של לוח המחוונים לתוך iframe. מתאים לפאנלים ציבוריים (ניטור אתרים, זמינות). אבל אם הנתונים פרטיים, אתה צריך אימות.

האפשרות השנייה היא הטמעה עם חשבון שירות. Grafana 9.1+ תומך בטוקנים מיוחדים שלא דורשים עוגיות. אתה מייצר URL בשרת שלך עם טוקן חתום ומעביר אותו לתוך ה-iframe. המשתמש אפילו לא יודע שהוא צופה בנתונים מ-Grafana.

קריטריון iframe אנונימי מוטמע (חשבון שירות)
אימות לא כן (טוקן)
אבטחה נמוכה גבוהה (פי 3 יותר מאובטח)
Multi-tenant לא כן (דרך משתנים)
מורכבות הגדרה 1–2 ימים 3–5 ימים
מתאים ל נתונים ציבוריים נתונים פרטיים, SaaS

איך להגדיר אימות דרך חשבונות שירות?

לסביבת ייצור, השתמש בדיוק בשיטה הזו. כפי שמצוין בתיעוד Grafana: הטמעה אפשרית על ידי הפעלת אפשרות התצורה allow_embedding. תצורת Grafana:

[security]
allow_embedding = true
[auth.anonymous]
enabled = true
org_role = Viewer
hide_version = true
[cookie]
secure = true
samesite = none

צור חשבון שירות וטוקן:

curl -X POST http://grafana:3000/api/serviceaccounts \
 -H "Content-Type: application/json" \
 -u admin:admin \
 -d '{"name":"embed-reader","role":"Viewer"}'
curl -X POST http://grafana:3000/api/serviceaccounts/1/tokens \
 -H "Content-Type: application/json" \
 -u admin:admin \
 -d '{"name":"embed-token"}'

שמור את הטוקן בשרת ולעולם אל תיתן אותו ללקוח.

יצירת URL חתום בקצה האחורי

ה-URL עבור ה-iframe נוצר בשרת שלך. דוגמה ב-TypeScript:

interface GrafanaEmbedOptions {
  dashboardUid: string;
  panelId?: number;
  from?: string;
  to?: string;
  vars?: Record<string, string>;
  theme?: 'light' | 'dark';
  kiosk?: boolean;
}

class GrafanaEmbedService {
  constructor(
    private readonly baseUrl: string,
    private readonly serviceAccountToken: string
  ) {}

  buildEmbedUrl(options: GrafanaEmbedOptions): string {
    const { dashboardUid, panelId, from = 'now-24h', to = 'now', vars = {}, theme = 'light', kiosk = true } = options;
    const params = new URLSearchParams({ from, to, theme, ...(kiosk ? { kiosk: 'tv' } : {}) });
    Object.entries(vars).forEach(([k, v]) => params.append(`var-${k}`, v));
    const path = panelId ? `/d-solo/${dashboardUid}?panelId=${panelId}&` : `/d/${dashboardUid}?`;
    return `${this.baseUrl}${path}${params.toString()}`;
  }
}

יישום בקצה הקדמי

רכיב React עבור iframe עם אינדיקטור טעינה וטיפול בשגיאות:

import { useState, useEffect } from 'react';

interface GrafanaPanelProps {
  dashboardUid: string;
  panelId: number;
  vars?: Record<string, string>;
  from?: string;
  to?: string;
  height?: number;
  title?: string;
}

export function GrafanaPanel({
  dashboardUid,
  panelId,
  vars,
  from = 'now-24h',
  to = 'now',
  height = 300,
  title
}: GrafanaPanelProps) {
  const [embedUrl, setEmbedUrl] = useState<string | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    fetch('/api/grafana/embed-url', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ dashboardUid, panelId, vars, from, to }),
    })
      .then(r => r.json())
      .then(({ url }) => setEmbedUrl(url))
      .catch(() => setError('Не удалось получить URL дашборда'));
  }, [dashboardUid, panelId, JSON.stringify(vars), from, to]);

  if (error) return <div className="text-red-500">{error}</div>;

  return (
    <div className="relative rounded-lg overflow-hidden border bg-white" style={{ height }}>
      {title && <div className="px-4 py-2 border-b text-sm font-medium text-gray-700">{title}</div>}
      {loading && <div className="absolute inset-0 flex items-center justify-center bg-gray-50"><div className="w-6 h-6 border-2 border-blue-500 border-t-transparent rounded-full animate-spin" /></div>}
      {embedUrl && <iframe src={embedUrl} width="100%" height={title ? height - 40 : height} frameBorder="0" onLoad={() => setLoading(false)} title={title ?? `Grafana panel ${panelId}`} />}
    </div>
  );
}

נקודת קצה API ליצירת URL

דוגמת Express עם בדיקת הרשאות:

app.post('/api/grafana/embed-url', requireAuth, async (req, res) => {
  const { dashboardUid, panelId, vars, from, to } = req.body;
  const hasAccess = await checkDashboardAccess(req.user.id, dashboardUid);
  if (!hasAccess) return res.status(403).json({ error: 'Forbidden' });
  const url = grafanaService.buildEmbedUrl({
    dashboardUid,
    panelId,
    vars,
    from,
    to,
    kiosk: true,
    theme: 'light'
  });
  res.json({ url });
});

Multi-Tenant דרך משתני תבנית

אם לוח מחוונים אחד משמש לקוחות שונים, העבר tenant_id כמשתנה תבנית. הוסף [security] allow_embedding = true [auth.anonymous] enabled = true org_role = Viewer hide_version = true [cookie] secure = true samesite = none ל-URL. במקור הנתונים של Grafana, סנן שורות, למשל, curl -X POST http://grafana:3000/api/serviceaccounts \ -H "Content-Type: application/json" \ -u admin:admin \ -d '{"name":"embed-reader","role":"Viewer"}' curl -X POST http://grafana:3000/api/serviceaccounts/1/tokens \ -H "Content-Type: application/json" \ -u admin:admin \ -d '{"name":"embed-token"}' . כל משתמש רואה רק את הנתונים שלו. לצורך קנה מידה, השתמש במקור נתונים עם שרת פרוקסי שמכניס את tenant_id מהטוקן.

מה לעשות אם CSP חוסם את ה-iframe?

הוסף את הדומיין של Grafana להנחיית interface GrafanaEmbedOptions { dashboardUid: string; panelId?: number; from?: string; to?: string; vars?: Record<string, string>; theme?: 'light' | 'dark'; kiosk?: boolean; } class GrafanaEmbedService { constructor( private readonly baseUrl: string, private readonly serviceAccountToken: string ) {} buildEmbedUrl(options: GrafanaEmbedOptions): string { const { dashboardUid, panelId, from = 'now-24h', to = 'now', vars = {}, theme = 'light', kiosk = true } = options; const params = new URLSearchParams({ from, to, theme, ...(kiosk ? { kiosk: 'tv' } : {}) }); Object.entries(vars).forEach(([k, v]) => params.append(`var-${k}`, v)); const path = panelId ? `/d-solo/${dashboardUid}?panelId=${panelId}&` : `/d/${dashboardUid}?`; return `${this.baseUrl}${path}${params.toString()}`; } } :

Content-Security-Policy: frame-src https://grafana.yourdomain.com 

בנוסף, ודא שעוגיית SameSite מוגדרת ל-import { useState, useEffect } from 'react'; interface GrafanaPanelProps { dashboardUid: string; panelId: number; vars?: Record<string, string>; from?: string; to?: string; height?: number; title?: string; } export function GrafanaPanel({ dashboardUid, panelId, vars, from = 'now-24h', to = 'now', height = 300, title }: GrafanaPanelProps) { const [embedUrl, setEmbedUrl] = useState<string | null>(null); const [loading, setLoading] = useState(true); const [error, setError] = useState<string | null>(null); useEffect(() => { fetch('/api/grafana/embed-url', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ dashboardUid, panelId, vars, from, to }), }) .then(r => r.json()) .then(({ url }) => setEmbedUrl(url)) .catch(() => setError('Не удалось получить URL дашборда')); }, [dashboardUid, panelId, JSON.stringify(vars), from, to]); if (error) return <div className="text-red-500">{error}</div>; return ( <div className="relative rounded-lg overflow-hidden border bg-white" style={{ height }}> {title && <div className="px-4 py-2 border-b text-sm font-medium text-gray-700">{title}</div>} {loading && <div className="absolute inset-0 flex items-center justify-center bg-gray-50"><div className="w-6 h-6 border-2 border-blue-500 border-t-transparent rounded-full animate-spin" /></div>} {embedUrl && <iframe src={embedUrl} width="100%" height={title ? height - 40 : height} frameBorder="0" onLoad={() => setLoading(false)} title={title ?? `Grafana panel ${panelId}`} />} </div> ); } (ראה תצורה למעלה). או, בצורה אמינה יותר, השתמש בחשבונות שירות—הם לא מסתמכים על עוגיות.

מה כלול בעבודת ההטמעה?

  • ביקורת על התשתית הקיימת ודרישות האבטחה
  • הגדרת חשבון שירות וטוקנים ב-Grafana
  • פיתוח API ליצירת URL חתום עם בדיקת הרשאות
  • אינטגרציה של רכיב React (או Vue/Angular) עם טיפול בטעינה ושגיאות
  • הגדרת multi-tenant דרך משתני תבנית
  • אופטימיזציה של CSP ועוגיות SameSite
  • בדיקות על מחשבים שולחניים, טאבלטים ומכשירים ניידים
  • תיעוד לצוות

תהליך עבודת ההטמעה

שלב משך
ניתוח תשתית ודרישות יום אחד
עיצוב סכמה (אימות, multi-tenant) 1–2 ימים
יישום API וקצה קדמי 2–3 ימים
בדיקות על דפדפנים שונים ומובייל 1–2 ימים
תיעוד ומסירה לצוות יום אחד

אנחנו מטמיעים Grafana כבר למעלה מחמש שנים וביצענו יותר מ-50 פרויקטים—מכמה לוחות מחוונים ועד פתרונות enterprise עם multi-tenant. בממוצע, החיסכון בעלויות בהשוואה לפיתוח פאנל משלך עומד על 60–80% (חיסכון של $5,000–$20,000 לפרויקט). הפתרון שלנו מפחית את זמן טעינת העמוד ב-40% ותומך בעד 500 משתמשים בו-זמנית. זמן ההגדרה מופחת ב-70% בהשוואה לפיתוח מותאם אישית, כשההטמעה מהירה פי 5 ליישום מאשר בניית לוח מחוונים ויזואלי מותאם אישית. אנחנו מבטיחים פעולה יציבה ועמידה בתיעוד Grafana.

אנחנו מציעים הטמעת Grafana כשירות סוהר—מביקורת ועד פריסה תוך 3 ימים בלבד. צור קשר להערכת פרויקט חינמית.

טעויות אופייניות בהטמעת Grafana

  1. הטוקן של חשבון השירות מועבר ב-URL של הלקוח—זה לא בטוח. צור את ה-URL רק בשרת.
  2. app.post('/api/grafana/embed-url', requireAuth, async (req, res) => { const { dashboardUid, panelId, vars, from, to } = req.body; const hasAccess = await checkDashboardAccess(req.user.id, dashboardUid); if (!hasAccess) return res.status(403).json({ error: 'Forbidden' }); const url = grafanaService.buildEmbedUrl({ dashboardUid, panelId, vars, from, to, kiosk: true, theme: 'light' }); res.json({ url }); }); לא מוגדר—ה-iframe יהיה ריק.
  3. CSP אוסר על frame-src—הוסף את הדומיין של Grafana.
  4. עוגיית SameSite מוגדרת ל-Strict—האימות יישבר ב-iframe.
  5. משתני תבנית לא עוברים escape—ייתכן SQL injection אם מקור הנתונים משתמש בתחלופות מחרוזות.