康心伴Logo
康心伴WellAlly
Data & Privacy

HealthTech API 安全最佳实践:OAuth 2.0 + mTLS 完整指南

5 分钟阅读

HealthTech API 安全最佳实践:OAuth 2.0 + mTLS 完整指南

概述

医疗健康 API 处理的是最敏感的个人数据,必须实施严格的安全措施。本文将详细介绍如何使用 OAuth 2.0 和 mTLS(双向 TLS)构建符合 HIPAA 和 GDPR 标准的安全 API。

安全威胁模型

威胁类型风险缓解措施
数据泄露健康数据暴露加密、访问控制、审计
身份伪造未经授权访问mTLS、OAuth 2.0
中间人攻击数据拦截TLS 1.3、证书固定
重放攻击请求重复时间戳、nonce
DDoS 攻击服务拒绝速率限制、API 网关

合规要求

  • HIPAA:加密传输、访问审计、最小权限原则
  • GDPR:数据保护、用户同意、被遗忘权
  • SOC 2:安全控制、访问管理、变更管理
  • ISO 27001:信息安全管理体系

OAuth 2.0 架构

1. 授权服务器配置

code
// lib/oauth/authorization-server.ts
import { Provider } from 'oidc-provider';
import { adapter } from './oidc-adapter';

export class AuthorizationServer {
  private static instance: Provider;

  static getInstance(): Provider {
    if (!this.instance) {
      this.instance = new Provider('https://api.healthtech.com', {
        adapter,
        clients: [
          {
            client_id: 'web-app',
            client_secret: process.env.WEB_APP_SECRET,
            redirect_uris: ['https://app.healthtech.com/callback'],
            grant_types: ['authorization_code', 'refresh_token'],
            response_types: ['code'],
          },
          {
            client_id: 'mobile-app',
            redirect_uris: ['healthtech://auth/callback'],
            grant_types: ['authorization_code', 'refresh_token'],
            response_types: ['code'],
            token_endpoint_auth_method: 'none', // Public client (PKCE)
          },
          {
            client_id: 'integration-partner',
            client_secret: process.env.PARTNER_SECRET,
            redirect_uris: ['https://partner.com/callback'],
            grant_types: ['client_credentials'],
            scope: 'api:read api:write',
          },
        ],
        scopes: ['openid', 'profile', 'email', 'api:read', 'api:write', 'health:data'],
        interactions: {
          url(ctx, interaction) {
            return `/interaction/${interaction.uid}`;
          },
        },
        features: {
          // 启用 PKCE
          pkce: {
            required: (ctx, client) => client.token_endpoint_auth_method === 'none',
          },
          // 启用设备授权流程
          deviceFlow: { enabled: true },
          // 启用 JWT Introspection
          introspection: { enabled: true },
          // 启用撤销
          revocation: { enabled: true },
        },
        cookies: {
          long: {
            signed: true,
            secure: true,
            sameSite: 'strict',
            httpOnly: true,
            maxAge: 30 * 24 * 60 * 60, // 30 days
          },
          short: {
            signed: true,
            secure: true,
            sameSite: 'strict',
            httpOnly: true,
            maxAge: 600, // 10 minutes
          },
        },
        ttl: {
          AuthorizationCode: 600, // 10 minutes
          IdToken: 3600, // 1 hour
          AccessToken: 3600, // 1 hour
          RefreshToken: 30 * 24 * 60 * 60, // 30 days
        },
      });
    }

    return this.instance;
  }
}
Code collapsed

2. OAuth 中间件

code
// middleware/oauth-auth.ts
import { NextRequest, NextResponse } from 'next/server';
import { AuthorizationServer } from '@/lib/oauth/authorization-server';

export interface AuthContext {
  userId: string;
  tenantId?: string;
  scopes: string[];
  clientId: string;
}

/**
 * OAuth 2.0 Bearer Token 验证中间件
 */
