gateway网关细粒度至 指定接口 免鉴权实现

537 阅读4分钟

 前言

在现在微服务架构中,我们常常将鉴权的工作交由网关来处理。那么类似于 登录、注册 的免鉴权接口,我们要如何实现呢?

鉴权方案

JWT介绍

首先,我们先简单了解一下 JWT 的原理。

JSON Web Token(JWT) 是一个非常轻巧的规范。这个规范允许我们使用JWT在用户和服务器之间传递安全可靠的信息。

一个JWT实际上就是一个字符串,它由三部分组成,头部、载荷与签名

头部(Header)

头部用于描述关于该JWT的最基本的信息,例如其类型以及签名所用的算法等。这也可以被表示成一个JSON对象。

{"typ":"JWT","alg":"HS256"}

在头部指明了签名算法是HS256算法。 我们进行BASE64编码

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9

载荷(playload)

载荷就是存放有效信息的地方,这里会在 生成 JWT时将信息进行编码,可以将我们可能需要用到的东西存在这里面,如 userIdusername 等,将来可以在解析后使用

定义一个payload:

{"sub":"1234567890","name":"itlils","admin":true,"age":18}

然后将其进行base64加密,得到Jwt的第二部分。

eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6Iml0bGlscyIsImFkbWluIjp0cnVlLCJhZ2UiOjE4fQ==

签证(signature)

jwt的第三部分是一个签证信息,这个签证信息由三部分组成:

header (base64后的)

payload (base64后的)

secret

这个部分需要base64加密后的header和base64加密后的payload使用.连接组成的字符串,然后通过header中声明的加密方式进行加盐secret组合加密,然后就构成了jwt的第三部分。

hs256("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6Iml0bGlscyIsImFkbWluIjp0cnVlLCJhZ2UiOjE4fQ==",secret)

将这三部分用.连接成一个完整的字符串,构成了最终的jwt

JTdCJTIydHlwJTIyJTNBJTIySldUJTIyJTJDJTIyYWxnJTIyJTNBJTIySFMyNTYlMjIlN0Q=.JTdCJTIyc3ViJTIyJTNBJTIyMTIzNDU2Nzg5MCUyMiUyQyUyMm5hbWUlMjIlM0ElMjJqYWNrJTIyJTJDJTIyYWRtaW4lMjIlM0F0cnVlJTdE.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

一般鉴权流程

微服务鉴权流程

其实微服务的鉴权流程没有太大差别,只是鉴权的位置不一样了。

一般我们会在 网关里完成 验证、解析、处理等操作

所以用户的请求也都会向这里发送

免鉴权方案

网关代码示例

我们一般使用网关的全局过滤器来完成这个工作,上代码

/**
 * jwt 认证过滤器
 *
 * @author durance
 */
@Component
@Slf4j
public class AuthorizeFilter implements GlobalFilter, Ordered {

	public static final String AUTHORIZE_TOKEN = "token";

	@Override
	public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
		ServerHttpRequest request = exchange.getRequest();
		ServerHttpResponse response = exchange.getResponse();
		// 进行请求路径判度,放行不需要认证的接口
		String path = request.getURI().getPath();
		String[] matchers = JwtProperties.matchers;
		for (String matcher : matchers) {
			if (path.contains(matcher)) {
				return chain.filter(exchange);
			}
		}
		// 拿到jwt的值
		String jwt = request.getHeaders().getFirst(AUTHORIZE_TOKEN);
		// 判度是否为空
		if (StringUtils.isEmpty(jwt)) {
			response.setStatusCode(HttpStatus.UNAUTHORIZED);
			return response.setComplete();
		}
		// 如果不为空则进行效验
		try {
			// 效验jwt正确性,如果错误会抛出异常
			JwtUtil.parseJwt(jwt);
			// 解析jwt拿到jwt的载荷跟其余信息
			Claims claims = JwtUtil.parseJwt(jwt);
			Integer userId = (Integer) claims.get("userId");
			ServerHttpRequest build = exchange.getRequest().mutate().header("userId", userId.toString()).build();
			exchange = exchange.mutate().request(build).build();
		} catch (Exception e) {
			// 出现异常可能是token过期或恶意攻击
			log.warn("jwt解析错误:{}", e.getMessage());
			response.setStatusCode(HttpStatus.UNAUTHORIZED);
			return response.setComplete();
		}

		return chain.filter(exchange);
	}

	@Override
	public int getOrder() {
		// 过滤器优先级,越小越先
		return -1;
	}

}

接口免鉴权操作

这里我们使用一个配置类来记录

使用字符串数组 matchers 来定义多个不需要鉴权的接口

然后对于每个请求的 url 都进行判断,是否包含该 子字符串

这样可能会造成误判,如果出现的接口情况是类似于

我想免鉴权 /user/a 这个接口,

用户请求的是 /user/a/b 这个接口。

这种情况也变成了免鉴权,但这并不是我们的本意。

所以应该完善的业务逻辑有:

截取此段,利用正则或者某种方法核对前后,是否已经是完整的接口了。


当然,这只是其中一种方案,另外这是一个 一层的 for 循环。时间复杂度 是 O(n).

在后期可以优化为 map 等数据接口来减低时间复杂度。提高鉴权效率。

鉴权后处理

因为 JWT 不光光能够用来验证,我们也需要拿到验证之后的一些结果。也就是当时在生成 JWT时存入的载荷。我们也需要将它解析后 再次存入请求头,其它的服务才能够正确地拿到对应地信息。如 userId等

优化

除去运行效率的优化,那么代码是否能够更优雅的去获取免鉴权的接口呢?

既然是配置类,如果我们是将免鉴权的接口直接 硬编码 在上面那也太 low 了。

所以,我们可以使用 SpringBoot 提供的   @ConfigurationProperties(建议) 或者 @Value 来把免鉴权的接口获取进来。减低耦合程度。

当然,这些配置我们都可以在 nacos配置中心 里进行配置,方便管理。