前言
首先,接口微服务间内部调用,最需要考虑的点就是服务提供方如何动态的辨别哪些接口需要暴露出去,而不过大的开放访问范围,避免发生安全问题。
其次,如何才能确保这些暴露出去的接口正常地接受前端服务请求或者网关转发过来的请求,而不会影响第三方客户端的调用。
服务端设计
上次在客户端中提到服务端的接口鉴权请求分为四种:
1.外部GateWay转发的用户请求,需要进行正常的鉴权。
2.外部GateWay的服务间调用求求,不需要进行鉴权。
3.内部微服务间的调用,请求中携带token令牌。
4.外部第三方服务调用,越过鉴权且需要进行请求校验。
首先,如果是不需要进行鉴权的情况,那么在服务中只需要将这个接口添加到security过滤链的permitAll白名单即可。
其次,如果外部不能直接访问服务,并且内部服务间不需要认证鉴权的情况。先将该接口动态的添加到security过滤链的permitAll白名单,和校验accesstoken的白名单(BearerTokenResolver)中,然后在调用接口时直接通过security过滤链,然后通过注解@Inner的value值判断是否是内部调用,并且通过判断头上是否有指定字段的方式进行校验。
最后,如果外部需要鉴权访问服务,并且内部服务间不需要认证鉴权的情况。除了正常的security过滤链的permitAll白名单和校验accesstoken的白名单以及@Inner注解外,还需要通过GateWay网关的拦截器,去掉外部请求头上的指定字段。
服务端代码
外部暴露网关清除请求的拦截器代码,适用于外部请求需要鉴权的情况
public class InnerRequestGlobalFilter implements GlobalFilter, Ordered {
private static final String HEADER_NAME = "指定字段";
/**
* Process the Web request and (optionally) delegate to the next
* {@code WebFilter} through the given {@link GatewayFilterChain}.
*
* @param exchange the current server exchange
* @param chain provides a way to delegate to the next filter
* @return {@code Mono<Void>} to indicate when request processing is complete
*/
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 1. 清洗请求头中from 参数
ServerHttpRequest request = exchange.getRequest().mutate()
.headers(httpHeaders -> {httpHeaders.remove(SecurityConstants.FROM);})
.build();
return chain.filter(exchange.mutate()
.request(newRequest.mutate()
.header(HEADER_NAME, basePath)
.build()).build());
}
@Override
public int getOrder() {
return -1000;
}
}
外部暴露网关清除请求的拦截器代码(外部需要鉴权访问)
动态配置服务端需要暴露的接口地址,在security请求链创建前,动态进行列表值修改
@Configuration
@ConditionalOnExpression("!'${user-settings.ignore-url}'.isEmpty()")
@Order(20)
public class PermitIgnoreUrlProperties implements InitializingBean {
private static final Pattern PATTERN = Pattern.compile("\\{(.*?)\\}");
@Autowired
private WebApplicationContext applicationContext;
@Autowired
private UserSetting userSetting;
@Override
public void afterPropertiesSet() throws Exception {
RequestMappingHandlerMapping mapping = applicationContext.getBean(RequestMappingHandlerMapping.class);
Map<RequestMappingInfo, HandlerMethod> map = mapping.getHandlerMethods();
//初始化过滤地址的集合
Set<String> ignoreUrlSet = new HashSet<>();
map.keySet().forEach(info -> {
HandlerMethod handlerMethod = map.get(info);
// 获取方法上边的注解 替代path variable 为 *
Inner method = AnnotationUtils.findAnnotation(handlerMethod.getMethod(), Inner.class);
Optional.ofNullable(method)
.ifPresent(inner -> info.getPathPatternsCondition().getPatterns()
.forEach(url -> ignoreUrlSet.add(ReUtil.replaceAll(url.getPatternString(), PATTERN, "**"))));
// 获取类上边的注解, 替代path variable 为 *
Inner controller = AnnotationUtils.findAnnotation(handlerMethod.getBeanType(), Inner.class);
Optional.ofNullable(controller)
.ifPresent(inner -> info.getPathPatternsCondition().getPatterns()
.forEach(url -> ignoreUrlSet.add(ReUtil.replaceAll(url.getPatternString(), PATTERN, "**"))));
});
List<String> handleIgnoreUrl = Arrays.asList(ignoreUrlSet.stream()
.collect(Collectors.joining(","))
.split(","));
userSetting.setIgnorePath(handleIgnoreUrl);
}
}
动态配置服务端需要暴露的接口地址
用来标注内部访问接口的Inner注释
import java.lang.annotation.*;
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Inner {
/**
* 是否AOP统一处理
*
* @return false, true
*/
boolean value() default true;
/**
* 需要特殊判空的字段(预留)
*
* @return {}
*/
String[] field() default {};
}
判断@Inner注解标注的暴露接口,是否存在指定头信息的切
@SneakyThrows
@Around("@annotation(inner)")
public Object aroundInner(ProceedingJoinPoint point, Inner inner){
LoginUser userInfo = SecurityUtils.getUserInfo();
//如果为登录访问则直接放行
if(userInfo!=null){
return point.proceed();
}
//获取第三方客户端请求接口的指定头信息值
String thirdClientApiValue = request.getHeader(ThirdAssociateEnum.ALLOW_OUT_CLIENT_API.getValue());
//获取请求路径
String requestURI = request.getRequestURI();
//获取当天的日期
String formattedDate = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd"));
//如果开启Aop校验
if (BooleanUtil.isTrue(inner.value())) {
if(StrUtil.isEmpty(thirdClientApiValue)||!encoder.matches(formattedDate + File.separator + requestURI + formattedDate,thirdClientApiValue)){
log.warn("接口{}请求未查询到放行的指定Header信息,没有权限!",point.getSignature().getName());
throw new AccessDeniedException("请求未查询到放行的指定Header信息,没有权限");
}
}
return point.proceed();
}
@Inner拦截切面