export async function oauthAuth(
  request: NextRequest,
  requiredScopes: string[] = []
): Promise<NextResponse | AuthContext> {
  const authHeader = request.headers.get('Authorization');

  if (!authHeader?.startsWith('Bearer ')) {
    return NextResponse.json(
      { error: 'unauthorized', error_description: '缺少或无效的 Authorization 头' },
      { status: 401 }
    );
  }

  const token = authHeader.substring(7);

  try {
    const provider = AuthorizationServer.getInstance();

    // 验证访问令牌
    const result = await provider.introspect(token);

    if (!result.active) {
      return NextResponse.json(
        { error: 'invalid_token', error_description: '令牌已过期或被撤销' },
        { status: 401 }
      );
    }

    // 验证所需范围
    const tokenScopes = result.scope?.split(' ') || [];
    const hasRequiredScopes = requiredScopes.every(scope =>
      tokenScopes.includes(scope)
    );

    if (!hasRequiredScopes) {
      return NextResponse.json(
        {
          error: 'insufficient_scope',
          error_description: `需要以下权限: ${requiredScopes.join(', ')}`
        },
        { status: 403 }
      );
    }

    return {
      userId: result.sub,
      tenantId: result.tid,
      scopes: tokenScopes,
      clientId: result.client_id,
    };
  } catch (error) {
    console.error('OAuth 验证错误:', error);
    return NextResponse.json(
      { error: 'server_error', error_description: '令牌验证失败' },
      { status: 500 }
    );
  }
}

/**
 * 检查特定权限
 */
export function hasScope(authContext: AuthContext, scope: string): boolean {
  return authContext.scopes.includes(scope);
}

/**
 * 检查租户访问权限
 */
export function hasTenantAccess(
  authContext: AuthContext,
  tenantId: string
): boolean {
  return authContext.tenantId === tenantId || authContext.scopes.includes('admin');
}
Code collapsed

3. API 路由保护

code
// app/api/health-data/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { oauthAuth, hasScope } from '@/middleware/oauth-auth';

/**
 * 获取健康数据(需要 api:read 权限)
 */
export async function GET(request: NextRequest) {
  // 验证 OAuth 令牌
  const authResult = await oauthAuth(request, ['api:read']);

  if (authResult instanceof NextResponse) {
    return authResult; // 返回错误响应
  }

  const { userId, tenantId, scopes } = authResult;

  try {
    // 获取健康数据
    const healthData = await getHealthData(userId, tenantId);

    return NextResponse.json({
      data: healthData,
      meta: {
        requested_at: new Date().toISOString(),
      },
    });
  } catch (error) {
    return NextResponse.json(
      { error: '获取数据失败' },
      { status: 500 }
    );
  }
}

/**
 * 创建健康记录(需要 api:write 权限)
 */
export async function POST(request: NextRequest) {
  const authResult = await oauthAuth(request, ['api:write']);

  if (authResult instanceof NextResponse) {
    return authResult;
  }

  const { userId, tenantId } = authResult;
  const data = await request.json();

  // 验证数据结构
  const validationResult = validateHealthData(data);
  if (!validationResult.valid) {
    return NextResponse.json(
      { error: 'validation_error', details: validationResult.errors },
      { status: 400 }
    );
  }

  try {
    const record = await createHealthRecord({
      userId,
      tenantId,
      ...data,
    });

    return NextResponse.json(
      { data: record },
      { status: 201 }
    );
  } catch (error) {
    return NextResponse.json(
      { error: '创建记录失败' },
      { status: 500 }
    );
  }
}
Code collapsed

mTLS 双向认证

1. 证书生成

code
#!/bin/bash
# scripts/generate-mtls-certs.sh

set -e

# CA 证书
openssl genrsa -out ca-key.pem 4096
openssl req -new -x509 -days 3650 \
  -key ca-key.pem -out ca-cert.pem \
  -subj "/C=CN/ST=Beijing/L=Beijing/O=HealthTech/CN=HealthTech CA"

# 服务器证书
openssl genrsa -out server-key.pem 4096
openssl req -new -key server-key.pem -out server-csr.pem \
  -subj "/C=CN/ST=Beijing/L=Beijing/O=HealthTech/CN=api.healthtech.com"
openssl x509 -req -days 365 \
  -in server-csr.pem \
  -CA ca-cert.pem -CAkey ca-key.pem -CAcreateserial \
  -out server-cert.pem \
  -extfile server-ext.cnf

