项目作者: xuanbg

项目描述 :
基于Spring Cloud Gateway的微服务网关
高级语言: Java
项目地址: git://github.com/xuanbg/gateway.git
创建时间: 2019-06-14T01:37:47Z
项目社区:https://github.com/xuanbg/gateway

开源协议:GNU General Public License v3.0

下载


基于 Spring Cloud Gateway 的网关使用说明

主要功能

通过对Http请求的拦截,根据接口配置数据实现对接口访问的限流和身份验证及鉴权功能。同时也在Info级别日志中输出请求参数、返回数据以及接口响应时间。
网关在转发请求前,将会添加以下请求头:

请求头 说明
requestId 请求ID,用于调用链路跟踪
fingerprint 客户端指纹,用于鉴别来源
loginInfo 包含应用ID、租户ID、用户ID等用户关键信息

网关的部分功能依赖于其他项目的配合

接口匹配

网关支持两种接口匹配模式:哈希匹配模式和正则匹配模式。对于包含URL路径参数的接口,只支持相对低效的正则匹配模式。所以请尽量避免使用包含路径参数的URL。请求URL如未匹配到接口,则会从Redis中加载数据更新哈希匹配表,再进行第二次哈希匹配。如仍然未能匹配到接口,则再次从Redis中加载数据更新正则匹配表,再进行第二次正则匹配。如二次匹配失败,则返回URL不存在的错误。

相关代码如下:

  1. InterfaceConfig config = getConfig(method, path);
  2. if (config == null) {
  3. reply = ReplyHelper.fail("请求的URL不存在");
  4. return initResponse(exchange);
  5. }
  1. /**
  2. * 通过匹配URL获取接口配置
  3. *
  4. * @param method 请求方法
  5. * @param url 请求URL
  6. * @return 接口配置
  7. */
  8. private InterfaceConfig getConfig(HttpMethod method, String url) {
  9. // 先进行哈希匹配
  10. String hash = Util.md5(method.name() + ":" + url);
  11. if (hashConfigs.containsKey(hash)) {
  12. return hashConfigs.get(hash);
  13. }
  14. // 哈希匹配失败后进行正则匹配
  15. String path = method + ":" + url;
  16. for (InterfaceConfig config : regConfigs) {
  17. String regular = config.getRegular();
  18. if (Pattern.compile(regular).matcher(path).matches()) {
  19. return config;
  20. }
  21. }
  22. // 重载配置进行哈希匹配
  23. hashConfigs = getHashConfigs();
  24. if (hashConfigs.containsKey(hash)) {
  25. return hashConfigs.get(hash);
  26. }
  27. // 重载配置进行正则匹配
  28. regConfigs = getRegularConfigs();
  29. for (InterfaceConfig config : regConfigs) {
  30. String regular = config.getRegular();
  31. if (Pattern.compile(regular).matcher(path).matches()) {
  32. return config;
  33. }
  34. }
  35. return null;
  36. }
  1. /**
  2. * 获取接口配置哈希表
  3. *
  4. * @return 接口配置表
  5. */
  6. private Map<String, InterfaceConfig> getHashConfigs() {
  7. String json = Redis.get("Config:Interface");
  8. List<InterfaceConfig> list = Json.toList(json, InterfaceConfig.class);
  9. Map<String, InterfaceConfig> map = new HashMap<>(list.size());
  10. for (InterfaceConfig config : list) {
  11. String url = config.getUrl();
  12. if (!url.contains("{")) {
  13. String hash = Util.md5(config.getMethod() + ":" + config.getUrl());
  14. map.put(hash, config);
  15. }
  16. }
  17. return map;
  18. }
  1. /**
  2. * 获取接口配置正则表
  3. *
  4. * @return 接口配置表
  5. */
  6. private List<InterfaceConfig> getRegularConfigs() {
  7. String json = Redis.get("Config:Interface");
  8. List<InterfaceConfig> list = Json.toList(json, InterfaceConfig.class);
  9. for (InterfaceConfig config : list) {
  10. String method = config.getMethod();
  11. String url = config.getUrl();
  12. if (url.contains("{")) {
  13. // 此正则表达式仅支持UUID作为路径参数,如使用其他类型的参数.请修改正则表达式以匹配参数类型
  14. String regular = method + ":" + url.replaceAll("/\\{[a-zA-Z]+}", "/[0-9a-f]{32}");
  15. config.setRegular(regular);
  16. }
  17. }
  18. return list.stream().filter(i -> i.getRegular() != null).collect(Collectors.toList());
  19. }

限流

接口限流可同时实现两种模式:

  1. 间隔限制模式。同一来源对接口的调用,必须大于设定的最小调用时间间隔。如调用间隔低于1秒,则重新进行计时作为惩罚。
  2. 次数限制模式。同一来源在单位时间内,调用次数不得高于设定的最大调用次数。限流周期从第一次调用开始计时,计时结束后,从下一次调用开始重新计时。

如满足限流条件,则返回请求过于频繁的错误(490)。如需实现接口限流,请在接口配置数据中配置以下参数:

