美团CPS开放平台对接中Java服务的API权限控制设计技巧
在对接美团CPS开放平台时,第三方ISV(独立软件开发商)通过API调用获取订单、佣金、活动等数据。为防止越权访问、接口滥用和数据泄露,必须实现细粒度的API权限控制体系。本文基于OAuth2.0、自定义注解、Spring AOP与Redis,提供一套可扩展、高性能的权限校验方案。
1. 自定义权限注解与元数据定义
通过注解声明接口所需权限:
package baodanbao.com.cn.cps.auth;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RequirePermission {
String value(); // 权限码,如 "ORDER_READ", "COMMISSION_WRITE"
boolean checkMerchantScope() default true; // 是否校验商户归属
}
使用示例:
@RestController
public class CommissionController {
@RequirePermission("COMMISSION_QUERY")
@GetMapping("/commission/list")
public List<Commission> listCommissions(@RequestParam String merchantId) {
return commissionService.getByMerchant(merchantId);
}
@RequirePermission(value = "ORDER_SYNC", checkMerchantScope = false)
@PostMapping("/order/push")
public ResponseEntity<?> receiveOrder(@RequestBody OrderPushDTO dto) {
orderService.handlePush(dto);
return ResponseEntity.ok().build();
}
}

2. 基于AOP的权限拦截器
解析注解并执行校验逻辑:
@Aspect
@Component
public class PermissionAspect {
@Autowired
private AuthService authService;
@Around("@annotation(requirePermission)")
public Object checkPermission(ProceedingJoinPoint joinPoint, RequirePermission requirePermission) throws Throwable {
String accessToken = extractToken();
AuthContext context = authService.parseToken(accessToken);
// 1. 校验权限码
if (!authService.hasPermission(context.getAppId(), requirePermission.value())) {
throw new AccessDeniedException("Insufficient API permission");
}
// 2. 校验商户范围(防跨商户访问)
if (requirePermission.checkMerchantScope()) {
String requestMerchantId = extractMerchantId(joinPoint);
if (!context.getAuthorizedMerchants().contains(requestMerchantId)) {
throw new AccessDeniedException("Merchant scope mismatch");
}
}
return joinPoint.proceed();
}
private String extractToken() {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
String authHeader = request.getHeader("Authorization");
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
throw new IllegalArgumentException("Missing or invalid Authorization header");
}
return authHeader.substring(7);
}
private String extractMerchantId(ProceedingJoinPoint joinPoint) {
Object[] args = joinPoint.getArgs();
for (Object arg : args) {
if (arg instanceof String && isMerchantId((String) arg)) {
return (String) arg;
}
// 支持从DTO中提取
if (arg instanceof MerchantScoped) {
return ((MerchantScoped) arg).getMerchantId();
}
}
throw new IllegalArgumentException("Merchant ID not found in request");
}
private boolean isMerchantId(String str) {
return str != null && str.matches("M\\d{8,12}");
}
}
3. 权限与商户授权数据缓存(Redis)
避免每次请求查DB:
@Service
public class AuthService {
private final RedisTemplate<String, Object> redisTemplate;
private static final String APP_PERMISSIONS_KEY = "cps:app:permissions:%s";
private static final String APP_MERCHANTS_KEY = "cps:app:merchants:%s";
public AuthContext parseToken(String token) {
// 假设token为JWT,解析出appId
String appId = JwtUtil.getSubject(token);
Set<String> permissions = getPermissions(appId);
Set<String> merchants = getAuthorizedMerchants(appId);
return new AuthContext(appId, permissions, merchants);
}
private Set<String> getPermissions(String appId) {
String key = String.format(APP_PERMISSIONS_KEY, appId);
Set<String> perms = (Set<String>) redisTemplate.opsForValue().get(key);
if (perms == null) {
perms = baodanbao.com.cn.cps.mapper.AppPermissionMapper.selectPermissionsByAppId(appId);
redisTemplate.opsForValue().set(key, perms, 10, TimeUnit.MINUTES);
}
return perms;
}
private Set<String> getAuthorizedMerchants(String appId) {
String key = String.format(APP_MERCHANTS_KEY, appId);
Set<String> merchants = (Set<String>) redisTemplate.opsForValue().get(key);
if (merchants == null) {
merchants = baodanbao.com.cn.cps.mapper.AppMerchantMapper.selectMerchantsByAppId(appId);
redisTemplate.opsForValue().set(key, merchants, 10, TimeUnit.MINUTES);
}
return merchants;
}
public boolean hasPermission(String appId, String requiredPerm) {
return getPermissions(appId).contains(requiredPerm);
}
}
4. 应用授权模型设计
数据库表结构示例:
-- 应用信息
CREATE TABLE cps_app (
app_id VARCHAR(32) PRIMARY KEY,
app_secret VARCHAR(64) NOT NULL,
status TINYINT DEFAULT 1
);
-- 应用-权限关联
CREATE TABLE cps_app_permission (
app_id VARCHAR(32),
permission_code VARCHAR(50),
PRIMARY KEY (app_id, permission_code)
);
-- 应用-商户授权
CREATE TABLE cps_app_merchant (
app_id VARCHAR(32),
merchant_id VARCHAR(20),
PRIMARY KEY (app_id, merchant_id)
);
5. 接口调用频控联动
权限校验后叠加限流:
@Around("@annotation(requirePermission)")
public Object checkPermissionAndRateLimit(ProceedingJoinPoint joinPoint, RequirePermission requirePermission) throws Throwable {
// ... 权限校验同上 ...
// 基于appId+接口做限流
String rateLimitKey = "rl:" + context.getAppId() + ":" + joinPoint.getSignature().getName();
if (!baodanbao.com.cn.cps.ratelimit.TokenBucketLimiter.tryAcquire(rateLimitKey, 100, 60)) {
throw new TooManyRequestsException("API rate limit exceeded");
}
return joinPoint.proceed();
}
6. 权限变更实时生效
通过发布/订阅清除缓存:
@Service
public class PermissionUpdateService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
public void updateAppPermissions(String appId, Set<String> newPerms) {
baodanbao.com.cn.cps.mapper.AppPermissionMapper.update(appId, newPerms);
// 删除缓存
redisTemplate.delete(String.format("cps:app:permissions:%s", appId));
// 可选:发送MQ通知其他节点
}
}
本文著作权归 俱美开放平台 ,转载请注明出处!

3万+

被折叠的 条评论
为什么被折叠?