# 客户端证书(用于集成伙伴)
openssl genrsa -out client-key.pem 4096
openssl req -new -key client-key.pem -out client-csr.pem \
  -subj "/C=CN/ST=Beijing/L=Beijing/O=HealthTech/CN=integration-client"
openssl x509 -req -days 365 \
  -in client-csr.pem \
  -CA ca-cert.pem -CAkey ca-key.pem \
  -out client-cert.pem

echo "证书生成完成"
echo "CA 证书: ca-cert.pem"
echo "服务器证书: server-cert.pem"
echo "服务器私钥: server-key.pem"
echo "客户端证书: client-cert.pem"
echo "客户端私钥: client-key.pem"
Code collapsed

2. Node.js mTLS 服务器

code
// lib/mtls/mtls-server.ts
import https from 'https';
import fs from 'fs';
import { NextApiRequest, NextApiResponse } from 'next';

export interface MtlsConfig {
  caCert: string;
  serverCert: string;
  serverKey: string;
  requestCert?: boolean;
  rejectUnauthorized?: boolean;
}

export class MtlsServer {
  private server: https.Server;

  constructor(config: MtlsConfig, handler: (req: any, res: any) => void) {
    const tlsOptions = {
      ca: fs.readFileSync(config.caCert),
      cert: fs.readFileSync(config.serverCert),
      key: fs.readFileSync(config.serverKey),
      requestCert: config.requestCert ?? true,
      rejectUnauthorized: config.rejectUnauthorized ?? true,
      minVersion: 'TLSv1.3',
      ciphers: [
        'TLS_AES_256_GCM_SHA384',
        'TLS_CHACHA20_POLY1305_SHA256',
        'TLS_AES_128_GCM_SHA256',
      ].join(':'),
    };

    this.server = https.createServer(tlsOptions, handler);
  }

  listen(port: number): Promise<void> {
    return new Promise((resolve) => {
      this.server.listen(port, () => {
        console.log(`mTLS 服务器监听端口 ${port}`);
        resolve();
      });
    });
  }

  close(): Promise<void> {
    return new Promise((resolve) => {
      this.server.close(() => resolve());
    });
  }
}

/**
 * 从请求中提取客户端证书信息
 */
export function extractClientInfo(req: https.IncomingMessage): {
  subject: string;
  issuer: string;
  serialNumber: string;
  validFrom: Date;
  validTo: Date;
} | null {
  const cert = req.socket.getPeerCertificate();

  if (!cert || Object.keys(cert).length === 0) {
    return null;
  }

  return {
    subject: cert.subject.CN,
    issuer: cert.issuer.CN,
    serialNumber: cert.serialNumber,
    validFrom: new Date(cert.valid_from),
    validTo: new Date(cert.valid_to),
  };
}

/**
 * 验证客户端证书
 */
export function validateClientCert(
  req: https.IncomingMessage,
  allowedSerials: string[]
): boolean {
  const cert = req.socket.getPeerCertificate();

  if (!cert || Object.keys(cert).length === 0) {
    return false;
  }

  // 检查证书是否过期
  const now = new Date();
  const validFrom = new Date(cert.valid_from);
  const validTo = new Date(cert.valid_to);

  if (now < validFrom || now > validTo) {
    return false;
  }

  // 检查序列号是否在允许列表中
  return allowedSerials.includes(cert.serialNumber);
}
Code collapsed

3. mTLS + OAuth 组合认证

code
// middleware/combined-auth.ts
import { NextRequest } from 'next/server';
import { oauthAuth } from './oauth-auth';
import { extractClientInfo, validateClientCert } from '@/lib/mtls/mtls-server';

export enum AuthMethod {
  OAUTH = 'oauth',
  MTLS = 'mtls',
  BOTH = 'both',
}

export interface CombinedAuthContext {
  method: AuthMethod;
  userId?: string;
  clientCert?: {
    subject: string;
    serialNumber: string;
  };
  scopes: string[];
}

/**
 * 组合认证:mTLS + OAuth
 */
