你当前正在访问 Microsoft Azure Global Edition 技术文档网站。 如果需要访问由世纪互联运营的 Microsoft Azure 中国技术文档网站,请访问 https://docs.azure.cn

保护 Webhook 终结点和 WebSocket 连接

确保从头到尾传递消息对于确保系统之间传输的敏感信息的保密性、完整性和可信度至关重要。 你的信任能力和意愿取决于远程系统发送者提供其身份信息。 通话自动化有两种通信可保护的事件的方法:Azure 事件网格发送的共享 IncomingCall 事件,以及通话自动化平台通过 Webhook 发送的所有其他中通话事件。

来电事件

Azure 通信服务依赖于 Azure 事件网格订阅来传送 IncomingCall 事件。 有关详细信息,请参阅将事件传送到 Microsoft Entra 保护的终结点

通话自动化 Webhook 事件

呼叫自动化事件 将发送到在接听呼叫或发出新的出站呼叫时指定的 Webhook 回调 URI。 回调 URI 必须是一个公共终结点,具备有效的 HTTPS 证书、域名系统名称和 IP 地址,并且防火墙端口已正确配置,以便呼叫自动化能够访问。 如果不采取必要的步骤保护它免受未经授权的访问,此匿名公共 Web 服务器可能会造成安全风险。

改进通话自动化 Webhook 回拨安全设置

在入站 HTTPS 请求的身份验证标头中,通话自动化发送的每个通话中 Webhook 回叫都使用已签名的 JSON Web 令牌 (JWT)。 可以使用标准 OpenID Connect (OIDC) JWT 验证技术来确保令牌的完整性。 JWT 的有效期为 5 分钟,并为发送到回调 URI 的每个事件创建一个新令牌。

  1. 获取 Open ID 配置 URL:<https://acscallautomation.communication.azure.com/calling/.well-known/acsopenidconfiguration>
  2. 安装 Microsoft.AspNetCore.Authentication.JwtBearer NuGet 包。
  3. 将应用程序配置为使用 NuGet 包和 Azure 通信服务资源的配置来验证 JWT。 需要获取在 JWT 有效负载中出现的 audience 值。
  4. 验证颁发者、受众和 JWT:
    • 受众是你用于设置通话自动化客户端的 Azure 通信服务资源 ID。 有关如何获取它的信息,请参阅 “获取 Azure 资源 ID”。
    • OpenId 配置中的 JSON Web 密钥集终结点包含用于验证 JWT 的密钥。 当签名有效且令牌未过期(在生成后的五分钟内),客户端可以使用令牌进行授权。

此示例代码演示如何使用 Microsoft.IdentityModel.Protocols.OpenIdConnect 来验证 Webhook 负载:

using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Protocols;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
using Microsoft.IdentityModel.Tokens;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

// Add Azure Communication Services CallAutomation OpenID configuration
var configurationManager = new ConfigurationManager<OpenIdConnectConfiguration>(
            builder.Configuration["OpenIdConfigUrl"],
            new OpenIdConnectConfigurationRetriever());
var configuration = configurationManager.GetConfigurationAsync().Result;

builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.Configuration = configuration;
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidAudience = builder.Configuration["AllowedAudience"]
        };
    });

builder.Services.AddAuthorization();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();

app.MapPost("/api/callback", (CloudEvent[] events) =>
{
    // Your implementation on the callback event
    return Results.Ok();
})
.RequireAuthorization()
.WithOpenApi();

app.UseAuthentication();
app.UseAuthorization();

app.Run();

改进通话自动化 Webhook 回拨安全设置

在入站 HTTPS 请求的身份验证标头中,通话自动化发送的每个通话中 Webhook 回叫都使用已签名的 JSON Web 令牌 (JWT)。 可以使用标准 OpenID Connect (OIDC) JWT 验证技术来确保令牌的完整性。 JWT 的有效期为 5 分钟,并为发送到回调 URI 的每个事件创建一个新令牌。

  1. 获取 Open ID 配置 URL:<https://acscallautomation.communication.azure.com/calling/.well-known/acsopenidconfiguration>

  2. 下面的示例使用 Spring 框架,该框架是将 Spring initializr 与 Maven 结合使用作为项目生成工具创建的。

  3. 在你的 pom.xml 中添加以下依赖项:

      <dependency>
       <groupId>org.springframework.boot</groupId>
       <artifactId>spring-boot-starter-security</artifactId>
      </dependency>
      <dependency>
       <groupId>org.springframework.security</groupId>
       <artifactId>spring-security-oauth2-jose</artifactId>
      </dependency>
      <dependency>
       <groupId>org.springframework.security</groupId>
       <artifactId>spring-security-oauth2-resource-server</artifactId>
      </dependency>
    
  4. 将应用程序配置为验证 JWT 和 Azure 通信服务资源的配置。 需要获取在 JWT 有效负载中出现的 audience 值。

  5. 验证颁发者、受众和 JWT:

    • 受众是你用于设置通话自动化客户端的 Azure 通信服务资源 ID。 有关如何获取它的信息,请参阅 “获取 Azure 资源 ID”。
    • OpenID 配置中的 JSON Web 密钥集终结点包含用于验证 JWT 的密钥。 当签名有效且令牌未过期(在生成后的五分钟内),客户端可以使用令牌进行授权。

