跳到主要内容

授权规则(黑白名单控制)

很多时候,我们需要根据调用方来限制资源是否通过,这时候可以使用 Sentinel 的访问控制(黑白名单)功能。黑白名单根据资源的请求来源(origin)限制资源是否通过,实现精细化的访问控制。

什么是授权规则

授权规则允许我们根据请求的来源(origin)来决定是否允许访问某个资源:

  • 白名单模式:只有请求来源位于白名单内时才可通过
  • 黑名单模式:请求来源位于黑名单时不通过,其余的请求通过

这在微服务架构中非常有用,比如:

  • 只允许特定的服务调用某个敏感接口
  • 禁止某些不稳定或有问题的服务调用
  • 实现服务间的访问隔离

调用来源识别

调用来源(origin)是授权规则判断的依据,需要由开发者自行设置。Sentinel 通过 ContextUtil.enter(contextName, origin) 方法中的 origin 参数来识别调用来源。

基本用法

// 设置调用来源为 "appA"
ContextUtil.enter("getUser", "appA");

try {
Entry entry = SphU.entry("getUser");
// 业务逻辑
entry.exit();
} catch (BlockException e) {
// 被限流或拒绝访问
} finally {
ContextUtil.exit();
}

在 Spring Cloud 中实现来源识别

在 Spring Cloud 应用中,可以通过实现 RequestOriginParser 接口来解析请求来源:

import com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.RequestOriginParser;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;

@Component
public class CustomRequestOriginParser implements RequestOriginParser {

@Override
public String parseOrigin(HttpServletRequest request) {
// 从请求头获取来源
String origin = request.getHeader("X-Request-Source");

// 如果没有指定来源,使用 IP 地址
if (origin == null || origin.isEmpty()) {
origin = request.getRemoteAddr();
}

return origin;
}
}

在网关中实现来源识别

在 Spring Cloud Gateway 中,可以通过配置来实现:

import com.alibaba.csp.sentinel.adapter.gateway.sc.callback.GatewayCallbackManager;
import org.springframework.web.server.ServerWebExchange;

// 自定义来源解析
GatewayCallbackManager.setGwOriginParser((exchange) -> {
// 从请求头获取来源
String source = exchange.getRequest().getHeaders().getFirst("X-Source-App");
if (source != null) {
return source;
}

// 从请求参数获取
source = exchange.getRequest().getQueryParams().getFirst("app");
if (source != null) {
return source;
}

// 默认使用 IP
return exchange.getRequest().getRemoteAddress().getAddress().getHostAddress();
});

规则配置

授权规则(AuthorityRule)的配置相对简单,主要包含以下属性:

属性说明默认值
resource资源名,规则的作用对象必填
strategy限制模式,AUTHORITY_WHITE(白名单)或 AUTHORITY_BLACK(黑名单)AUTHORITY_WHITE
limitApp对应的黑名单/白名单,不同 origin 用逗号分隔-

白名单配置

白名单模式下,只有列表中的来源可以访问:

import com.alibaba.csp.sentinel.slots.block.RuleConstant;
import com.alibaba.csp.sentinel.slots.block.authority.AuthorityRule;
import com.alibaba.csp.sentinel.slots.block.authority.AuthorityRuleManager;

// 配置白名单规则:只允许 appA 和 appB 访问
AuthorityRule rule = new AuthorityRule();
rule.setResource("sensitive-api"); // 资源名
rule.setStrategy(RuleConstant.AUTHORITY_WHITE); // 白名单模式
rule.setLimitApp("appA,appB"); // 允许的来源

AuthorityRuleManager.loadRules(Collections.singletonList(rule));

当来源为 "appA" 或 "appB" 时,请求可以通过;其他来源的请求会被拒绝,抛出 AuthorityException

黑名单配置

黑名单模式下,列表中的来源被禁止访问:

// 配置黑名单规则:禁止 appC 和 appD 访问
AuthorityRule rule = new AuthorityRule();
rule.setResource("sensitive-api");
rule.setStrategy(RuleConstant.AUTHORITY_BLACK); // 黑名单模式
rule.setLimitApp("appC,appD"); // 禁止的来源

AuthorityRuleManager.loadRules(Collections.singletonList(rule));

当来源为 "appC" 或 "appD" 时,请求会被拒绝;其他来源的请求可以通过。

