Σημείωμα
Η πρόσβαση σε αυτήν τη σελίδα απαιτεί εξουσιοδότηση. Μπορείτε να δοκιμάσετε να εισέλθετε ή να αλλάξετε καταλόγους.
Η πρόσβαση σε αυτήν τη σελίδα απαιτεί εξουσιοδότηση. Μπορείτε να δοκιμάσετε να αλλάξετε καταλόγους.
Τα απομακρυσμένα τελικά σημεία σε φόρτους εργασίας Microsoft Fabric χρησιμοποιούν έναν εξειδικευμένο μηχανισμό ελέγχου ταυτότητας που παρέχει τόσο το περιβάλλον χρήστη όσο και την ταυτότητα της εφαρμογής. Αυτό το άρθρο εξηγεί πώς να εφαρμόσετε έλεγχο ταυτότητας για απομακρυσμένα τελικά σημεία που χειρίζονται εργασίες, ειδοποιήσεις κύκλου ζωής και άλλες λειτουργίες υποστήριξης.
Επισκόπηση ροών ελέγχου ταυτότητας
Οι φόρτοι εργασίας Fabric περιλαμβάνουν τρεις κύριες ροές ελέγχου ταυτότητας:
- Fabric σε φόρτο εργασίας - Το Fabric καλεί το απομακρυσμένο τελικό σημείο σας με τη μορφή SubjectAndAppToken
- Φόρτος εργασίας στο Fabric - Ο φόρτος εργασίας σας καλεί API Fabric με διακριτικά SubjectAndAppToken ή Bearer
- Workload Frontend to Backend - Το frontend σας καλεί το backend σας με Bearer tokens
Αυτό το άρθρο εστιάζει στον έλεγχο ταυτότητας αιτήσεων από το Fabric στον φόρτο εργασίας σας και στην πραγματοποίηση κλήσεων με έλεγχο ταυτότητας στο Fabric.
Μορφή SubjectAndAppToken
Όταν το Fabric καλεί το απομακρυσμένο τελικό σημείο σας (για εργασίες, ειδοποιήσεις κύκλου ζωής ή άλλες λειτουργίες), χρησιμοποιεί μια μορφή ελέγχου ταυτότητας διπλού διακριτικού που ονομάζεται SubjectAndAppToken.
Δομή κεφαλίδας εξουσιοδότησης
SubjectAndAppToken1.0 subjectToken="<user-delegated-token>", appToken="<app-only-token>"
Αυτή η μορφή περιλαμβάνει δύο ξεχωριστά διακριτικά:
-
subjectToken- Ένα εκχωρημένο διακριτικό που αντιπροσωπεύει τον χρήστη για λογαριασμό του οποίου εκτελείται η λειτουργία -
appToken- Ένα διακριτικό μόνο για εφαρμογή από την εφαρμογή Fabric, που αποδεικνύει ότι η αίτηση προέρχεται από το Fabric
Σημαντικό
Δεν subjectToken είναι πάντα παρόν. Μπορεί να λείπει σε σενάρια όπως:
- Λειτουργίες κύριας υπηρεσίας - Όταν μια κύρια υπηρεσία αλληλεπιδρά με δημόσια API Fabric χωρίς περιβάλλον χρήστη
- Λειτουργίες συστήματος - Λειτουργίες όπως η διαγραφή στοιχείων όπου δεν εμπλέκεται άμεσα κανένας χρήστης
- Αυτοματοποιημένες ροές εργασίας - διοχετεύσεις CI/CD ή προγραμματισμένες λειτουργίες που εκτελούνται χωρίς αλληλεπίδραση με τον χρήστη
Το απομακρυσμένο τελικό σημείο σας πρέπει να έχει σχεδιαστεί για να χειρίζεται και τις δύο περιπτώσεις: αιτήσεις με περιβάλλον χρήστη (υπάρχει subjectToken) και αιτήσεις χωρίς περιβάλλον χρήστη (απουσία subjectToken).
Γιατί Dual Tokens;
Η προσέγγιση διπλού διακριτικού παρέχει τρία βασικά οφέλη:
- Επικύρωση - Επαληθεύστε ότι η αίτηση προήλθε από το Fabric επικυρώνοντας το appToken
- Περιβάλλον χρήστη - Το subjectToken παρέχει το περιβάλλον χρήστη για την ενέργεια που εκτελείται
- Inter-Service Επικοινωνία - Χρησιμοποιήστε το subjectToken για να αποκτήσετε διακριτικά On-Behalf-Of (OBO) για κλήση άλλων υπηρεσιών με περιβάλλον χρήστη
Ανάλυση SubjectAndAppToken
Το απομακρυσμένο τελικό σημείο πρέπει να αναλύσει την κεφαλίδα εξουσιοδότησης για να εξαγάγει και τα δύο διακριτικά.
Παράδειγμα JavaScript
/**
* Parse SubjectAndAppToken from Authorization header
* @param {string} authHeader - Authorization header value
* @returns {object|null} Parsed tokens or null if invalid format
*/
function parseSubjectAndAppToken(authHeader) {
if (!authHeader || !authHeader.startsWith('SubjectAndAppToken1.0 ')) {
return null;
}
const tokenPart = authHeader.substring('SubjectAndAppToken1.0 '.length);
const tokens = {};
const parts = tokenPart.split(',');
for (const part of parts) {
const [key, value] = part.split('=');
if (key && value) {
// Remove surrounding quotes from the token value
const cleanValue = value.trim().replace(/^"(.*)"$/, '$1');
tokens[key.trim()] = cleanValue;
}
}
return {
subjectToken: tokens.subjectToken || null,
appToken: tokens.appToken || null
};
}
Παράδειγμα χρήσης
const authHeader = req.headers['authorization'];
const tokens = parseSubjectAndAppToken(authHeader);
if (!tokens || !tokens.appToken) {
return res.status(401).json({ error: 'Invalid authorization header' });
}
// Now you have access to:
// - tokens.subjectToken (user-delegated token, may be null)
// - tokens.appToken (Fabric app token, always present)
// Check if user context is available
if (tokens.subjectToken) {
console.log('Request has user context');
} else {
console.log('Request is app-only (no user context)');
}
Επικύρωση διακριτικού
Και τα δύο διακριτικά πρέπει να επικυρωθούν για να διασφαλιστεί η αυθεντικότητα και οι κατάλληλοι ισχυρισμοί.
Επικύρωση διακριτικού εφαρμογής
Είναι ένα διακριτικό μόνο για εφαρμογές από το appToken Fabric. Επικυρώνω:
- Υπογραφή διακριτικού - Επαλήθευση με Azure κλειδιά υπογραφής AD
-
Διάρκεια ζωής διακριτικού - Έλεγχος
nbf(όχι πριν) καιexp(λήξη) αξιώσεις - Κοινό - Πρέπει να αντιστοιχεί στην εγγραφή της εφαρμογής του φόρτου εργασίας σας
- Εκδότης - Πρέπει να είναι από Azure AD με σωστό μισθωτή
-
idtypαξίωση - Πρέπει να είναι"app"για διακριτικά μόνο για εφαρμογές -
Καμία
scpαξίωση - Τα διακριτικά μόνο για εφαρμογές δεν έχουν αξιώσεις εμβέλειας - Αναγνωριστικό μισθωτή - Πρέπει να αντιστοιχεί στο αναγνωριστικό μισθωτή εκδότη
Δείγμα ισχυρισμών appToken:
{
"aud": "api://localdevinstance/aaaabbbb-0000-cccc-1111-dddd2222eeee/Fabric.WorkloadSample/123",
"iss": "https://sts.windows.net/12345678-77f3-4fcc-bdaa-487b920cb7ee/",
"iat": 1700047232,
"nbf": 1700047232,
"exp": 1700133932,
"appid": "11112222-bbbb-3333-cccc-4444dddd5555",
"appidacr": "2",
"idtyp": "app",
"oid": "aaaaaaaa-0000-1111-2222-bbbbbbbbbbbb",
"tid": "bbbbcccc-1111-dddd-2222-eeee3333ffff",
"ver": "1.0"
}
Επικύρωση διακριτικού θέματος
Το subjectToken είναι ένα διακριτικό που έχει ανατεθεί από τον χρήστη. Επικυρώνω:
- Υπογραφή διακριτικού - Επαλήθευση με Azure κλειδιά υπογραφής AD
-
Διάρκεια ζωής διακριτικού - Έλεγχος
nbfκαιexpαξιώσεις - Κοινό - Πρέπει να αντιστοιχεί στην εγγραφή της εφαρμογής του φόρτου εργασίας σας
- Εκδότης - Πρέπει να είναι από Azure AD με σωστό μισθωτή
-
scpαξίωση - Πρέπει να περιλαμβάνει"FabricWorkloadControl"πεδίο εφαρμογής -
Καμία
idtypαξίωση - Τα διακριτικά με ανάθεση δεν έχουν αυτήν την αξίωση -
appidπρέπει να ταιριάζει - Θα πρέπει να είναι το ίδιο όπως στο appToken
Δείγματα ισχυρισμών subjectToken:
{
"aud": "api://localdevinstance/aaaabbbb-0000-cccc-1111-dddd2222eeee/Fabric.WorkloadSample/123",
"iss": "https://sts.windows.net/12345678-77f3-4fcc-bdaa-487b920cb7ee/",
"iat": 1700050446,
"nbf": 1700050446,
"exp": 1700054558,
"appid": "11112222-bbbb-3333-cccc-4444dddd5555",
"scp": "FabricWorkloadControl",
"name": "john doe",
"oid": "bbbbbbbb-1111-2222-3333-cccccccccccc",
"upn": "user1@contoso.com",
"tid": "bbbbcccc-1111-dddd-2222-eeee3333ffff",
"ver": "1.0"
}
Παράδειγμα επικύρωσης διακριτικού JavaScript
const jwt = require('jsonwebtoken');
const jwksClient = require('jwks-rsa');
/**
* Validate Azure AD token
* @param {string} token - JWT token to validate
* @param {object} options - Validation options
* @returns {Promise<object>} Validated token claims
*/
async function validateAadToken(token, options = {}) {
const { isAppOnly = false, tenantId = null, audience = null } = options;
// Decode token to get header and tenant
const decoded = jwt.decode(token, { complete: true });
if (!decoded) {
throw new Error('Invalid token format');
}
const { header, payload } = decoded;
const tokenTenantId = payload.tid;
// Get signing key from Azure AD
const client = jwksClient({
jwksUri: `https://login.microsoftonline.com/${tokenTenantId}/discovery/v2.0/keys`,
cache: true,
cacheMaxAge: 86400000 // 24 hours
});
const signingKey = await new Promise((resolve, reject) => {
client.getSigningKey(header.kid, (err, key) => {
if (err) reject(err);
else resolve(key.getPublicKey());
});
});
// Verify token signature and claims
const verifyOptions = {
algorithms: ['RS256'],
audience: audience,
clockTolerance: 60 // 1 minute clock skew
};
const verified = jwt.verify(token, signingKey, verifyOptions);
// Validate app-only vs delegated token
if (isAppOnly) {
if (verified.idtyp !== 'app') {
throw new Error('Expected app-only token');
}
if (verified.scp) {
throw new Error('App-only token should not have scp claim');
}
} else {
if (verified.idtyp) {
throw new Error('Expected delegated token');
}
if (!verified.scp || !verified.scp.includes('FabricWorkloadControl')) {
throw new Error('Missing required FabricWorkloadControl scope');
}
}
// Validate tenant if required
if (tenantId && verified.tid !== tenantId) {
throw new Error(`Token tenant mismatch: expected ${tenantId}, got ${verified.tid}`);
}
return verified;
}
Ενδιάμεσο λογισμικό ελέγχου ταυτότητας
Εφαρμόστε ενδιάμεσο λογισμικό για τον έλεγχο ταυτότητας εισερχόμενων αιτήσεων από το Fabric.
Ολοκλήρωση ροής ελέγχου ταυτότητας
/**
* Authentication middleware that validates control plane calls from Fabric
* Handles both user-delegated (with subjectToken) and app-only (without subjectToken) scenarios
* @param {object} req - Express request object
* @param {object} res - Express response object
* @param {object} options - Authentication options
* @param {boolean} options.requireSubjectToken - Whether subject token is required (default: false)
* @returns {Promise<boolean>} True if authenticated successfully
*/
async function authenticateControlPlaneCall(req, res, options = {}) {
const { requireSubjectToken = false } = options;
try {
// Extract and parse authorization header
const authHeader = req.headers['authorization'];
if (!authHeader) {
return res.status(401).json({ error: 'Missing Authorization header' });
}
const tokens = parseSubjectAndAppToken(authHeader);
if (!tokens || !tokens.appToken) {
return res.status(401).json({ error: 'Invalid Authorization header format' });
}
// Get tenant ID from header
const tenantId = req.headers['ms-client-tenant-id'];
if (!tenantId) {
return res.status(400).json({ error: 'Missing ms-client-tenant-id header' });
}
// Get configuration
const publisherTenantId = process.env.TENANT_ID;
const audience = process.env.BACKEND_AUDIENCE;
const fabricBackendAppId = '00000009-0000-0000-c000-000000000000';
// Validate app token (from Fabric)
const appTokenClaims = await validateAadToken(tokens.appToken, {
isAppOnly: true,
audience: audience
});
// Verify app token is from Fabric
const appTokenAppId = appTokenClaims.appid || appTokenClaims.azp;
if (appTokenAppId !== fabricBackendAppId) {
return res.status(401).json({ error: 'App token not from Fabric' });
}
// Verify app token is in publisher's tenant
if (publisherTenantId && appTokenClaims.tid !== publisherTenantId) {
return res.status(401).json({ error: 'App token tenant mismatch' });
}
// Validate subject token if present
let subjectTokenClaims = null;
if (tokens.subjectToken) {
subjectTokenClaims = await validateAadToken(tokens.subjectToken, {
isAppOnly: false,
audience: audience,
tenantId: tenantId
});
// Verify subject token has same appid as app token
const subjectAppId = subjectTokenClaims.appid || subjectTokenClaims.azp;
if (subjectAppId !== appTokenAppId) {
return res.status(401).json({ error: 'Token appid mismatch' });
}
} else if (requireSubjectToken) {
// If subject token is required but missing, reject the request
return res.status(401).json({
error: 'Subject token required for this operation'
});
}
// Store authentication context in request
req.authContext = {
subjectToken: tokens.subjectToken,
appToken: tokens.appToken,
tenantId: tenantId,
hasSubjectContext: !!tokens.subjectToken,
appTokenClaims: appTokenClaims,
subjectTokenClaims: subjectTokenClaims,
userId: subjectTokenClaims?.oid || subjectTokenClaims?.sub,
userName: subjectTokenClaims?.name || subjectTokenClaims?.upn
};
return true; // Authentication successful
} catch (error) {
console.error('Authentication failed:', error.message);
res.status(401).json({ error: 'Authentication failed' });
return false;
}
}
Χρήση του ενδιάμεσου λογισμικού
app.post('/api/jobs/execute', async (req, res) => {
// Authenticate the request (subjectToken is optional)
const authenticated = await authenticateControlPlaneCall(req, res);
if (!authenticated) {
return; // Response already sent by middleware
}
// Access authentication context
const { hasSubjectContext, userId, userName, tenantId, subjectToken } = req.authContext;
if (hasSubjectContext) {
console.log(`Executing job for user: ${userName} (${userId})`);
} else {
console.log('Executing job in app-only context (no user)');
}
// Execute job logic...
});
// Example: Require user context for specific operations
app.post('/api/lifecycle/create', async (req, res) => {
// For create operations, we might require user context
const authenticated = await authenticateControlPlaneCall(req, res, {
requireSubjectToken: true
});
if (!authenticated) {
return; // Response already sent by middleware
}
// User context is guaranteed to be present here
const { userId, userName } = req.authContext;
console.log(`Creating item for user: ${userName}`);
// Handle creation...
});
// Example: Allow app-only context for delete operations
app.post('/api/lifecycle/delete', async (req, res) => {
// Delete operations might not have user context
const authenticated = await authenticateControlPlaneCall(req, res, {
requireSubjectToken: false // Default, but shown for clarity
});
if (!authenticated) {
return;
}
const { hasSubjectContext, userName } = req.authContext;
if (hasSubjectContext) {
console.log(`Deleting item for user: ${userName}`);
} else {
console.log('Deleting item (system operation)');
}
// Handle deletion...
});
Ανταλλαγή διακριτικών - ΡοήBehalf-Of (OBO)
Για να καλέσετε API Fabric ή να αποκτήσετε πρόσβαση στο OneLake από το απομακρυσμένο τελικό σημείο σας, πρέπει να ανταλλάξετε το subjectToken του χρήστη με διακριτικά για συγκεκριμένους πόρους χρησιμοποιώντας τη ροή OAuth 2.0 On-Behalf-Of.
Υπηρεσία ανταλλαγής διακριτικών
const https = require('https');
const { URL } = require('url');
const AAD_LOGIN_URL = 'https://login.microsoftonline.com';
const ONELAKE_SCOPE = 'https://storage.azure.com/.default';
const FABRIC_SCOPE = 'https://analysis.windows.net/powerbi/api/.default';
/**
* Get token for any scope using OBO flow
* @param {string} userToken - User's access token (subjectToken)
* @param {string} tenantId - User's tenant ID
* @param {string} scope - Target resource scope
* @returns {Promise<string>} Access token for the requested scope
* @throws {Error} Throws consent-related errors (AADSTS65001, AADSTS65005) that should be propagated to UX
*/
async function getTokenForScope(userToken, tenantId, scope) {
const clientId = process.env.BACKEND_APPID;
const clientSecret = process.env.BACKEND_CLIENT_SECRET;
if (!clientId || !clientSecret) {
throw new Error('BACKEND_APPID and BACKEND_CLIENT_SECRET required');
}
const tokenEndpoint = `${AAD_LOGIN_URL}/${tenantId}/oauth2/v2.0/token`;
const requestBody = new URLSearchParams({
grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',
client_id: clientId,
client_secret: clientSecret,
assertion: userToken,
scope: scope,
requested_token_use: 'on_behalf_of'
}).toString();
const response = await makeTokenRequest(tokenEndpoint, requestBody);
if (!response.access_token) {
throw new Error('Token exchange failed: missing access_token');
}
return response.access_token;
}
/**
* Get OneLake access token
* @param {string} userToken - User's access token from authContext
* @param {string} tenantId - User's tenant ID
* @returns {Promise<string>} OneLake access token
*/
async function getOneLakeToken(userToken, tenantId) {
return getTokenForScope(userToken, tenantId, ONELAKE_SCOPE);
}
/**
* Get Fabric OBO token for calling Fabric APIs
* @param {string} userToken - User's access token
* @param {string} tenantId - User's tenant ID
* @returns {Promise<string>} Fabric OBO token
*/
async function getFabricOboToken(userToken, tenantId) {
return getTokenForScope(userToken, tenantId, FABRIC_SCOPE);
}
/**
* Make HTTPS request to token endpoint
*/
function makeTokenRequest(url, body) {
return new Promise((resolve, reject) => {
const parsedUrl = new URL(url);
const options = {
hostname: parsedUrl.hostname,
port: 443,
path: parsedUrl.pathname,
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Content-Length': Buffer.byteLength(body)
}
};
const req = https.request(options, (res) => {
let responseBody = '';
res.on('data', (chunk) => { responseBody += chunk; });
res.on('end', () => {
const parsed = JSON.parse(responseBody);
if (res.statusCode >= 200 && res.statusCode < 300) {
resolve(parsed);
} else {
const error = parsed.error_description || parsed.error || `HTTP ${res.statusCode}`;
reject(new Error(error));
}
});
});
req.on('error', reject);
req.write(body);
req.end();
});
}
Χρήση του Token Exchange
// In your job or lifecycle notification handler
async function handleJobExecution(req, res) {
const authenticated = await authenticateControlPlaneCall(req, res);
if (!authenticated) return;
const { hasSubjectContext, subjectToken, tenantId } = req.authContext;
try {
// Only exchange token if user context is available
if (hasSubjectContext) {
// Get OneLake token to access item data on behalf of the user
const oneLakeToken = await getOneLakeToken(subjectToken, tenantId);
// Use OneLake token to read/write data
const data = await readFromOneLake(oneLakeToken, workspaceId, itemId);
// Process data...
} else {
// Handle app-only scenario
// You may need to use different authentication or skip user-specific operations
console.log('Processing job without user context');
// Use workspace or item-level access instead
}
res.status(200).json({ status: 'completed' });
} catch (error) {
console.error('Job execution failed:', error);
res.status(500).json({ error: 'Job execution failed' });
}
}
Κλήση API Fabric
Για να καλέσετε API ελέγχου φόρτου εργασίας Fabric, πρέπει να δημιουργήσετε ένα SubjectAndAppToken με διακριτικά OBO και S2S.
Διακριτικό Service-to-Service (S2S)
Εκτός από το διακριτικό OBO, χρειάζεστε ένα διακριτικό S2S μόνο για εφαρμογή για το τμήμα appToken.
/**
* Get S2S (Service-to-Service) token using client credentials flow
* @param {string} tenantId - Publisher's tenant ID
* @param {string} scope - Target resource scope
* @returns {Promise<string>} S2S access token
*/
async function getS2STokenForScope(tenantId, scope) {
const clientId = process.env.BACKEND_APPID;
const clientSecret = process.env.BACKEND_CLIENT_SECRET;
if (!clientId || !clientSecret) {
throw new Error('BACKEND_APPID and BACKEND_CLIENT_SECRET required');
}
const tokenEndpoint = `${AAD_LOGIN_URL}/${tenantId}/oauth2/v2.0/token`;
const requestBody = new URLSearchParams({
grant_type: 'client_credentials',
client_id: clientId,
client_secret: clientSecret,
scope: scope
}).toString();
const response = await makeTokenRequest(tokenEndpoint, requestBody);
if (!response.access_token) {
throw new Error('S2S token acquisition failed');
}
return response.access_token;
}
/**
* Get Fabric S2S token
* @param {string} publisherTenantId - Publisher's tenant ID
* @returns {Promise<string>} Fabric S2S access token
*/
async function getFabricS2SToken(publisherTenantId) {
return getS2STokenForScope(publisherTenantId, FABRIC_SCOPE);
}
Δημιουργία σύνθετου διακριτικού για API Fabric
/**
* Build composite token for Fabric API calls
* @param {object} authContext - Authentication context
* @returns {Promise<string>} SubjectAndAppToken formatted header value
*/
async function buildCompositeToken(authContext) {
const { subjectToken, tenantId } = authContext;
const publisherTenantId = process.env.TENANT_ID;
if (!subjectToken) {
throw new Error('Subject token is required');
}
// Exchange user's subject token for Fabric OBO token
const fabricOboToken = await getFabricOboToken(subjectToken, tenantId);
// Acquire S2S token using publisher tenant
const fabricS2SToken = await getFabricS2SToken(publisherTenantId);
// Combine into SubjectAndAppToken format
return `SubjectAndAppToken1.0 subjectToken="${fabricOboToken}", appToken="${fabricS2SToken}"`;
}
Παράδειγμα κλήσης API Fabric
const axios = require('axios');
/**
* Call Fabric workload control API
*/
async function callFabricApi(authContext, workspaceId, itemId) {
// Build composite token
const authHeader = await buildCompositeToken(authContext);
// Call Fabric API
const response = await axios.get(
`https://api.fabric.microsoft.com/v1/workspaces/${workspaceId}/items/${itemId}`,
{
headers: {
'Authorization': authHeader,
'Content-Type': 'application/json'
}
}
);
return response.data;
}
Πλήρες παράδειγμα: Εκτέλεση εργασίας με έλεγχο ταυτότητας
Ακολουθεί ένα πλήρες παράδειγμα τελικού σημείου εκτέλεσης εργασίας με έλεγχο ταυτότητας:
const express = require('express');
const app = express();
app.post('/api/jobs/:jobType/instances/:instanceId', async (req, res) => {
// Authenticate the request from Fabric
const authenticated = await authenticateControlPlaneCall(req, res);
if (!authenticated) {
return; // Response already sent
}
const { jobType, instanceId } = req.params;
const { workspaceId, itemId } = req.body;
const { subjectToken, tenantId, userId } = req.authContext;
console.log(`Starting job ${jobType} for user ${userId}`);
try {
// Get OneLake token to access item data
const oneLakeToken = await getOneLakeToken(subjectToken, tenantId);
// Read data from OneLake
const itemData = await readItemData(oneLakeToken, workspaceId, itemId);
// Process the job
const result = await processJob(jobType, itemData);
// Write results back to OneLake
await writeResults(oneLakeToken, workspaceId, itemId, result);
// Return success
res.status(202).json({
status: 'InProgress',
instanceId: instanceId,
message: 'Job started successfully'
});
} catch (error) {
console.error(`Job ${instanceId} failed:`, error);
res.status(500).json({
status: 'Failed',
instanceId: instanceId,
error: error.message
});
}
});
Long-Running Λειτουργίες
Για μακροχρόνιες λειτουργίες όπως εργασίες, τα διακριτικά ενδέχεται να λήξουν πριν από την ολοκλήρωση. Εφαρμογή λογικής ανανέωσης διακριτικού:
// Store refresh tokens securely
const tokenCache = new Map();
/**
* Get cached token or acquire new one
*/
async function getOrRefreshToken(userToken, tenantId, scope, cacheKey) {
const cached = tokenCache.get(cacheKey);
// Check if cached token is still valid (with 5 min buffer)
if (cached && cached.expiresAt > Date.now() + 300000) {
return cached.token;
}
// Acquire new token
const response = await getTokenForScopeWithRefresh(userToken, tenantId, scope);
// Cache the token
tokenCache.set(cacheKey, {
token: response.access_token,
expiresAt: Date.now() + (response.expires_in * 1000)
});
return response.access_token;
}
Για περισσότερες πληροφορίες σχετικά με το χειρισμό μακροχρόνιων διεργασιών OBO, ανατρέξτε στην ενότητα Μακροχρόνιες διεργασίες OBO.
Βέλτιστες πρακτικές ασφάλειας
Αποθήκευση διακριτικών
- Ποτέ μην καταγράφετε πλήρη διακριτικά - καταγράψτε μόνο τους τελευταίους 4 χαρακτήρες για εντοπισμό σφαλμάτων
- Αποθηκεύστε τα διακριτικά με ασφάλεια μόνο στη μνήμη
- Διαγράψτε τα διακριτικά από τη μνήμη μετά τη χρήση
- Να μην διατηρούνται διακριτικά στο δίσκο ή στις βάσεις δεδομένων
Επικύρωση διακριτικού
- Να επικυρώνετε πάντα το appToken σε μορφή SubjectAndAppToken
- Επικυρώστε το subjectToken μόνο εάν υπάρχει
- Επαλήθευση υπογραφών διακριτικών σε κλειδιά Azure AD
- Ελέγξτε τη λήξη του διακριτικού και όχι πριν
- Επικύρωση αξιώσεων εκδότη, κοινού και μισθωτή
- Επαλήθευση απαιτούμενων αξιώσεων (scp, idtyp κ.λπ.)
Χειρισμός περιβάλλοντος χρήστη που λείπει
- Σχεδιάστε το απομακρυσμένο τελικό σημείο σας για να χειρίζεστε σενάρια τόσο με ανάθεση από τον χρήστη όσο και με σενάρια μόνο για εφαρμογές
- Ελέγξτε
hasSubjectContextτο authContext πριν αποκτήσετε πρόσβαση σε ιδιότητες για συγκεκριμένους χρήστες - Για λειτουργίες που απαιτούν περιβάλλον χρήστη, ορίστε
requireSubjectToken: trueτις επιλογές ενδιάμεσου λογισμικού - Για λειτουργίες διαγραφής ή λειτουργίες συστήματος, επιτρέψτε το subjectToken που λείπει
- Τεκμηριώστε ποιες λειτουργίες υποστηρίζουν περιβάλλον μόνο για εφαρμογές και ποιες απαιτούν περιβάλλον χρήστη
Ρύθμιση παραμέτρων περιβάλλοντος
// Required environment variables
const requiredEnvVars = [
'BACKEND_APPID', // Your workload's app registration ID
'BACKEND_CLIENT_SECRET', // Your app's client secret
'TENANT_ID', // Your publisher tenant ID
'BACKEND_AUDIENCE' // Expected token audience
];
// Validate on startup
requiredEnvVars.forEach(varName => {
if (!process.env[varName]) {
throw new Error(`Missing required environment variable: ${varName}`);
}
});
Χειρισμός σφαλμάτων και μετάδοση συναίνεσης
Όταν η ανταλλαγή διακριτικών αποτυγχάνει λόγω έλλειψης συναίνεσης ή εμβέλειας, το backend σας θα πρέπει να διαδώσει αυτά τα σφάλματα στο UX του frontend για να ζητήσει τη συγκατάθεση του χρήστη. Τα συνήθη σφάλματα που σχετίζονται με τη συναίνεση περιλαμβάνουν:
- AADSTS65001 - Ο χρήστης ή ο διαχειριστής δεν έχει συναινέσει στη χρήση της εφαρμογής
- AADSTS65005 - Η εφαρμογή απαιτεί συγκεκριμένα πεδία που δεν έχουν συναινέσει
/**
* Handle token exchange with consent error propagation
* Consent errors should be returned to the frontend to trigger consent flow
*/
async function handleTokenExchangeWithConsent(req, res) {
const { subjectToken, tenantId } = req.authContext;
const clientId = process.env.BACKEND_APPID;
try {
const token = await getTokenForScope(subjectToken, tenantId, ONELAKE_SCOPE);
// Use token for OneLake operations...
} catch (error) {
// Check for consent-related errors
if (error.message.includes('AADSTS65001') || error.message.includes('AADSTS65005')) {
// Extract scope from error if available
const requiredScope = extractScopeFromError(error.message) || 'https://storage.azure.com/.default';
// Build consent URL for the frontend to redirect the user
const consentUrl = buildConsentUrl({
clientId: clientId,
tenantId: tenantId,
scope: requiredScope,
redirectUri: process.env.FRONTEND_URL || 'https://your-frontend.azurewebsites.net'
});
// Return consent required response
return res.status(403).json({
error: 'ConsentRequired',
errorCode: error.message.includes('AADSTS65001') ? 'AADSTS65001' : 'AADSTS65005',
message: 'User consent is required to access this resource',
consentUrl: consentUrl,
requiredScope: requiredScope
});
}
// Other errors should be handled normally
throw error;
}
}
/**
* Build consent URL for user authorization
* @param {object} options - Consent URL options
* @returns {string} Formatted consent URL
*/
function buildConsentUrl(options) {
const { clientId, tenantId, scope, redirectUri } = options;
const params = new URLSearchParams({
client_id: clientId,
response_type: 'code',
redirect_uri: redirectUri,
response_mode: 'query',
scope: scope,
state: 'consent_required' // Your frontend can use this to handle the redirect
});
return `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/authorize?${params.toString()}`;
}
/**
* Extract scope from error message
*/
function extractScopeFromError(errorMessage) {
// Azure AD error messages often include the missing scope
const scopeMatch = errorMessage.match(/scope[s]?[:\s]+([^\s,]+)/);
return scopeMatch ? scopeMatch[1] : null;
}
Χειρισμός συναίνεσης Frontend
Όταν το frontend σας λαμβάνει ένα ConsentRequired σφάλμα, θα πρέπει να ανακατευθύνει τον χρήστη στη διεύθυνση URL συναίνεσης:
// Frontend code to handle consent errors
async function callBackendApi(endpoint, data) {
try {
const response = await fetch(endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
if (response.status === 403) {
const error = await response.json();
if (error.error === 'ConsentRequired' && error.consentUrl) {
// Redirect user to consent page
window.location.href = error.consentUrl;
return;
}
}
return await response.json();
} catch (error) {
console.error('API call failed:', error);
throw error;
}
}
Σημείωμα
Το δείγμα ανάπτυξης φόρτου εργασίας Fabric περιλαμβάνει πλήρη παραδείγματα χειρισμού σφαλμάτων συναίνεσης και μετάδοσης στο περιβάλλον χρήστη του frontend. Δείτε το ενδιάμεσο λογισμικό ελέγχου ταυτότητας και τον χειρισμό σφαλμάτων διεπαφής του δείγματος για υλοποιήσεις έτοιμες για παραγωγή.
Πρόσθετα σενάρια σφάλματος
// Handle various token exchange errors
try {
const token = await getTokenForScope(subjectToken, tenantId, scope);
} catch (error) {
const errorMessage = error.message;
if (errorMessage.includes('AADSTS65001') || errorMessage.includes('AADSTS65005')) {
// Consent required - propagate to frontend
return propagateConsentError(res, error, tenantId);
} else if (errorMessage.includes('AADSTS50013')) {
// Invalid assertion - token may be expired
return res.status(401).json({
error: 'InvalidToken',
message: 'The provided token is invalid or expired'
});
} else if (errorMessage.includes('AADSTS700016')) {
// Application not found in tenant
return res.status(400).json({
error: 'ApplicationNotFound',
message: 'Application is not configured in this tenant'
});
}
// Unknown error
throw error;
}
Επίλυση προβλημάτων
Συνήθη ζητήματα ελέγχου ταυτότητας
Σφάλμα: "Μη έγκυρη μορφή διακριτικού"
- Βεβαιωθείτε ότι η κεφαλίδα εξουσιοδότησης ξεκινά με
SubjectAndAppToken1.0 - Ελέγξτε ότι υπάρχουν τόσο το subjectToken όσο και το appToken
- Βεβαιωθείτε ότι τα διακριτικά αναφέρονται σωστά στην κεφαλίδα
Σφάλμα: "Η επικύρωση διακριτικού απέτυχε"
- Επαληθεύστε BACKEND_APPID αντιστοιχεί στην εγγραφή της εφαρμογής σας
- Ελέγξτε ότι BACKEND_AUDIENCE έχει ρυθμιστεί σωστά
- Βεβαιωθείτε ότι τα διακριτικά δεν έχουν λήξει
- Επαληθεύστε ότι ο εκδότης συμφωνεί με τον μισθωτή σας Azure AD
Σφάλμα: "Η ανταλλαγή διακριτικών απέτυχε: AADSTS65001"
- Απαιτείται η συναίνεση του χρήστη για το ζητούμενο εύρος
- Βεβαιωθείτε ότι η εγγραφή της εφαρμογής σας έχει τα απαραίτητα δικαιώματα API
- Επαληθεύστε ότι έχει χορηγηθεί συγκατάθεση διαχειριστή, εάν απαιτείται
Σφάλμα: "Το διακριτικό εφαρμογής δεν προέρχεται από το Fabric"
- Επαληθεύστε ότι η αξίωση του
appidappToken συμφωνεί με το αναγνωριστικό εφαρμογής του Fabric - Βεβαιωθείτε ότι κάνετε δοκιμές στο σωστό περιβάλλον (ανάπτυξη/παραγωγή)