目录
1. 介绍
2. Nacos SPI 鉴权机制
3. 后台管理 / HTTP 接口鉴权
4. 客户端 / GRPC 接口鉴权
1. 介绍
鉴权功能默认没有开启,开启后的效果就是 Nacos 的接口需要用户登录并且具有权限才能调用该接口。例如注册实例、发布配置等。
鉴权也就是 我是谁、我能干什么
2. Nacos SPI 鉴权机制
默认未开启鉴权,需要启用。
Nacos 鉴权系统使用 SPI 机制实现,这意味着用户可以自己实现扩展一个鉴权系统。Nacos 默认也提供了两个默认实现。
鉴权 SPI 接口是 com.alibaba.nacos.plugin.auth.spi.server.AuthPluginService,所有鉴权类都实现此接口。
注释里对该接口的方法做了解释说明
下来看看默认实现 NacosAuthPluginService 的鉴权源码
com.alibaba.nacos.plugin.auth.impl.NacosAuthPluginService
以下只看这两个关键方法
validateIdentity 验证身份
下面只看关键代码:用户登录,用户登录的过程就是拿到token,然后校验token,校验通过就从token中获取到用户
validateIdentity 验证权限
3. 后台管理 / HTTP 接口鉴权
首先使用 @Secured 定义了受保护资源,然后使用 com.alibaba.nacos.core.auth.AuthFilter 进行解析处理
AuthFilter 是一个 Servlet 的 Filter,拦截了全部请求
接下来看看 AuthFilter 的 filter 如何实现
// com.alibaba.nacos.core.auth.AuthFilter
public class AuthFilter implements Filter {
private final AuthConfigs authConfigs;
private final ControllerMethodsCache methodsCache;
private final HttpProtocolAuthService protocolAuthService;
public AuthFilter(AuthConfigs authConfigs, ControllerMethodsCache methodsCache) {
this.authConfigs = authConfigs;
this.methodsCache = methodsCache;
this.protocolAuthService = new HttpProtocolAuthService(authConfigs);
this.protocolAuthService.initialize();
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
if (!authConfigs.isAuthEnabled()) {
// 没启用鉴权,本次过滤器直接通过
chain.doFilter(request, response);
return;
}
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse resp = (HttpServletResponse) response;
// 省略部分代码
try {
// 获取到当前请求的接口方法
Method method = methodsCache.getMethod(req);
if (method == null) {
// 方法不存在,说明请求的是静态资源(.css,.png等),本次过滤器直接通过
chain.doFilter(request, response);
return;
}
if (method.isAnnotationPresent(Secured.class) && authConfigs.isAuthEnabled()) {
// 如果当前方法上放了注解 Secured 并且 启用了鉴权功能(上面的application.properties改true的那个配置项) 就进来
// 获取到 注解 Secured
Secured secured = method.getAnnotation(Secured.class);
if (!protocolAuthService.enableAuth(secured)) {
// 默认配置永远启用 auth,所以默认配置这里进不来
chain.doFilter(request, response);
return;
}
// 解析请求中的资源(从请求中到namespaceId、group、name等信息构建成一个 Resource 对象)
Resource resource = protocolAuthService.parseResource(req, secured);
// 从请求中获取到需要的 身份相关字段与值 (Authorization、accessToken、username、password)
IdentityContext identityContext = protocolAuthService.parseIdentity(req);
// 验证身份
boolean result = protocolAuthService.validateIdentity(identityContext, resource);
if (!result) {
// 验证失败抛出异常
throw new AccessException("Validate Identity failed.");
}
injectIdentityId(req, identityContext);
String action = secured.action().toString();
// 验证权限
result = protocolAuthService.validateAuthority(identityContext, new Permission(resource, action));
if (!result) {
// 验证失败抛出异常
throw new AccessException("Validate Authority failed.");
}
}
chain.doFilter(request, response);
} catch (AccessException e) {
// 省略错误处理
}
}
}
整体主流程概括下就是:获取到请求的数据、验证身份、验证权限。
鉴权功能的实现就是 验证身份、验证权限 这两个功能的实现
- 验证身份 protocolAuthService.validateIdentity()
- 验证权限 protocolAuthService.validateAuthority()
看看这两个方法如何实现
// com.alibaba.nacos.auth.AbstractProtocolAuthService
public abstract class AbstractProtocolAuthService<R> implements ProtocolAuthService<R> {
@Override
public boolean validateIdentity(IdentityContext identityContext, Resource resource) throws AccessException {
Optional<AuthPluginService> authPluginService = AuthPluginManager.getInstance()
.findAuthServiceSpiImpl(authConfigs.getNacosAuthSystemType());
if (authPluginService.isPresent()) {
return authPluginService.get().validateIdentity(identityContext, resource);
}
return true;
}
@Override
public boolean validateAuthority(IdentityContext identityContext, Permission permission) throws AccessException {
Optional<AuthPluginService> authPluginService = AuthPluginManager.getInstance()
.findAuthServiceSpiImpl(authConfigs.getNacosAuthSystemType());
if (authPluginService.isPresent()) {
return authPluginService.get().validateAuthority(identityContext, permission);
}
return true;
}
}
解释一下
Optional<AuthPluginService> authPluginService = AuthPluginManager.getInstance()
.findAuthServiceSpiImpl(authConfigs.getNacosAuthSystemType());
authConfigs.getNacosAuthSystemType() 为获取到当前系统配置的 授权类型
因为鉴权系统是 SPI 机制的,所以意味着用户可以自己扩展,而默认是 Nacos 默认的实现,可以通过配置文件更改为自己实现的。
此时拿到了配置的授权类型 字符串 nacos
然后需要找到这个 spi 实现类
可以发现 SPI 接口 AuthPluginService 实现类 Nacos 实现了 2 个
LdapAuthPluginService、NacosAuthPluginService(默认)
就是根据这里的 authServiceName 找到了这个鉴权实现类
找到该 SPI 鉴权类后,就调用该类的方法来完成鉴权逻辑了。
4. 客户端 / GRPC 接口鉴权
客户端需要配置账号才能有权限注册服务,否则报错403 无权限
SpringCloud nacos 的配置
请求 GRPC 接口需要 传入 accessToken,accessToken 从哪里来?就是下面这段代码:
在创建 NamingFactory.createNamingService(Properties properties) 里初始化过程中,最终调用了以下 initSecurityProxy 方法
该方法内容:
- 创建一个可调度的1个线程的线程池
- 根据配置的 username password 请求 nacos server 做一次登录
- 可调度线程池 每5秒 定时调度任务 登录一次,也就是每隔 5 秒就请求登录接口。
登录方法做的就是 将 accessToken 存起来
由于 Nacos Client 2.x 版本之后使用的是 GRPC 请求了,所以不走 HTTP 了,那么鉴权如何实现呢
答案就是 过滤器,没错还是过滤器,不过不是 Servlet 的AuthFilter了,而是专门给 GRPC 做的过滤器。
RemoteRequestAuthFilter
GRPC 过滤器就是在每次接收到 GRPC 请求之后,先调用过滤器来实现的。
观察代码,发现 filter 里面的代码也是判断了 Secured 注解,判断逻辑和之前的一样
观察注册实例 GRPC 接口,也有 Secured 注解
此次就流程就全部走完了。