export async function combinedAuth(
  request: NextRequest,
  requiredScopes: string[] = [],
  allowedCertSerials: string[] = []
): Promise<NextResponse | CombinedAuthContext> {
  const authHeader = request.headers.get('Authorization');
  const clientCertHeader = request.headers.get('X-Client-Cert');

  // 情况 1: 仅 OAuth
  if (authHeader?.startsWith('Bearer ') && !clientCertHeader) {
    const oauthResult = await oauthAuth(request, requiredScopes);

    if (oauthResult instanceof NextResponse) {
      return oauthResult;
    }

    return {
      method: AuthMethod.OAUTH,
      userId: oauthResult.userId,
      scopes: oauthResult.scopes,
    };
  }

  // 情况 2: 仅 mTLS(用于服务间通信)
  if (clientCertHeader && !authHeader) {
    // 验证客户端证书
    const cert = parseClientCertHeader(clientCertHeader);

    if (!cert || !allowedCertSerials.includes(cert.serialNumber)) {
      return NextResponse.json(
        { error: 'invalid_client_certificate' },
        { status: 401 }
      );
    }

    return {
      method: AuthMethod.MTLS,
      clientCert: {
        subject: cert.subject,
        serialNumber: cert.serialNumber,
      },
      scopes: ['api:read', 'api:write'], // mTLS 客户端默认获得完整权限
    };
  }

  // 情况 3: 同时使用 mTLS 和 OAuth(最高安全级别)
  if (authHeader && clientCertHeader) {
    // 验证证书
    const cert = parseClientCertHeader(clientCertHeader);

    if (!cert || !allowedCertSerials.includes(cert.serialNumber)) {
      return NextResponse.json(
        { error: 'invalid_client_certificate' },
        { status: 401 }
      );
    }

    // 验证 OAuth 令牌
    const oauthResult = await oauthAuth(request, requiredScopes);

    if (oauthResult instanceof NextResponse) {
      return oauthResult;
    }

    return {
      method: AuthMethod.BOTH,
      userId: oauthResult.userId,
      clientCert: {
        subject: cert.subject,
        serialNumber: cert.serialNumber,
      },
      scopes: oauthResult.scopes,
    };
  }

  // 无认证信息
  return NextResponse.json(
    { error: 'unauthorized', error_description: '缺少认证信息' },
    { status: 401 }
  );
}

function parseClientCertHeader(header: string): any {
  try {
    return JSON.parse(Buffer.from(header, 'base64').toString());
  } catch {
    return null;
  }
}
Code collapsed

4. API 网关集成

code
// lib/api-gateway/gateway.ts
import { createProxyMiddleware } from 'http-proxy-middleware';
import { combinedAuth } from '@/middleware/combined-auth';

export const apiGateway = createProxyMiddleware({
  target: process.env.BACKEND_URL,
  changeOrigin: true,
  pathRewrite: {
    '^/api': '',
  },
  onProxyReq: (proxyReq, req, res) => {
    // 添加请求 ID 用于追踪
    proxyReq.setHeader('X-Request-ID', generateRequestId());

    // 添加客户端信息
    const clientIp = req.socket.remoteAddress;
    proxyReq.setHeader('X-Forwarded-For', clientIp);

    // 添加认证信息到后端
    const authContext = (req as any).authContext;
    if (authContext) {
      proxyReq.setHeader('X-Auth-Method', authContext.method);
      proxyReq.setHeader('X-User-ID', authContext.userId || '');
      proxyReq.setHeader('X-Client-Serial', authContext.clientCert?.serialNumber || '');
    }
  },
  onProxyRes: (proxyRes, req, res) => {
    // 添加安全头
    proxyRes.headers['X-Content-Type-Options'] = 'nosniff';
    proxyRes.headers['X-Frame-Options'] = 'DENY';
    proxyRes.headers['X-XSS-Protection'] = '1; mode=block';
    proxyRes.headers['Strict-Transport-Security'] = 'max-age=31536000; includeSubDomains';

    // 记录响应日志
    logApiResponse(req, proxyRes);
  },
  onError: (err, req, res) => {
    console.error('API Gateway 错误:', err);
    res.status(500).json({ error: 'gateway_error' });
  },
});

/**
 * API 路由配置
 */