完整示例

定义资源并配置规则

import com.alibaba.csp.sentinel.Entry;
import com.alibaba.csp.sentinel.SphU;
import com.alibaba.csp.sentinel.context.ContextUtil;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.alibaba.csp.sentinel.slots.block.RuleConstant;
import com.alibaba.csp.sentinel.slots.block.authority.AuthorityRule;
import com.alibaba.csp.sentinel.slots.block.authority.AuthorityRuleManager;

import java.util.Collections;

public class AuthorityDemo {

public static void main(String[] args) {
// 初始化授权规则
initAuthorityRules();

// 模拟不同来源的请求
simulateRequest("appA"); // 白名单,应该通过
simulateRequest("appB"); // 白名单,应该通过
simulateRequest("appC"); // 不在白名单,应该被拒绝
}

private static void simulateRequest(String origin) {
ContextUtil.enter("test-api", origin);

Entry entry = null;
try {
entry = SphU.entry("test-api");
System.out.println("来源 " + origin + " 访问成功");
} catch (BlockException e) {
System.out.println("来源 " + origin + " 被拒绝访问: " + e.getClass().getSimpleName());
} finally {
if (entry != null) {
entry.exit();
}
ContextUtil.exit();
}
}

private static void initAuthorityRules() {
AuthorityRule rule = new AuthorityRule();
rule.setResource("test-api");
rule.setStrategy(RuleConstant.AUTHORITY_WHITE);
rule.setLimitApp("appA,appB");

AuthorityRuleManager.loadRules(Collections.singletonList(rule));
}
}

输出结果:

来源 appA 访问成功
来源 appB 访问成功
来源 appC 被拒绝访问: AuthorityException

Spring Boot Web 应用示例

import com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.RequestOriginParser;
import com.alibaba.csp.sentinel.slots.block.RuleConstant;
import com.alibaba.csp.sentinel.slots.block.authority.AuthorityRule;
import com.alibaba.csp.sentinel.slots.block.authority.AuthorityRuleManager;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.PostConstruct;
import javax.servlet.http.HttpServletRequest;
import java.util.Collections;

@SpringBootApplication
@RestController
public class Application {

public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}

@Bean
public RequestOriginParser requestOriginParser() {
return (HttpServletRequest request) -> {
// 从请求头获取来源应用名
String source = request.getHeader("X-App-Name");
return source != null ? source : "unknown";
};
}

@PostConstruct
public void initRules() {
// 配置授权规则
AuthorityRule rule = new AuthorityRule();
rule.setResource("GET:/api/admin");
rule.setStrategy(RuleConstant.AUTHORITY_WHITE);
rule.setLimitApp("admin-service,monitor-service");

AuthorityRuleManager.loadRules(Collections.singletonList(rule));
}

@GetMapping("/api/admin")
public String admin() {
return "admin access";
}
}

多规则组合

可以为同一个资源配置多个授权规则,规则之间是"或"的关系,只要满足任一规则即可通过。

// 规则一:白名单允许 appA 和 appB
AuthorityRule rule1 = new AuthorityRule();
rule1.setResource("api");
rule1.setStrategy(RuleConstant.AUTHORITY_WHITE);
rule1.setLimitApp("appA,appB");

// 规则二:黑名单禁止 appC
AuthorityRule rule2 = new AuthorityRule();
rule2.setResource("api");
rule2.setStrategy(RuleConstant.AUTHORITY_BLACK);
rule2.setLimitApp("appC");

// 两个规则同时加载
AuthorityRuleManager.loadRules(Arrays.asList(rule1, rule2));

在这个例子中:

  • appA 和 appB 在白名单中,可以通过
  • appC 在黑名单中,会被拒绝
  • appD 既不在白名单也不在黑名单,由于白名单规则存在,会被拒绝

与其他规则的关系

授权规则在 Slot Chain 中的位置在 FlowSlot 之前,优先级高于流量控制规则:

请求 -> AuthoritySlot -> FlowSlot -> DegradeSlot -> 业务逻辑

这意味着:

  • 如果请求被授权规则拒绝,不会进入流量控制判断
  • 授权规则的优先级最高

异常处理

当请求被授权规则拒绝时,会抛出 AuthorityException。可以通过以下方式处理:

统一异常处理

