进阶功能

本文介绍VodAppServer的高级特性与使用场景。

JWT播放鉴权

重要

使用JWT播放鉴权需集成播放器V7.10.0及以上版本。

工作原理

VodAppServer使用JWT(JSON Web Token)实现播放鉴权:服务端通过PlayKey本地签名生成playAuth,经视频点播校验后返回视频流。

vidauth-local-sign

生成播放凭证(服务端本地签名)

生成示例:

// 核心代码
String playAuth = JwtUtil.getPlayAuthToken(videoId, playKey);
说明

playKey可参考playKey是什么获取。

自定义Token有效期

// 默认有效期:3600秒(1小时)
String playAuth = JwtUtil.getPlayAuthToken(videoId, playKey);

// 自定义有效期:7200秒(2小时)
String playAuth = JwtUtil.getPlayAuthToken(videoId, playKey, 7200);

配置播放密钥

获取播放密钥

播放密钥获取请参考playKey是什么

设置播放密钥

// 调用接口设置
SetAppPlayKeyResponse response = vodSdkService.SetAppPlayKey(
    JwtConstants.DEFAULT_APP_ID,
    "your_new_play_key"
);

安全与性能

AccessKey安全

推荐使用以下三种方式配置AccessKey,任选其一即可。

  • 使用环境变量。

    # 设置环境变量
    export ALIYUN_AK=your_access_key
    export ALIYUN_SK=your_secret_key
    # 配置文件引用
    aliyun:
      vod:
        ak: ${ALIYUN_AK}
        sk: ${ALIYUN_SK}
        region: ${ALIYUN_VOD_REGION:cn-shanghai}  # 地域标识,支持环境变量
  • 使用配置中心。

    @Configuration
    public class VodConfigLoader {
        
        @Value("${config.center.url}")
        private String configCenterUrl;
        
        @Bean
        public VodConfig vodConfig() {
            // 从配置中心加载
            return configClient.load("vod-config");
        }
    }
  • 使用阿里云RAM角色

    // 使用 ECS 实例角色
    // 注意:region 需要从配置中读取,不能硬编码
    String regionId = vodConfig.getRegion(); // 从配置读取
    DefaultProfile profile = DefaultProfile.getProfile(
        regionId
        // 不需要 AK/SK,自动使用实例角色
    );

接口限流

@Component
public class RateLimiter {
    
    private final Semaphore semaphore = new Semaphore(100);  // 并发限制
    
    public <T> T execute(Supplier<T> action) throws InterruptedException {
        semaphore.acquire();
        try {
            return action.get();
        } finally {
            semaphore.release();
        }
    }
}

缓存策略

@Service
public class CachedVodService {
    
    @Cacheable(value = "playlistCache", key = "#playlistId", unless = "#result == null")
    public PlayList getPlaylist(String playlistId) {
        return vodSdkService.getPlaylist(playlistId);
    }
    
    @CacheEvict(value = "playlistCache", key = "#playlistId")
    public void updatePlaylist(String playlistId, PlayList playlist) {
        vodSdkService.updatePlaylist(playlistId, playlist);
    }
}

异步处理

@Service
public class AsyncVideoProcessor {
    
    @Async("vodExecutor")
    public CompletableFuture<String> generatePlayAuth(String videoId) {
        String playAuth = JwtUtil.getPlayAuthToken(videoId, playKey);
        return CompletableFuture.completedFuture(playAuth);
    }
    
    public List<String> batchGeneratePlayAuth(List<String> videoIds) {
        List<CompletableFuture<String>> futures = videoIds.stream()
            .map(this::generatePlayAuth)
            .collect(Collectors.toList());
        
        return futures.stream()
            .map(CompletableFuture::join)
            .collect(Collectors.toList());
    }
}

扩展开发

自定义拦截器

@Component
public class VodAuthInterceptor implements HandlerInterceptor {
    
    @Override
    public boolean preHandle(HttpServletRequest request, 
                           HttpServletResponse response, 
                           Object handler) throws Exception {
        // 验证请求签名
        String signature = request.getHeader("X-Signature");
        if (!validateSignature(signature)) {
            response.setStatus(HttpStatus.UNAUTHORIZED.value());
            return false;
        }
        return true;
    }
    
    private boolean validateSignature(String signature) {
        // 实现签名验证逻辑
        return true;
    }
}

自定义异常处理

@ControllerAdvice
public class GlobalExceptionHandler {
    
    @ExceptionHandler(ClientException.class)
    public ResponseEntity<CallResult> handleClientException(ClientException e) {
        CallResult result = new CallResult();
        // 这里示例直接复用 SYSTEM_INNER_ERROR,可根据需要在 ResultCode 中扩展专门的 API 错误码
        result.setCode(ResultCode.SYSTEM_INNER_ERROR.code);
        result.setHttpCode("500");
        result.setSuccess(false);
        result.setMessage("阿里云 API 调用失败: " + e.getMessage());
        
        return ResponseEntity.status(500).body(result);
    }
    
    @ExceptionHandler(Exception.class)
    public ResponseEntity<CallResult> handleException(Exception e) {
        CallResult result = new CallResult();
        result.setCode(ResultCode.SYSTEM_INNER_ERROR.code);
        result.setHttpCode("500");
        result.setSuccess(false);
        result.setMessage("系统错误: " + e.getMessage());
        
        return ResponseEntity.status(500).body(result);
    }
}

性能监控

接口耗时统计

@Aspect
@Component
public class PerformanceMonitor {
    
    @Around("execution(* com.aliyun.appserver.controller..*(..))")
    public Object monitorPerformance(ProceedingJoinPoint pjp) throws Throwable {
        long startTime = System.currentTimeMillis();
        
        Object result = pjp.proceed();
        
        long duration = System.currentTimeMillis() - startTime;
        String methodName = pjp.getSignature().getName();
        
        if (duration > 1000) {
            log.warn("接口 {} 耗时过长: {}ms", methodName, duration);
        }
        
        return result;
    }
}

日志记录

@Slf4j
@Component
public class ApiLogger {
    
    public void logRequest(HttpServletRequest request) {
        log.info("API 请求 - 方法: {}, URI: {}, 参数: {}", 
            request.getMethod(),
            request.getRequestURI(),
            request.getParameterMap()
        );
    }
    
    public void logResponse(CallResult result, long duration) {
        log.info("API 响应 - 状态: {}, 耗时: {}ms, 消息: {}", 
            result.getSuccess(),
            duration,
            result.getMessage()
        );
    }
}