参数 是否必需 说明
isLimit 是否限流,如该参数配置成false,则下面3个参数都不会起作用
limitGap 最小调用时间间隔(秒)
limitCycle 限流周期(秒)
limitMax 最大调用次数/限流周期
message 触发限流时反馈的错误消息

如未配置任何限流参数,即使isLimit为true也不能实现限流。

限流相关代码如下:

  1. if (config.getLimit()) {
  2. String key = method + ":" + path;
  3. String limitKey = Util.md5(fingerprint + "|" + key);
  4. if (isLimited(limitKey, config.getLimitGap(), config.getLimitCycle(), config.getLimitMax(), config.getMessage())) {
  5. return initResponse(exchange);
  6. }
  7. }
  1. /**
  2. * 是否被限流
  3. *
  4. * @param key 键值
  5. * @param gap 访问最小时间间隔(秒)
  6. * @param cycle 限流计时周期(秒)
  7. * @param max 限制次数/限流周期
  8. * @param msg 消息
  9. * @return 是否限制访问
  10. */
  11. private boolean isLimited(String key, Integer gap, Integer cycle, Integer max,String msg) {
  12. return isLimited(key, gap) || isLimited(key, cycle, max, msg);
  13. }
  1. /**
  2. * 是否被限流(访问间隔小于最小时间间隔)
  3. *
  4. * @param key 键值
  5. * @param gap 访问最小时间间隔
  6. * @return 是否限制访问
  7. */
  8. private boolean isLimited(String key, Integer gap) {
  9. if (key == null || key.isEmpty() || gap == null || gap.equals(0)) {
  10. return false;
  11. }
  12. key = "Surplus:" + key;
  13. String val = Redis.get(key);
  14. if (val == null || val.isEmpty()) {
  15. Redis.set(key, DateHelper.getDateTime(), gap, TimeUnit.SECONDS);
  16. return false;
  17. }
  18. Date time = DateHelper.parseDateTime(val);
  19. long bypass = System.currentTimeMillis() - Objects.requireNonNull(time).getTime();
  20. // 调用时间间隔低于1秒时,重置调用时间为当前时间作为惩罚
  21. if (bypass < 1000) {
  22. Redis.set(key, DateHelper.getDateTime(), gap, TimeUnit.SECONDS);
  23. }
  24. reply = ReplyHelper.tooOften();
  25. return true;
  26. }
  1. /**
  2. * 是否被限流(限流计时周期内超过最大访问次数)
  3. *
  4. * @param key 键值
  5. * @param cycle 限流计时周期(秒)
  6. * @param max 限制次数/限流周期
  7. * @param msg 消息
  8. * @return 是否限制访问
  9. */
  10. private Boolean isLimited(String key, Integer cycle, Integer max, String msg) {
  11. if (key == null || key.isEmpty() || cycle == null || cycle.equals(0) || max == null || max.equals(0)) {
  12. return false;
  13. }
  14. // 如记录不存在,则记录访问次数为1
  15. key = "Limit:" + key;
  16. String val = Redis.get(key);
  17. if (val == null || val.isEmpty()) {
  18. Redis.set(key, "1", cycle, TimeUnit.SECONDS);
  19. return false;
  20. }
  21. // 读取访问次数,如次数超过限制,返回true,否则访问次数增加1次
  22. Integer count = Integer.valueOf(val);
  23. long expire = Redis.getExpire(key, TimeUnit.SECONDS);
  24. if (count > max) {
  25. reply = ReplyHelper.tooOften(msg);
  26. return true;
  27. }
  28. count++;
  29. Redis.set(key, count.toString(), expire, TimeUnit.SECONDS);
  30. return false;
  31. }

身份验证及鉴权

只需在接口配置中设置isVerify参数为true,即可开启接口的身份验证功能。如设置了authCode参数,则在通过身份验证后再进行鉴权。鉴权的依据来自于对用户的授权数据(需要在资源中设置相应的授权码),授权数据会在用户获取Token时加载到Redis并与Token绑定。