import com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.BlockExceptionHandler;
import com.alibaba.csp.sentinel.slots.block.authority.AuthorityException;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@Configuration
public class SentinelConfig {

@Bean
public BlockExceptionHandler blockExceptionHandler() {
return (HttpServletRequest request, HttpServletResponse response, BlockException e) -> {
if (e instanceof AuthorityException) {
response.setStatus(403);
response.getWriter().write("{\"code\":403,\"message\":\"访问被拒绝\"}");
}
};
}
}

注解方式处理

import com.alibaba.csp.sentinel.annotation.SentinelResource;
import com.alibaba.csp.sentinel.slots.block.BlockException;

@Service
public class AdminService {

@SentinelResource(value = "adminOperation", blockHandler = "handleBlock")
public String adminOperation() {
return "admin operation success";
}

public String handleBlock(BlockException e) {
if (e instanceof AuthorityException) {
return "无权限访问";
}
return "系统繁忙";
}
}

动态规则配置

授权规则同样支持动态配置源:

Nacos 数据源

spring:
cloud:
sentinel:
datasource:
authority:
nacos:
server-addr: localhost:8848
dataId: ${spring.application.name}-authority-rules
groupId: SENTINEL_GROUP
rule-type: authority

规则 JSON 格式:

[
{
"resource": "sensitive-api",
"strategy": 0,
"limitApp": "appA,appB"
}
]

其中 strategy 的值:

  • 0:白名单
  • 1:黑名单

使用场景

1. 服务间访问控制

只允许特定的服务调用敏感接口:

// 只允许支付服务和订单服务调用
AuthorityRule rule = new AuthorityRule();
rule.setResource("api/payment");
rule.setStrategy(RuleConstant.AUTHORITY_WHITE);
rule.setLimitApp("payment-service,order-service");

2. 多租户隔离

实现不同租户之间的访问隔离:

// 根据租户 ID 进行隔离
@Bean
public RequestOriginParser requestOriginParser() {
return (request) -> {
String tenantId = request.getHeader("X-Tenant-Id");
return tenantId != null ? "tenant-" + tenantId : "default";
};
}

3. 内外部访问区分

区分内部服务和外部请求:

// 内部接口只允许内部服务访问
AuthorityRule rule = new AuthorityRule();
rule.setResource("api/internal");
rule.setStrategy(RuleConstant.AUTHORITY_WHITE);
rule.setLimitApp("internal-gateway,admin-console");

4. 禁止问题服务

临时禁止某个有问题的服务:

// 禁止问题服务访问
AuthorityRule rule = new AuthorityRule();
rule.setResource("api/*");
rule.setStrategy(RuleConstant.AUTHORITY_BLACK);
rule.setLimitApp("problem-service");

注意事项

1. 来源标识的唯一性

确保 origin 的唯一性和稳定性。如果使用 IP 地址作为来源,需要考虑代理和负载均衡的情况。

2. 性能考虑

授权规则的匹配是线性遍历,规则数量较多时会影响性能。建议:

  • 单个资源的授权规则数量控制在合理范围
  • 避免过多的来源标识

3. 与其他规则的配合

授权规则优先级最高,在配置时需要考虑与其他规则的配合关系。

4. 规则持久化

生产环境建议使用动态规则源,避免应用重启后规则丢失。

最佳实践

1. 合理设计来源标识

来源标识应该具有业务含义,便于管理和理解:

// 推荐:使用服务名作为来源
return request.getHeader("X-Service-Name");

// 不推荐:使用 IP 地址(不稳定)
return request.getRemoteAddr();

2. 优先使用白名单

白名单模式更加安全,只允许已知的服务访问:

// 推荐做法
rule.setStrategy(RuleConstant.AUTHORITY_WHITE);
rule.setLimitApp("trusted-service-a,trusted-service-b");

3. 结合日志和监控

记录被拒绝的访问请求,便于排查问题:

@Bean
public BlockExceptionHandler blockExceptionHandler() {
return (request, response, e) -> {
if (e instanceof AuthorityException) {
String origin = request.getHeader("X-App-Name");
log.warn("访问被拒绝, origin={}, resource={}",
origin, ((AuthorityException) e).getRule().getResource());
// ...
}
};
}

4. 定期审计规则

定期检查授权规则的合理性,清理不再需要的规则。

下一步