【自研网关系列】过滤器链 -- 鉴权过滤器

🌈Yu-Gateway:基于 Netty 构建的自研 API 网关,采用 Java 原生实现,整合 Nacos 作为注册配置中心。其设计目标是为微服务架构提供高性能、可扩展的统一入口和基础设施,承载请求路由、安全控制、流量治理等核心网关职能。

🌈项目代码地址:https://github.com/YYYUUU42/YuGateway-master

如果该项目对你有帮助,可以在 github 上点个 ⭐ 喔 🥰🥰

🌈自研网关系列:可以点开专栏,参看完整的文档

目录

什么是JWT

鉴权过滤器实现过程

配置文件修改

用户登录

登录具体实现

鉴权功能具体实现


1、什么是JWT

在自研网关这个项目中,主要是使用 Jwt 来实现简易的鉴权功能

JWT(JSON Web Token)是一种开放标准(RFC 7519),用于在各方之间安全地传输信息作为 JSON 对象。它通常用于在不同系统之间进行身份验证和授权,以及在各种应用中传递声明性信息。

JWT 由三部分组成,它们通过点号(.)分隔:

  • Header(头部):包含了关于生成的 JWT 的元数据信息,例如算法和令牌类型。
  • Payload(负载):包含了实际的声明(claim)信息,这些声明是关于实体(通常是用户)和其他数据的信息。有三种类型的声明:注册声明、公共声明和私有声明。
  • Signature(签名):用于验证JWT的完整性,确保数据在传输过程中没有被篡改。签名是基于头部和负载,使用一个密钥(秘密或公开的)进行加密生成的。

JWT 的作用包括:

  • 身份验证:JWT可用于验证用户身份,确保请求来自经过身份验证的用户。用户登录后,可以生成JWT并将其存储在客户端,然后在后续请求中使用它来证明身份。
  • 授权:JWT可以包含用户的授权信息,以便在服务器端验证用户是否有权限执行某个操作或访问某个资源。
  • 信息交换:JWT可用于在不同系统之间安全地传递信息,例如在微服务架构中进行服务之间的通信。

流程:

  1. 客户端携带令牌访问资源服务获取资源。
  2. 资源服务远程请求认证服务校验令牌的合法性
  3. 如果令牌合法资源服务向客户端返回资源。

2、鉴权过滤器实现过程

2.1、配置文件修改

修改 nacos 上的配置文件,添加需要鉴权的路径

{
  "rules": [
    {
      "id": "user-private",
      "name": "user-private",
      "paths": [
        "/user/userInfo"
      ],
      "prefix": "/user/private",
      "protocol": "http",
      "serviceId": "backend-user-server",
      "filterConfigs": [
        {
          "config": {
            "load_balance": "Random"
          },
          "id": "load_balance_filter"
        },
{
          "id": "auth_filter",
          "config": {
            "auth_path": [
              "/user/userInfo"
            ]
          }
        }
      ]
    },
    {
      "id": "user",
      "name": "user",
      "paths": [
        "/user/login"
      ],
      "prefix": "/user",
      "protocol": "http",
      "serviceId": "backend-user-server",
      "filterConfigs": [
        {
          "config": {
            "load_balance": "Random"
          },
          "id": "load_balance_filter"
        }
      ]
    },
    {
      "id": "http-server",
      "name": "http-server",
      "paths": [
        "/http-server/ping"
      ],
      "prefix": "/http-server",
      "protocol": "http",
      "retryConfig": {
        "times": 3
      },
      "serviceId": "backend-http-server",
      "filterConfigs": [
        {
          "config": {
            "load_balance": "RoundRobin"
          },
          "id": "load_balance_filter"
        },
        {
          "id": "auth_filter"
        }
      ]
    }
  ]
}

2.2、用户登录

简单模拟登录接口,生成一个包含用户信息的JWT token,将这个token设置为Cookie并返回