export const gatewayRoutes = [
  {
    path: '/api/health',
    auth: {
      required: true,
      scopes: ['api:read'],
      methods: ['both'], // oauth, mtls, or both
    },
    rateLimit: {
      windowMs: 60000, // 1 minute
      max: 100,
    },
  },
  {
    path: '/api/health',
    auth: {
      required: true,
      scopes: ['api:write'],
      methods: ['oauth', 'both'], // oauth or both only
    },
    rateLimit: {
      windowMs: 60000,
      max: 20,
    },
    methods: ['POST', 'PUT', 'DELETE'],
  },
];
Code collapsed

速率限制与 DDoS 防护

code
// lib/rate-limit/redis-store.ts
import Redis from 'ioredis';

const redis = new Redis(process.env.REDIS_URL);

export interface RateLimitConfig {
  keyPrefix: string;
  windowMs: number;
  maxRequests: number;
  skipSuccessfulRequests?: boolean;
  skipFailedRequests?: boolean;
}

export class RateLimiter {
  private config: RateLimitConfig;

  constructor(config: RateLimitConfig) {
    this.config = config;
  }

  async check(
    identifier: string,
    options?: { skip?: boolean }
  ): Promise<{
    allowed: boolean;
    limit: number;
    remaining: number;
    resetTime: Date;
  }> {
    const { keyPrefix, windowMs, maxRequests } = this.config;
    const key = `${keyPrefix}:${identifier}`;
    const now = Date.now();
    const windowStart = now - windowMs;

    // 使用 Redis Sorted Set 记录请求时间戳
    const pipeline = redis.pipeline();

    // 删除窗口外的旧记录
    pipeline.zremrangebyscore(key, 0, windowStart);

    // 添加当前请求
    if (!options?.skip) {
      pipeline.zadd(key, now, `${now}-${Math.random()}`);
    }

    // 计算当前窗口内的请求数
    pipeline.zcard(key);

    // 设置过期时间
    pipeline.expire(key, Math.ceil(windowMs / 1000));

    const results = await pipeline.exec();
    const currentCount = results?.[2]?.[1] as number || 0;

    return {
      allowed: currentCount < maxRequests,
      limit: maxRequests,
      remaining: Math.max(0, maxRequests - currentCount),
      resetTime: new Date(now + windowMs),
    };
  }

  async reset(identifier: string): Promise<void> {
    const key = `${this.config.keyPrefix}:${identifier}`;
    await redis.del(key);
  }
}

/**
 * 速率限制中间件
 */
export function rateLimitMiddleware(config: RateLimitConfig) {
  const limiter = new RateLimiter(config);

  return async (
    req: NextRequest,
    res?: NextResponse
  ): Promise<NextResponse | null> => {
    const identifier = getRateLimitIdentifier(req);
    const result = await limiter.check(identifier);

    // 添加速率限制头
    const headers = new Headers();
    headers.set('X-RateLimit-Limit', result.limit.toString());
    headers.set('X-RateLimit-Remaining', result.remaining.toString());
    headers.set('X-RateLimit-Reset', result.resetTime.toISOString());

    if (!result.allowed) {
      return NextResponse.json(
        {
          error: 'rate_limit_exceeded',
          error_description: '请求过于频繁,请稍后再试',
          retry_after: Math.ceil((result.resetTime.getTime() - Date.now()) / 1000),
        },
        {
          status: 429,
          headers,
        }
      );
    }

    return null;
  };
}

function getRateLimitIdentifier(req: NextRequest): string {
  // 优先使用用户 ID
  const authContext = (req as any).authContext;
  if (authContext?.userId) {
    return `user:${authContext.userId}`;
  }

  // 其次使用客户端证书序列号
  if (authContext?.clientCert?.serialNumber) {
    return `cert:${authContext.clientCert.serialNumber}`;
  }

  // 最后使用 IP 地址
  const ip = req.headers.get('X-Forwarded-For') ||
             req.headers.get('X-Real-IP') ||
             'unknown';
  return `ip:${ip}`;
}
Code collapsed

安全审计日志

code
// lib/audit/audit-logger.ts
import { pool } from '@/lib/db';

