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 攻击防护
- 访问控制缺失防护
- 安全配置错误防护
参考资料
免责声明:本文提供的安全实施指南仅供参考。在生产环境中部署前,请务必进行专业的安全审计和渗透测试。