@ApiInvoker(path = "/user/login")
@GetMapping("/user/login")
public String login(@RequestParam("phoneNumber") String phoneNumber,
					@RequestParam("code") String code,
					HttpServletResponse response) {
	Map<String, Object> params = new HashMap<>();
	params.put(FilterConst.TOKEN_USERID_KEY, String.valueOf(phoneNumber + code));
	String token = JWTUtil.generateToken(params, FilterConst.TOKEN_SECRET);
	
    log.info("token:{}", token);
	response.addCookie(new Cookie(FilterConst.COOKIE_KEY, token));
	return token;
}

2.3、登录具体实现

首先会根据请求头得到注册到 nacos 的实例信息,和请求路径配对,得到 Rules 配置文件中的过滤器链配置

这里会和 spi 文件存在的过滤器链和 nacos 上的 Rules 过滤器配置进行判断,得到最终的过滤器链

这是nacos 上的 Rules 过滤器配置

这是 spi 的过滤器信息

由于是用户登录,所以是没有鉴权过滤器的

最终过滤器,一般来讲,在大多数网关项目中,负载均衡和路由转发通常是必要的过滤器

  • 负载均衡:当有多个实例提供相同的服务时,负载均衡器可以将请求分发到这些实例中的一个,以确保所有实例的负载均匀,提高系统的可用性和伸缩性。
  • 路由转发:路由转发是将客户端的请求转发到正确的服务实例的过程。在微服务架构中,由于服务实例可能分布在不同的服务器或容器中,因此需要一个路由机制来确定将请求转发到哪个服务实例。

全部执行完后,会将 token 存储在 Cookie 中,鉴权部分会用到

2.4、鉴权功能具体实现

请求结果

简单模拟请求接口

@ApiInvoker(path = "/user/userInfo")
@GetMapping("/user/userInfo")
public UserInfo getUserInfo(@RequestHeader("userId") String userId) {
	log.info("userId :{}", userId);
	return UserInfo.builder()
			.id(Integer.parseInt(userId))
			.name("yu")
			.phoneNumber("1234")
			.build();
}

实现流程和登录差不多,就是多了鉴权过滤器,具体代码

/**
 * @author yu
 * @description 鉴权过滤器
 */
@Slf4j
@FilterAspect(id= FilterConst.AUTH_FILTER_ID, name = FilterConst.AUTH_FILTER_NAME, order = FilterConst.AUTH_FILTER_ORDER)
public class AuthFilter implements Filter {

    private final Logger logger = LoggerFactory.getLogger(AuthFilter.class);

    @Override
	public void doFilter(GatewayContext ctx) throws Exception {
		// 遍历所有的过滤器配置
		for (Rule.FilterConfig config : ctx.getRules().getFilterConfigs()) {
			// 如果当前的过滤器ID不是我们需要的过滤器ID,那么就跳过这个过滤器配置
			if (!config.getId().equals(FilterConst.AUTH_FILTER_ID)) {
				continue;
			}


			// 解析过滤器配置,获取到我们需要的认证路径
			List<String> authPaths = new ArrayList<>();
			Map<String, List<String>> configMap = new ConcurrentHashMap<>();

			if (config.getConfig() != null) {
				configMap = JSON.parseObject(config.getConfig(), Map.class);
				authPaths = configMap.getOrDefault(FilterConst.AUTH_FILTER_KEY, new ArrayList<>());
			}

			// 获取当前请求的路径
			String curRequestKey = ctx.getRequest().getPath();

			// 如果当前请求的路径不是我们需要的认证路径,那么就返回,不进行后续的处理
			if (!authPaths.contains(curRequestKey)) {
				return;
			}

			// 从请求中获取token,如果token不存在,那么就抛出一个未授权的异常
			String token = Optional.ofNullable(ctx.getRequest().getCookie(FilterConst.COOKIE_KEY))
					.map(Cookie::value)
					.orElseThrow(() -> new ResponseException(ResponseCode.UNAUTHORIZED));

			// 对获取到的token进行验证
			authenticateToken(ctx, token);
		}
	}