此示例代码演示如何使用 JWT 配置 OIDC 客户端以验证 Webhook 有效负载:

package callautomation.example.security;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult;
import org.springframework.security.oauth2.jwt.*;

@EnableWebSecurity
public class TokenValidationConfiguration {
    @Value("ACS resource ID")
    private String audience;

    @Value("https://acscallautomation.communication.azure.com")
    private String issuer;

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .mvcMatchers("/api/callbacks").permitAll()
                .anyRequest()
                .and()
                .oauth2ResourceServer()
                .jwt()
                .decoder(jwtDecoder());

        return http.build();
    }

    class AudienceValidator implements OAuth2TokenValidator<Jwt> {
        private String audience;

        OAuth2Error error = new OAuth2Error("invalid_token", "The required audience is missing", null);

        public AudienceValidator(String audience) {
            this.audience = audience;
        }

        @Override
        public OAuth2TokenValidatorResult validate(Jwt token) {
            if (token.getAudience().contains(audience)) {
                return OAuth2TokenValidatorResult.success();
            } else {
                return OAuth2TokenValidatorResult.failure(error);
            }
        }
    }

    JwtDecoder jwtDecoder() {
        OAuth2TokenValidator<Jwt> withAudience = new AudienceValidator(audience);
        OAuth2TokenValidator<Jwt> withIssuer = JwtValidators.createDefaultWithIssuer(issuer);
        OAuth2TokenValidator<Jwt> validator = new DelegatingOAuth2TokenValidator<>(withAudience, withIssuer);

        NimbusJwtDecoder jwtDecoder = (NimbusJwtDecoder) JwtDecoders.fromOidcIssuerLocation(issuer);
        jwtDecoder.setJwtValidator(validator);

        return jwtDecoder;
    }
}

改进通话自动化 Webhook 回拨安全设置

在入站 HTTPS 请求的身份验证标头中,通话自动化发送的每个通话中 Webhook 回叫都使用已签名的 JSON Web 令牌 (JWT)。 可以使用标准 OpenID Connect (OIDC) JWT 验证技术来确保令牌的完整性。 JWT 的有效期为 5 分钟,并为发送到回调 URI 的每个事件创建一个新令牌。

  1. 获取 Open ID 配置 URL:<https://acscallautomation.communication.azure.com/calling/.well-known/acsopenidconfiguration>

  2. 安装以下包:

    npm install express jwks-rsa jsonwebtoken
    
  3. 将应用程序配置为验证 JWT 和 Azure 通信服务资源的配置。 需要获取在 JWT 有效负载中出现的 audience 值。

  4. 验证颁发者、受众和 JWT:

    • 受众是你用于设置通话自动化客户端的 Azure 通信服务资源 ID。 有关如何获取它的信息,请参阅 “获取 Azure 资源 ID”。
    • OpenID 配置中的 JSON Web 密钥集终结点包含用于验证 JWT 的密钥。 当签名有效且令牌未过期(在生成后的五分钟内),客户端可以使用令牌进行授权。

此示例代码演示如何使用 JWT 配置 OIDC 客户端以验证 Webhook 有效负载:

import express from "express";
import { JwksClient } from "jwks-rsa";
import { verify } from "jsonwebtoken";

const app = express();
const port = 3000;
const audience = "ACS resource ID";
const issuer = "https://acscallautomation.communication.azure.com";

app.use(express.json());

app.post("/api/callback", (req, res) => {
    const token = req?.headers?.authorization?.split(" ")[1] || "";

    if (!token) {
        res.sendStatus(401);

        return;
    }

    try {
        verify(
            token,
            (header, callback) => {
                const client = new JwksClient({
                    jwksUri: "https://acscallautomation.communication.azure.com/calling/keys",
                });

                client.getSigningKey(header.kid, (err, key) => {
                    const signingKey = key?.publicKey || key?.rsaPublicKey;

                    callback(err, signingKey);
                });
            },
            {
                audience,
                issuer,
                algorithms: ["RS256"],
            });
        // Your implementation on the callback event
        res.sendStatus(200);
    } catch (error) {
        res.sendStatus(401);
    }
});

