需求场景见#415
由于网关网关与子服务之间没有直接关系, 因此必须通过redis等中间件进行交互.
仅实现了根据请求路径(path)和请求方式(method)进行鉴权, 如果映射(见org.springframework.web.bind.annotation.RequestMapping)中存在params / headers / consumes / produces等匹配方式则行不通
子服务启动时
通过bean requestMappingHandlerMapping
遍历接口
查所有含SaIgnore注解的接口(方法或类上拥有), 作为includeList
如includeList中的接口含有通配符, 则将剩余接口放到excludeList
原因: 通配符会导致优先级处理问题, 如以下几对路径会造成匹配错误(实际访问路径为/abc/abc
时均为接口B优先, 但是如果接口A上有SaIgnore则会被放行), 而这部分场景不常见, 且数据量不大, 如果对excludeList做过滤意义也不大.
接口A | 接口B |
---|---|
/*/abc | /abc/abc |
/** | /abc/abc |
/a?c/abc | /abc/abc |
/{path}/abc | /abc/abc |
生成uuid, 作为version存储, 用于网关判断是否需要更新
includeList和excludeList大致结构如下:
[{
methods: ['GET', 'POST'],
patterns: ['/abc/def', '/abc/ghi'],
}, {
methods: ['GET'],
patterns: ['/abc/jkl']
}]
将includeList, excludeList, version存储到缓存中, 并以appId(spring.application.name)作为前缀区分
网关收到请求时
如果存在动态路由, 如Controller中有RefreshScope注解,
SaTokenSaIgnoreCollectRunner
中的代码需要在刷新配置后再次调用.
SaIgnoreCollector.java
package kim.nzxy.ly.common.sa.ignore;
import cn.dev33.satoken.router.SaRouter;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.web.bind.annotation.RequestMethod;
import java.util.List;
import java.util.Objects;
import java.util.Set;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class SaIgnoreCollector {
/**
* 请求方式
*/
private Set<RequestMethod> methods;
/**
* 路由规则
*/
private List<String> patterns;
}
SaIgnoreGatewayUtil.java
package kim.nzxy.ly.common.sa.ignore;
/**
* SaIgnore 网关适配管理
* @author ly-chn
*/
public class SaIgnoreGatewayUtil {
private static final String KEY_PREFIX = "sa-token:ignore:";
public static String includeKey(String appId) {
return KEY_PREFIX + "includes:" + appId;
}
public static String excludeKey(String appId) {
return KEY_PREFIX +"excludes:"+appId;
}
public static String versionKey(String appId) {
return KEY_PREFIX + "version:" + appId;
}
}
SaTokenSaIgnoreCollectRunner.java
package kim.nzxy.ly.common.runner;
import cn.dev33.satoken.SaManager;
import cn.dev33.satoken.annotation.SaIgnore;
import kim.nzxy.ly.common.util.SpringContextUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.CommandLineRunner;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMapping;
import org.springframework.web.util.pattern.PathPattern;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.stream.Collectors;
/**
* @author ly-chn
*/
@Component
@RequiredArgsConstructor
@Slf4j
public class SaTokenSaIgnoreCollectRunner implements CommandLineRunner {
@Override
public void run(String... args) {
RequestMappingInfoHandlerMapping mapping = SpringContextUtil.getBean("requestMappingHandlerMapping", RequestMappingInfoHandlerMapping.class);
Map<RequestMappingInfo, HandlerMethod> handlerMethods = mapping.getHandlerMethods();
List<SaIgnoreCollector> includeList = new ArrayList<>();
handlerMethods.forEach((info, method) -> {
if (hasIgnored(method)) {
// noinspection DataFlowIssue
includeList.add(new SaIgnoreCollector(info.getMethodsCondition().getMethods(),
info.getPathPatternsCondition().getPatterns().stream()
.map(PathPattern::getPatternString).collect(Collectors.toList())));
}
});
String appId = SpringContextUtil.getId();
List<SaIgnoreCollector> excludeList = new ArrayList<>();
if (includeList.stream().anyMatch(this::hasAntStylePath)) {
handlerMethods.forEach((info, method) -> {
if (!method.hasMethodAnnotation(SaIgnore.class)) {
// noinspection DataFlowIssue
excludeList.add(new SaIgnoreCollector(info.getMethodsCondition().getMethods(),
info.getPathPatternsCondition().getPatterns().stream()
.map(PathPattern::getPatternString).collect(Collectors.toList())));
}
});
}
SaManager.getSaTokenDao().setObject(SaIgnoreGatewayUtil.excludeKey(appId), excludeList, -1);
SaManager.getSaTokenDao().setObject(SaIgnoreGatewayUtil.includeKey(appId), includeList, -1);
SaManager.getSaTokenDao().setObject(SaIgnoreGatewayUtil.versionKey(appId), UUID.randomUUID().toString(), -1);
}
private boolean hasAntStylePath(SaIgnoreCollector collector) {
String s = collector.getPatterns().toString();
return s.contains("?") || s.contains("*");
}
private boolean hasIgnored(HandlerMethod method) {
return method.hasMethodAnnotation(SaIgnore.class)|| AnnotatedElementUtils.isAnnotated(method.getBeanType(), SaIgnore.class);
}
}
SpringContextUtil.java
package kim.nzxy.ly.common.util;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
/**
* @author ly-chn
*/
@Component
public class SpringContextUtil implements ApplicationContextAware {
private static ApplicationContext context;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
context = applicationContext;
}
public static <T> T getBean(Class<T> requiredType) {
return context.getBean(requiredType);
}
public static <T> T getBean(String name, Class<T> requiredType) {
return context.getBean(name, requiredType);
}
public static String getId() {
return context.getId();
}
}
我们的逻辑是根据服务id+请求方式+请求路径来判断, 那么sa-token文档中的SaReactorFilter就不能用了, 需要自己写一个了
SaIgnoreCollectorCache.java
package kim.nzxy.ly.common.sa.ignore;
import cn.dev33.satoken.SaManager;
import cn.dev33.satoken.dao.SaTokenDao;
import cn.dev33.satoken.util.SaFoxUtil;
import kim.nzxy.ly.common.runner.SaIgnoreCollector;
import kim.nzxy.ly.common.runner.SaIgnoreGatewayUtil;
import java.util.*;
public class SaIgnoreCollectorCache {
private static final Map<String, List<SaIgnoreCollector>> INCLUDE_CACHE = new HashMap<>(16);
private static final Map<String, List<SaIgnoreCollector>> EXCLUDE_CACHE = new HashMap<>(16);
private static final Map<String, String> VERSION_CACHE = new HashMap<>(16);
private static final List<SaIgnoreCollector> EMPTY = Collections.emptyList();
public static List<SaIgnoreCollector> getIncludeList(String appId) {
refresh(appId);
return Optional.ofNullable(INCLUDE_CACHE.get(appId)).orElse(EMPTY);
}
public static List<SaIgnoreCollector> getExcludeList(String appId) {
refresh(appId);
return Optional.ofNullable(INCLUDE_CACHE.get(appId)).orElse(EMPTY);
}
@SuppressWarnings("unchecked")
private static void refresh(String appId) {
SaTokenDao dao = SaManager.getSaTokenDao();
String version = dao.get(SaIgnoreGatewayUtil.versionKey(appId));
// 取不到版本, 说明没更新
if (SaFoxUtil.isEmpty(version)) {
INCLUDE_CACHE.remove(appId);
EXCLUDE_CACHE.remove(appId);
return;
}
if (!Objects.equals(VERSION_CACHE.get(appId), version)) {
VERSION_CACHE.put(appId, version);
INCLUDE_CACHE.put(appId, (List<SaIgnoreCollector>) dao.getObject(SaIgnoreGatewayUtil.includeKey(appId)));
EXCLUDE_CACHE.put(appId, (List<SaIgnoreCollector>) dao.getObject(SaIgnoreGatewayUtil.excludeKey(appId)));
}
}
}
SaIgnoreFilter.java
package kim.nzxy.gateway.filter;
import cn.dev33.satoken.stp.StpUtil;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.cloud.gateway.route.Route;
import org.springframework.core.Ordered;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import kim.nzxy.ly.common.runner.SaIgnoreCollector;
import java.net.URI;
import java.util.List;
import java.util.Optional;
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR;
/**
* @author Liaoliao
*/
@Component
public class SaIgnoreFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 登录了就放行
if (StpUtil.isLogin()) {
return chain.filter(exchange);
}
Route route = exchange.getAttribute(GATEWAY_ROUTE_ATTR);
Optional<String> appId = Optional.ofNullable(route).map(Route::getUri).map(URI::getHost);
if (appId.isPresent()) {
String path = exchange.getRequest().getPath().value();
String method = exchange.getRequest().getMethodValue();
RequestMethod requestMethod = RequestMethod.valueOf(method);
List<SaIgnoreCollector> excludeList = SaIgnoreCollectorCache.getExcludeList("application");
if (!excludeList.isEmpty()) {
boolean unIgnore = excludeList.stream().anyMatch(it -> it.match(requestMethod, path));
if (unIgnore) {
return needLogin(exchange);
}
}
List<SaIgnoreCollector> includeList = SaIgnoreCollectorCache.getIncludeList("application");
if (includeList.isEmpty()) {
return needLogin(exchange);
}
return includeList.stream().anyMatch(it -> it.match(requestMethod, path));
} else {
// 非lb, 自行处理
}
return chain.filter(exchange);
}
@Override
public int getOrder() {
return -100;
}
public Mono<Void> needLogin(ServerWebExchange exchange) {
ServerHttpResponse response = exchange.getResponse();
response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
// todo: 构建自己的json
return response.writeWith(Mono.just(response.bufferFactory().wrap("{msg: xxx, code: xxx}".getBytes())));
}
}
非sa-token内部支持, 且对SaIgnore支持不全, 如确实有SaIgnore需求, 建议网关不执行鉴权策略, 由子服务自行鉴权
微服务体量的服务, 鉴权策略等信息应当尽可能与编码解耦, 但是开发团队规模不够大, 维护投入少的情况下, 还是推荐高耦合的编码方式, 以降低开发心智负担.