export enum AuditEventType {
  AUTHENTICATION = 'authentication',
  AUTHORIZATION = 'authorization',
  DATA_ACCESS = 'data_access',
  DATA_MODIFICATION = 'data_modification',
  CONFIGURATION_CHANGE = 'configuration_change',
  SECURITY_EVENT = 'security_event',
}

export interface AuditLog {
  eventType: AuditEventType;
  userId?: string;
  tenantId?: string;
  resource: string;
  action: string;
  ipAddress: string;
  userAgent: string;
  success: boolean;
  errorCode?: string;
  metadata?: Record<string, any>;
  timestamp: Date;
}

export class AuditLogger {
  /**
   * 记录审计事件
   */
  static async log(event: AuditLog): Promise<void> {
    await pool.query(
      `INSERT INTO audit_logs
       (event_type, user_id, tenant_id, resource, action,
        ip_address, user_agent, success, error_code, metadata, timestamp)
       VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)`,
      [
        event.eventType,
        event.userId,
        event.tenantId,
        event.resource,
        event.action,
        event.ipAddress,
        event.userAgent,
        event.success,
        event.errorCode,
        event.metadata ? JSON.stringify(event.metadata) : null,
        event.timestamp,
      ]
    );

    // 对于高风险事件,发送实时告警
    if (this.isHighRiskEvent(event)) {
      await this.sendAlert(event);
    }
  }

  /**
   * 查询审计日志
   */
  static async query(filters: {
    userId?: string;
    tenantId?: string;
    eventType?: AuditEventType;
    startDate?: Date;
    endDate?: Date;
    limit?: number;
  }): Promise<AuditLog[]> {
    const conditions: string[] = [];
    const params: any[] = [];
    let paramCount = 1;

    if (filters.userId) {
      conditions.push(`user_id = $${paramCount}`);
      params.push(filters.userId);
      paramCount++;
    }

    if (filters.tenantId) {
      conditions.push(`tenant_id = $${paramCount}`);
      params.push(filters.tenantId);
      paramCount++;
    }

    if (filters.eventType) {
      conditions.push(`event_type = $${paramCount}`);
      params.push(filters.eventType);
      paramCount++;
    }

    if (filters.startDate) {
      conditions.push(`timestamp >= $${paramCount}`);
      params.push(filters.startDate);
      paramCount++;
    }

    if (filters.endDate) {
      conditions.push(`timestamp <= $${paramCount}`);
      params.push(filters.endDate);
      paramCount++;
    }

    const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
    const limitClause = filters.limit ? `LIMIT ${filters.limit}` : '';

    const result = await pool.query(
      `SELECT * FROM audit_logs
       ${whereClause}
       ORDER BY timestamp DESC
       ${limitClause}`,
      params
    );

    return result.rows;
  }

  private static isHighRiskEvent(event: AuditLog): boolean {
    return (
      !event.success ||
      event.eventType === AuditEventType.SECURITY_EVENT ||
      event.action === 'delete' ||
      event.action === 'export_all_data'
    );
  }

  private static async sendAlert(event: AuditLog): Promise<void> {
    // 发送到告警系统(Slack、PagerDuty 等)
    console.error('高风险安全事件:', event);
  }
}
Code collapsed

合规检查清单

HIPAA 合规

  • 所有 API 使用 TLS 1.3 加密
  • 实施 mTLS 用于服务间通信
  • 访问日志记录至少 6 年
  • 最小权限原则
  • 定期安全评估
  • 业务连续性计划

GDPR 合规

  • 数据加密(静态和传输)
  • 访问控制机制
  • 数据主体权利实现
  • 数据处理记录
  • 数据泄露通知流程
  • 数据保护影响评估

OWASP 安全

  • 注入攻击防护
  • 失效的身份认证防护
  • 敏感数据暴露防护
  • XXE 攻击防护
  • 访问控制缺失防护
  • 安全配置错误防护

参考资料


免责声明:本文提供的安全实施指南仅供参考。在生产环境中部署前,请务必进行专业的安全审计和渗透测试。

#

文章标签

oauth
mtls
api-security
healthtech
authentication
tls

觉得这篇文章有帮助?

立即体验康心伴,开始您的健康管理之旅