一、背景
本文主要讲述微服务模式,API网关和后端服务的分工与协调。我们采用kong作为API网关,承担的是非功能性的工作,包括Token校验、cors跨域、接口开放、限流、监控、流量染色等。
Kong自带的有一些插件,详见下图,我们用到的主要有:
Authentication
Key Auth
Security
Acl、Cors、Ip Restriction
Traffic Control
Rate Limiting
Analytics & Monitoring
Prometheus

- Key Auth 在http header里传入api key字段,并且校验
- Cors 解决前后端的跨域问题
- Ip Restriction 基于IP的限流
- Prometheus 采集调用次数与耗时的指标
自定义插件
- auth 进行token校验、签名校验
- Canary Release 灰度发布,用来对流量进行染色
- Upstream 接口开放,按需配置,让你的接口更加安全
- Rewrite 对接口的URI进行正则匹配,替换或截取
二、目标
- 后端服务做到无状态的
- 便于弹性伸缩
- 提高接口的安全性
三、服务调用整体框架
这里以某个后端服务为例,梳理了内外网、服务之间的调用链路。

-
内网调用
要么走内网域名,要么通过consul服务发现。 当然这里也涉及到内部的调用安全问题,暂时未纳入考虑范围。
内网网关也选择Kong的原因是需要对内网流量进行染色,适用于灰度发布。 -
外网调用
前端调用,可能是没有token或签名,还需要解决跨域问题(建议将资源都发布到oss,一次性做好跨域处理)
端的调用,一般都是需要token和签名的,会由Kong去调用认证服务,校验合法性,并返回userId,透传到后端服务。 -
角色权限
除了token的合法性外,还有userId和token是否一致、用户的角色是否能够访问某个接口、不同的角色能够访问的资源不同(需要做数据的过滤或区分)。
目前平台是把权限这块下沉到具体的服务自己实现。所有后文,我会总结下如何简单实现。 -
应用监控
采用ip + port的方式,适用于所有的可观测性指标的采集。 -
接口测试
建议使用内网的ip+port的方式,当然你还需要对内网域名和外网域名下的访问做回归测试,甚至是不同的客户端应用版本,也需要做回归测试。
四、域名的管理
假定公司有三套环境,生产、测试和开发,对应的内网域名都是一样的,外网域名分别是xxx.net / xxx.test.com / xxx.dev.com。
内网域名进来的请求,都不需要token校验,但还是需要解决跨域问题,上图这一点没有全部描述出来。
外网域名进来的请求,除了登录接口、获取短信验证码等不需要token校验外,都是需要由kong做请求拦截的。
当然,能够走consul服务发现的服务之间调用,尽量走consul了。
五、权限的简单实现
编写自己的自定义注解,并在接口层使用该注解;拦截器中扫描到自定义的注解,将http header透传下来的userId,查询它的权限及角色; 进行权限的校验,并将用户信息保存在上下文里。
5.1、自定义注解
/**
* 权限限制.
*
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface PermissionLimit {
/**
* 权限校验(默认true)
*/
boolean limit() default true;
/**
* 要求的权限标签列表
*
* @return
*/
AuthorityEnum[] authorityTagSet() default {AuthorityEnum.ADMIN};
}
5.2、引用注解
@PermissionLimit(authorityTagSet = {AuthorityEnum.MANAGER, AuthorityEnum.ADMIN})
5.3、角色的枚举
import org.apache.commons.lang3.StringUtils;
import java.util.Arrays;
import java.util.Collections;
import java.util.Map;
import java.util.function.Function;
import static java.util.stream.Collectors.toMap;
/**
* 权限枚举
*
* @author Administrator
*/
public enum AuthorityEnum {
/**
* 超级管理员
*/
ADMIN("admin", "超级管理员"),
/**
* 管理员
*/
MANAGER("auditor", "审核员"),
/**
* 用户
*/
USER("user", "用户");
private String code;
private String name;
AuthorityEnum(String code, String name) {
this.code = code;
this.name = name;
}
public String getCode() {
return code;
}
public String getName() {
return name;
}
public static String getName(String code) {
return CODE_MAP.containsKey(code) ? CODE_MAP.get(code).getName() : null;
}
private static final Map<String, AuthorityEnum> CODE_MAP =
Collections.unmodifiableMap(Arrays.stream(values()).collect(toMap(AuthorityEnum::getCode, Function.identity(), (v1, v2) -> v2)));
public static AuthorityEnum of(String ordinal) {
if(StringUtils.isEmpty(ordinal) || !CODE_MAP.containsKey(ordinal)){
return USER;
}
return CODE_MAP.get(ordinal);
}
}
5.4、权限拦截器
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.NumberUtil;
import cn.hutool.core.util.StrUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Arrays;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
/**
* 权限拦截
*
* @author zhuwenping
*/
@Slf4j
@Component
public class PermissionInterceptor extends HandlerInterceptorAdapter {
@Autowired
private UserService userService;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (!(handler instanceof HandlerMethod)) {
return super.preHandle(request, response, handler);
}
HandlerMethod method = (HandlerMethod) handler;
PermissionLimit permission = method.getMethodAnnotation(PermissionLimit.class);
if (Objects.nonNull(permission)) {
//0、是否开启校验权限,如果否,则跳过此步骤。
if (!permission.limit()) {
return super.preHandle(request, response, handler);
}
AuthorityEnum[] authorityTagArray = permission.authorityTagSet();
Set<AuthorityEnum> authorityTagSet = Arrays.stream(authorityTagArray).collect(Collectors.toSet());
//1、从token中解析出当前登录用户的userId
String authUserIdStr = request.getHeader(JwtAuthHeaders.AUTH_USER_ID);
Precondition.isTrue(StrUtil.isNotBlank(authUserIdStr), "用户未登录");
Precondition.isTrue(NumberUtil.isNumber(authUserIdStr), "无效的用户ID");
//2、查询用户的权限标签
UserDTO userDTO = userService.getUser(Long.parseLong(authUserIdStr));
Precondition.isTrue(Objects.nonNull(userDTO), "用户不存在");
if (CollectionUtil.isNotEmpty(authorityTagSet)) {
Precondition.isTrue(authorityTagSet.contains(AuthorityEnum.of(userDTO.getAuthorityTag())), "用户的权限不足");
}
//3、把用户的权限保存到线程上下文
UserAuthorityThreadLocal.setAuthority(AuthorityEnum.of(userDTO.getAuthorityTag()));
}
return super.preHandle(request, response, handler);
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
log.info("进入到拦截器中:afterCompletion() 方法中");
// remove线程上下文中的用户权限
UserAuthorityThreadLocal.remove();
}
}
5.5、用户信息的上下文
/**
* 用户权限保存至线程上下文.
*
*/
public class UserAuthorityThreadLocal {
static InheritableThreadLocal<AuthorityEnum> authorityContext = new InheritableThreadLocal<AuthorityEnum>() {
@Override
protected AuthorityEnum initialValue() {
return super.initialValue();
}
};
public static void setAuthority(AuthorityEnum authority) {
authorityContext.set(authority);
}
public static void remove() {
authorityContext.remove();
}
public static AuthorityEnum getAuthority() {
return authorityContext.get();
}
}
5.6、使用示例
//1、if/else判断,程序走向不同的逻辑
if (AuthorityEnum.ADMIN.equals(UserAuthorityThreadLocal.getAuthority()) || AuthorityEnum.MANAGER.equals(UserAuthorityThreadLocal.getAuthority())) {
//
} else {
//
}
//2、流计算中作filter数据过滤
List<QueryQuestionRes> sortedList = randomList.stream()
.filter(q -> AuthorityEnum.ADMIN.equals(UserAuthorityThreadLocal.getAuthority()))
.collect(Collectors.toList());
六、待补充的部分
- 灰度发布,流量染色
- http header中的userId透传
- userId和token是否一致的问题
- kong的自定义插件
Kong的自定义插件
- kong的可观测性,除了Prometheus能够采集的指标外,我们还开发了打印日志功能,将一些异常情况,输出到指定的文件,然后上报到ELK。
- Kong的upsteam中的target需要和consul中的服务节点保持一致,做到实时地动态更新,减少运维过程中带来的不必要的错误。建议在发布过程中,调用kong 的 api接口,添加或删除target。 这样,你监控了consul中的服务节点,如果是健康的,也就可以保证upstream的target节点也是健康无误。
- 流量染色插件,这里的配套实现离不开java agent技术。
网友评论