	/**
	 * 验证token
	 */
	private void authenticateToken(GatewayContext ctx, String token) {
		try {
			long tokenUserId = parseUserIdFromToken(token);

			String headerUserId = ctx.getRequest().getHeaders().get("userId");
			String pathUserId = ctx.getRequest().getQueryParametersMultiple("userId").get(0);
			String actualUserId = headerUserId != null ? headerUserId : pathUserId;

			if (actualUserId == null || Long.parseLong(actualUserId) != tokenUserId) {
				throw new ResponseException(ResponseCode.USERID_MISMATCH);
			}

			ctx.getRequest().setUserId(tokenUserId);
			log.info("AuthFilter 解析 token 成功, userId {}", tokenUserId);
		} catch (Exception e) {
			log.info("AuthFilter 解析 token 失败, 请求路径 {}", ctx.getRequest().getPath());
			throw new ResponseException(ResponseCode.UNAUTHORIZED);
		}
	}


	/**
	 * 解析token中的载荷——用户ID
	 */
	private long parseUserIdFromToken(String token) {
		// 使用Optional来处理可能为null的值
		Optional.ofNullable(token)
				.filter(t -> !t.isEmpty())
				.orElseThrow(() -> new IllegalArgumentException("Token cannot be null or empty."));

		Jwt jwt = null;

		try {
			// 使用静态解析器实例,提高性能
			jwt = Jwts.parser().setSigningKey(FilterConst.TOKEN_SECRET).parse(token);
		} catch (SignatureException | ExpiredJwtException e) {
			throw new RuntimeException("Token 验证错误: ", e);
		}

		try {
			// 验证字符串是否可以转换为long类型,并检查范围
			DefaultClaims claims = (DefaultClaims) jwt.getBody();
			String jwtUserId = claims.get("userId", String.class);
			long userId = Long.parseLong(jwtUserId);

			if (userId == Long.MIN_VALUE || userId == Long.MAX_VALUE) {
				throw new IllegalArgumentException("UseId 超出范围.");
			}
			return userId;
		} catch (NumberFormatException e) {
			logger.error("UserId 解析错误: ", e);
			throw new IllegalArgumentException("无效 userId 格式");
		}

	}
}

匹配鉴权过滤器配置

判断当前请求路径是否需要鉴权的

// 解析过滤器配置,获取到我们需要的认证路径
List<String> authPaths = new ArrayList<>();
Map<String, List<String>> configMap = new ConcurrentHashMap<>();
if (config.getConfig() != null) {
	configMap = JSON.parseObject(config.getConfig(), Map.class);
	authPaths = configMap.getOrDefault(FilterConst.AUTH_FILTER_KEY, new ArrayList<>());
}

// 获取当前请求的路径
String curRequestKey = ctx.getRequest().getPath();

// 如果当前请求的路径不是我们需要的认证路径,那么就返回,不进行后续的处理
if (!authPaths.contains(curRequestKey)) {
	return;
}

首先对 token 解析,jwt = Jwts.parser().setSigningKey(FilterConst.TOKEN_SECRET).parse(token);

对请求 id,判断请求 id 是否和 token 解析的 id 相同,

long tokenUserId = parseUserIdFromToken(token);

String headerUserId = ctx.getRequest().getHeaders().get("userId");
String pathUserId = ctx.getRequest().getQueryParametersMultiple("userId").get(0);
String actualUserId = headerUserId != null ? headerUserId : pathUserId;

if (actualUserId == null || Long.parseLong(actualUserId) != tokenUserId) {
	throw new ResponseException(ResponseCode.USERID_MISMATCH);
}

