美团CPS开放平台对接中Java服务的API权限控制设计技巧

美团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通知其他节点
    }
}

本文著作权归 俱美开放平台 ,转载请注明出处!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值