app.listen(port, () => {
    console.log(`Server running on port ${port}`);
});

改进通话自动化 Webhook 回拨安全设置

在入站 HTTPS 请求的身份验证标头中,通话自动化发送的每个通话中 Webhook 回叫都使用已签名的 JSON Web 令牌 (JWT)。 可以使用标准 OpenID Connect (OIDC) JWT 验证技术来确保令牌的完整性。 JWT 的有效期为 5 分钟,并为发送到回调 URI 的每个事件创建一个新令牌。

  1. 获取 Open ID 配置 URL:<https://acscallautomation.communication.azure.com/calling/.well-known/acsopenidconfiguration>

  2. 安装以下包:

    pip install flask pyjwt
    
  3. 将应用程序配置为验证 JWT 和 Azure 通信服务资源的配置。 需要获取在 JWT 有效负载中出现的 audience 值。

  4. 验证颁发者、受众和 JWT:

    • 受众是你用于设置通话自动化客户端的 Azure 通信服务资源 ID。 有关如何获取它的信息,请参阅 “获取 Azure 资源 ID”。
    • OpenId 配置中的 JSON Web 密钥集终结点包含用于验证 JWT 的密钥。 当签名有效且令牌未过期(在生成后的五分钟内),客户端可以使用令牌进行授权。

此示例代码演示如何使用 JWT 配置 OIDC 客户端以验证 Webhook 有效负载:

from flask import Flask, jsonify, abort, request
import jwt

app = Flask(__name__)


@app.route("/api/callback", methods=["POST"])
def handle_callback_event():
    token = request.headers.get("authorization").split()[1]

    if not token:
        abort(401)

    try:
        jwks_client = jwt.PyJWKClient(
            "https://acscallautomation.communication.azure.com/calling/keys"
        )
        jwt.decode(
            token,
            jwks_client.get_signing_key_from_jwt(token).key,
            algorithms=["RS256"],
            issuer="https://acscallautomation.communication.azure.com",
            audience="ACS resource ID",
        )
        # Your implementation on the callback event
        return jsonify(success=True)
    except jwt.InvalidTokenError:
        print("Token is invalid")
        abort(401)
    except Exception as e:
        print("uncaught exception" + e)
        abort(500)


if __name__ == "__main__":
    app.run()

重要

我们的服务在身份验证标头中使用标准 JSON Web 令牌,并且仅支持 OpenID Connect (OIDC) JWT 验证。

查询参数令牌身份验证

查询参数令牌身份验证是通过将预先共享的机密令牌追加到 Webhook 终结点 URL 作为查询字符串参数来保护 Webhook 回调的简单方法。 此令牌充当轻型身份验证密钥,使系统能够验证 Webhook 回调事件是否源自调用自动化服务。

https://api.example.com/webhook?token=8f2d9c63a7b14d32b53c9e12a1f47fcb

收到 webhook 回叫事件时,通话自动化服务会包含与你所配置的完全一致的令牌(参见上面的示例)。 收到请求后,系统将查询参数中的令牌与存储的受信任值进行比较。 应拒绝没有令牌或值不正确的请求。

通话自动化 Websocket 事件

WebSocket 标头中的身份验证令牌

通话自动化的每个 WebSocket 连接请求现在都在身份验证标头中包含已签名的 JWT。 此令牌使用标准 OIDC JWT 验证方法进行验证:

  • JWT 的生存期为 24 小时。
  • 将为 WebSocket 服务器的每个连接请求生成一个新令牌。

WebSocket 代码示例

此示例代码演示如何使用 JWT 令牌对 WebSocket 连接请求进行身份验证。

// 1. Load OpenID Connect metadata
var configurationManager = new ConfigurationManager<OpenIdConnectConfiguration>(
    builder.Configuration["OpenIdConfigUrl"],
    new OpenIdConnectConfigurationRetriever());

var openIdConfig = await configurationManager.GetConfigurationAsync();
// 2. Register JWT authentication
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.Configuration = openIdConfig;
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidAudience = builder.Configuration["AllowedAudience"]
        };
    });

builder.Services.AddAuthorization();

var app = builder.Build();

// 3. Use authentication & authorization middleware
app.UseAuthentication();
app.UseAuthorization();

app.UseWebSockets();