相关代码如下:

  1. if (!config.getVerify()) {
  2. return chain.filter(exchange);
  3. }
  4. // 验证及鉴权
  5. String token = headers.getFirst("Authorization");
  6. boolean isVerified = verify(token, fingerprint, config.getAuthCode());
  7. if (!isVerified) {
  8. return initResponse(exchange);
  9. }
  1. /**
  2. * 验证用户令牌并鉴权
  3. *
  4. * @param token 令牌
  5. * @param fingerprint 用户特征串
  6. * @param authCodes 接口授权码
  7. * @return 是否通过验证
  8. */
  9. private boolean verify(String token, String fingerprint, String authCodes) {
  10. if (token == null || token.isEmpty()) {
  11. reply = ReplyHelper.invalidToken();
  12. return false;
  13. }
  14. Verify verify = new Verify(token, fingerprint);
  15. reply = verify.compare(authCodes);
  16. if (!reply.getSuccess()) {
  17. return false;
  18. }
  19. TokenInfo basis = verify.getBasis();
  20. loginInfo.setAppId(basis.getAppId());
  21. loginInfo.setTenantId(basis.getTenantId());
  22. loginInfo.setDeptId(basis.getDeptId());
  23. loginInfo.setUserId(verify.getUserId());
  24. loginInfo.setUserName(verify.getUserName());
  25. return true;
  26. }
  1. public class Verify {
  2. private final Logger logger = LoggerFactory.getLogger(this.getClass());
  3. /**
  4. * 令牌哈希值
  5. */
  6. private final String hash;
  7. /**
  8. * 缓存中的令牌信息
  9. */
  10. private TokenInfo basis;
  11. /**
  12. * 令牌ID
  13. */
  14. private String tokenId;
  15. /**
  16. * 用户ID
  17. */
  18. private String userId;
  19. /**
  20. * 构造方法
  21. *
  22. * @param token 访问令牌
  23. * @param fingerprint 用户特征串
  24. */
  25. public Verify(String token, String fingerprint) {
  26. // 初始化参数
  27. hash = Util.md5(token + fingerprint);
  28. AccessToken accessToken = Json.toAccessToken(token);
  29. if (accessToken == null) {
  30. logger.error("提取验证信息失败。TokenManage is:" + token);
  31. return;
  32. }
  33. tokenId = accessToken.getId();
  34. userId = accessToken.getUserId();
  35. basis = getToken();
  36. }
  37. /**
  38. * 验证Token合法性
  39. *
  40. * @param authCodes 接口授权码
  41. * @return Reply Token验证结果
  42. */
  43. public Reply compare(String authCodes) {
  44. if (basis == null) {
  45. return ReplyHelper.invalidToken();
  46. }
  47. if (isInvalid()) {
  48. return ReplyHelper.fail("用户已被禁用");
  49. }
  50. // 验证令牌
  51. if (!basis.verifyToken(hash)) {
  52. return ReplyHelper.invalidToken();
  53. }
  54. if (basis.isExpiry(true)) {
  55. return ReplyHelper.expiredToken();
  56. }
  57. if (basis.isFailure()) {
  58. return ReplyHelper.invalidToken();
  59. }
  60. // 无需鉴权,返回成功
  61. if (authCodes == null || authCodes.isEmpty()) {
  62. return ReplyHelper.success();
  63. }
  64. // 进行鉴权,返回鉴权结果
  65. if (isPermit(authCodes)) {
  66. return ReplyHelper.success();
  67. }
  68. String account = getUser().getAccount();
  69. logger.warn("用户『" + account + "』试图使用未授权的功能:" + authCodes);
  70. return ReplyHelper.noAuth();
  71. }
  72. /**
  73. * 获取令牌中的用户ID
  74. *
  75. * @return 是否同一用户
  76. */
  77. public boolean userIsEquals(String userId) {
  78. return this.userId.equals(userId);
  79. }
  80. /**
  81. * 获取缓存中的Token
  82. *
  83. * @return TokenInfo
  84. */
  85. public TokenInfo getBasis() {
  86. return basis;
  87. }
  88. /**
  89. * 获取令牌持有人的用户ID
  90. *
  91. * @return 用户ID
  92. */
  93. public String getUserId() {
  94. return userId;
  95. }
  96. /**
  97. * 获取令牌持有人的用户名
  98. *
  99. * @return 用户名
  100. */
  101. public String getUserName() {
  102. return getUser().getName();
  103. }
  104. /**
  105. * 根据令牌ID获取缓存中的Token
  106. *
  107. * @return TokenInfo(可能为null)
  108. */
  109. private TokenInfo getToken() {
  110. String key = "Token:" + tokenId;
  111. String json = Redis.get(key);
  112. return Json.toBean(json, TokenInfo.class);
  113. }
  114. /**
  115. * 用户是否被禁用
  116. *
  117. * @return User(可能为null)
  118. */
  119. private boolean isInvalid() {
  120. String key = "User:" + userId;
  121. String value = Redis.get(key, "IsInvalid");
  122. if (value == null) {
  123. return true;
  124. }
  125. return Boolean.parseBoolean(value);
  126. }
  127. /**
  128. * 读取缓存中的用户数据
  129. *
  130. * @return 用户数据
  131. */
  132. private User getUser() {
  133. String key = "User:" + userId;
  134. String value = Redis.get(key, "User");
  135. return Json.toBean(value, User.class);
  136. }
  137. /**
  138. * 指定的功能是否授权给用户
  139. *
  140. * @param authCode 接口授权码
  141. * @return 功能是否授权给用户
  142. */
  143. private Boolean isPermit(String authCode) {
  144. List<String> functions = basis.getPermitFuncs();
  145. if (functions == null){
  146. return false;
  147. }
  148. return functions.stream().anyMatch(i -> {
  149. String[] codes = i.split(",");
  150. for (String code : codes) {
  151. if (authCode.equalsIgnoreCase(code)) {
  152. return true;
  153. }
  154. }
  155. return false;
  156. });
  157. }
  158. }