🌈Yu-Gateway::基于 Netty 构建的自研 API 网关,采用 Java 原生实现,整合 Nacos 作为注册配置中心。其设计目标是为微服务架构提供高性能、可扩展的统一入口和基础设施,承载请求路由、安全控制、流量治理等核心网关职能。
🌈项目代码地址:https://github.com/YYYUUU42/YuGateway-master
如果该项目对你有帮助,可以在 github 上点个 ⭐ 喔 🥰🥰
🌈自研网关系列:可以点开专栏,参看完整的文档
目录
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);
@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 修改会报错