// 4. WebSocket token validation manually in middleware
app.Use(async (context, next) =>
{
    if (context.Request.Path != "/ws")
    {
        await next(context);
        return;
    }

    if (!context.WebSockets.IsWebSocketRequest)
    {
        context.Response.StatusCode = StatusCodes.Status400BadRequest;
        await context.Response.WriteAsync("WebSocket connection expected.");
        return;
    }

    var result = await context.AuthenticateAsync();
    if (!result.Succeeded)
    {
        context.Response.StatusCode = StatusCodes.Status401Unauthorized;
        await context.Response.WriteAsync("Unauthorized WebSocket connection.");
        return;
    }

    context.User = result.Principal;

    // Optional: Log headers
    var correlationId = context.Request.Headers["x-ms-call-correlation-id"].FirstOrDefault();
    var callConnectionId = context.Request.Headers["x-ms-call-connection-id"].FirstOrDefault();

    Console.WriteLine($"Authenticated WebSocket - Correlation ID: {correlationId ?? "not provided"}");
    Console.WriteLine($"Authenticated WebSocket - CallConnection ID: {callConnectionId ?? "not provided"}");

    // Now you can safely accept the WebSocket and process the connection
    // var webSocket = await context.WebSockets.AcceptWebSocketAsync();
    // var mediaService = new AcsMediaStreamingHandler(webSocket, builder.Configuration);
    // await mediaService.ProcessWebSocketAsync();
});

WebSocket 代码示例

此示例演示如何配置 OIDC 客户端以使用 JWT 验证 WebSocket 连接请求。

const audience = "ACS resource ID";
const issuer = "https://acscallautomation.communication.azure.com";

const jwksClient = new JwksClient({
  jwksUri: "https://acscallautomation.communication.azure.com/calling/keys",
});

wss.on("connection", async (ws, req) => {
  try {
    const authHeader = req.headers?.authorization || "";
    const token = authHeader.split(" ")[1];

    if (!token) {
      ws.close(1008, "Unauthorized");
      return;
    }

    verify(
      token,
      async (header, callback) => {
        try {
          const key = await jwksClient.getSigningKey(header.kid);
          const signingKey = key.getPublicKey();
          callback(null, signingKey);
        } catch (err) {
          callback(err);
        }
      },
      {
        audience,
        issuer,
        algorithms: ["RS256"],
      },
      (err, decoded) => {
        if (err) {
          console.error("WebSocket authentication failed:", err);
          ws.close(1008, "Unauthorized");
          return;
        }

        console.log(
          "Authenticated WebSocket connection with decoded JWT payload:",
          decoded
        );

        ws.on("message", async (message) => {
          // Process message
        });

        ws.on("close", () => {
          console.log("WebSocket connection closed");
        });
      }
    );
  } catch (err) {
    console.error("Unexpected error during WebSocket setup:", err);
    ws.close(1011, "Internal Server Error"); // 1011 = internal error
  }
});

WebSocket 代码示例

此示例演示如何配置符合 OIDC 的客户端,以使用 JWT 验证 WebSocket 连接请求。

请确保安装所需的包: pip install cryptography

JWKS_URL = "https://acscallautomation.communication.azure.com/calling/keys"
ISSUER = "https://acscallautomation.communication.azure.com"
AUDIENCE = "ACS resource ID”

@app.websocket('/ws')
async def ws():
    try:
        auth_header = websocket.headers.get("Authorization")
        if not auth_header or not auth_header.startswith("Bearer "):
            await websocket.close(1008)  # Policy violation
            return

        token = auth_header.split()[1]

        jwks_client = PyJWKClient(JWKS_URL)
        signing_key = jwks_client.get_signing_key_from_jwt(token)

        decoded = jwt.decode(
            token,
            signing_key.key,
            algorithms=["RS256"],
            issuer=ISSUER,
            audience=AUDIENCE,
        )

        app.logger.info(f"Authenticated WebSocket connection with decoded JWT payload: {decoded}")
        await websocket.send("Connection authenticated.")

        while True:
            data = await websocket.receive()
            # Process incoming data

    except InvalidTokenError as e:
        app.logger.warning(f"Invalid token: {e}")
        await websocket.close(1008)
    except Exception as e:
        app.logger.error(f"Uncaught exception: {e}")
        await websocket.close(1011)  # Internal error

IP 范围

保护 WebSocket 连接的另一种方法是仅允许 Microsoft 连接来自某些 IP 范围。

Category IP 范围或 FQDN Ports
通话自动化媒体 52.112.0.0/14, 52.122.0.0/15, 2603:1063::/38 UDP:3478、3479、3480、3481
通话自动化回调 URL *.lync.com、*.teams.cloud.microsoft、*.teams.microsoft.com、teams.cloud.microsoft、teams.microsoft.com、52.112.0.0/14、52.122.0.0/15、2603:1027::/48、2603:1037::/48、2603:1047::/48、2603:1057::/48、2603:1063::/38、2620:1ec:6::/48、2620:1ec:40::/42 TCP:443、80 UDP:443