用户 id 和 token 中的不同或 Cookie 修改会报错

  • 19
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
lamp-cloud微服务脚手架的前身是zuihou-admin-cloud,从3.0.0版本开始,改名为lamp-cloud,它是lamp项目的其中一员。 lamp-cloud微服务脚手架是一个基于SpringCloud(Hoxton.SR10) + SpringBoot(2.3.10.RELEASE)的SaaS微服务脚手架,具有统一授、认证后台管理系统,其中包含具备用户管理、资源限管理、关API、分布式事务、大文件断点分片续传等多个模块,支持多业务系统并行开发,可以作为后端服务的开发脚手架。代码简洁,架构清晰,适合学习和直接项目中使用。核心技术采用Nacos、Fegin、Ribbon、Zuul、Hystrix、JWT Token、Mybatis、SpringBoot、Redis、RibbitMQ等主要框架和中间件。 lamp-cloud微服务脚手架功能: 1、服务注册&发现与调用: 基于Nacos来实现的服务注册与发现,使用使用Feign来实现服务互调, 可以做到使用HTTP请求远程调用时能与调用本地方法一样的编码体验,开发者完全感知不到这是远程方法,更感知不到这是个HTTP请求。 2、服务: 通过JWT的方式来加强服务之间调度的限验证,保证内部服务的安全性。 3、负载均衡: 将服务保留的rest进行代理和关控制,除了平常经常使用的node.js、nginx外,Spring Cloud系的zuul和ribbon,可以帮我们进行正常的关管控和负载均衡。其中扩展和借国外项目的扩展基于JWT的Zuul限流插件,方面进行限流。 4、熔断机制: 因为采取了服务的分布,为了避免服务之间的调用“雪崩”,采用了Hystrix的作为熔断器,避免了服务之间的“雪崩”。 5、监控: 利用Spring Boot Admin 来监控各个独立Service的运行状态;利用turbine来实时查看接口的运行状态和调用频率;通过Zipkin来查看各个服务之间的调用链等。 6、链路调用监控: 利用Zipkin实现微服务的全链路性能监控, 从整体维度到局部维度展示各项指标,将跨应用的所有调用链性能信息集中展现,可方便度量整体和局部性能,并且方便找到故障产生的源头,生产上可极大缩短故障排除时间。有了它,我们能做到: 请求链路追踪,故障快速定位:可以通过调用链结合业务日志快速定位错误信息。 可视化:各个阶段耗时,进行性能分析。 依赖优化:各个调用环节的可用性、梳理服务依赖关系以及优化。 数据分析,优化链路:可以得到用户的行为路径,汇总分析应用在很多业务场景。 7、数据限 利用基于Mybatis的DataScopeInterceptor拦截器实现了简单的数据限 8、SaaS(多租户)的无感解决方案 使用Mybatis拦截器实现对所有SQL的拦截,修改默认的Schema,从而实现多租户数据隔离的目的。 并且支持可插拔。 9、二级缓存 采用J2Cache操作缓存,第一级缓存使用内存(Caffeine),第二级缓存使用 Redis。 由于大量的缓存读取会导致 L2 的络成为整个系统的瓶颈,因此 L1 的目标是降低对 L2 的读取次数。 该缓存框架主要用于集群环境中。单机也可使用,用于避免应用重启导致的缓存冷启动后对后端业务的冲击。 10、优雅的Bean转换 采用Dozer组件来对 DTO、DO、PO等对象的优化转换 11、前后端统一表单验证 严谨的表单验证通常需要 前端+后端同时验证, 但传统的项目,均只能前后端各做一次检验, 后期规则变更,又得前后端同时修改。 故在hibernate-validator的基础上封装了zuihou-validator-starter起步依赖,提供一个通用接口,可以获取需要校验表单的规则,然后前端使用后端返回的规则, 以后若规则改变,只需要后端修改即可。 12、防跨站脚本攻击(XSS) 通过过滤器对所有请求中的 表单参数 进行过滤 通过Json反序化器实现对所有 application/json 类型的参数 进行过滤 13、当前登录用户信息注入器 通过注解实现用户身份注入 14、在线API 由于原生swagger-ui某些功能支持不够友好,故采用了国内开源的swagger-bootstrap-ui,并制作了stater,方便springboot用户使用。 15、代码生成器 基于Mybatis-plus-generator自定义了一套代码生成器, 通过配置数据库字段的注释,自动生成枚举类、数据字典注解、SaveDTO、UpdateDTO、表单验证规则注解、Swagger注解等。 16、定时任务调度器: 基于xxl-jobs进行了功能增强。(如:指定时间发送任务、执行器和调度器合并项目、多数据源) 17、大文

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值