🌈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可用于在不同系统之间安全地传递信息,例如在微服务架构中进行服务之间的通信。
流程:
- 客户端携带令牌访问资源服务获取资源。
- 资源服务远程请求认证服务校验令牌的合法性
- 如果令牌合法资源服务向客户端返回资源。
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);@Overridepublic 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 修改会报错