spring boot dubbo3 学习-认证过滤器

版本
spring boot: 3.4.1
dubbo:3.3.1
java17

官方文档

基于目前开始流行“宏”服务了,去掉了API网关。直接由流量网关(Higress等)转发微服务,所以权限认证的功能就会在每个宏服务自己来做了。而个人觉得宏服务就是,一个可以“独立”提供完整功能的服务端,同时它对内又可以提供微服务接口。

dubbo3 可以直接提供 rest服务,不需要做额外处理

1,过滤器代码

package com.zx.frame.filter;

import lombok.extern.slf4j.Slf4j;
import org.apache.dubbo.common.constants.CommonConstants;
import org.apache.dubbo.common.extension.Activate;
import org.apache.dubbo.common.utils.ConfigUtils;
import org.apache.dubbo.rpc.*;

import java.lang.reflect.Method;
import java.util.List;

/**
 * dubbo 权限认证过滤器
 */
@Slf4j
@Activate(group = {CommonConstants.PROVIDER}, order = 2)
public class LoginAuthFilter  implements Filter {

    private RedisUtil redisUtil;

    private MethodAuth methodAuth;


    private RpcServiceProxy authRpc;

    /**
     * 这个filter 不受spring管理,所以需要setter方法注入
     */
    @SuppressWarnings("")
    public void setRedisUtil(RedisUtil redisUtil) {
        this.redisUtil = redisUtil;
    }

    @SuppressWarnings("")
    public void setMethodAuth(MethodAuth methodAuth) {
        this.methodAuth = methodAuth;
    }

    @SuppressWarnings("")
    public void setRpcServiceProxy(RpcServiceProxy authRpc) {
        this.authRpc = authRpc;
    }

    @Override
    public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {

        log.info("进入dubbo 权限认证过滤器 {}.{}", invoker.getInterface().getName(), invocation.getMethodName());

        // dubbo rpc token 校验
        String rpcToken = invoker.getUrl().getParameter("token");
        if (ConfigUtils.isNotEmpty(rpcToken)) {
           // 如果是 dubbo rpc 调用,不需要校验token
            return invoker.invoke(invocation);
        }


        // dubbo会把 header里的值放到 attachment里面
        String webToken = invocation.getAttachment(SystemConstant.TOKEN_KEY);
        if (StringUtils.isEmpty(webToken) || !redisUtil.hasKey(webToken)) {
            log.error("webToken 不存在");
            return AsyncRpcResult.newDefaultAsyncResult(RpcResult.error(BaseErrEnum.LOGIN_TIME_OUT.getMessage()), invocation);
        }

        // TODO 验证web token 是否过期 并取得用户信息
//        SystemAccount account = redisUtil.get(webToken);
        Integer userId = 1;

        // 取得接口权限列表
        RpcResult<List<String>> perms = authRpc.getAuthRpc().getPerms(userId, new Integer[]{MenuType.CLASS, MenuType.METHOD});


        // 接口权限校验
        Method method = DubboRpcUtils.getMethod(invocation);

        if (method != null && !methodAuth.hasMethodAuth(method, perms.getData())) {
            return AsyncRpcResult.newDefaultAsyncResult(RpcResult.error(BaseErrEnum.AUTH_INTERFACE_ERR.getMessage()), invocation);
        }

        // 向下游传递用户信息 通过 RpcContext.getServerAttachment().getAttachment(SystemConstant.USER_ID) 取得。 web token 也能取到
        // 只要是dubbo rpc 调用 不论多少层都能取到
        RpcContext.getClientAttachment().setAttachment(SystemConstant.USER_ID, userId);
        RpcContext.getClientAttachment().setAttachment(SystemConstant.USER_NAME, "username");

        // 当前服务 传递用户信息 通过 RpcContext.getServiceContext().getAttachment(SystemConstant.USER_ID)
        RpcContext.getServiceContext().setAttachment(SystemConstant.USER_ID, userId);
        RpcContext.getServiceContext().setAttachment(SystemConstant.USER_NAME, "username");

        return invoker.invoke(invocation);
    }
}

说明:

  • 1,dubbo过滤器不受spring 管理,所以无法通过 @Autowired 等注解注入。只能通过setter注入,这个dubbo生命周期做的事。
  • 2,需要区分是dubbo微服务间调用,还是其他方式调用(http调用,流量网关转发等)。我是利用了dubbo自带的鉴权token来区分的,请参考官方文档
    通过@DubboService(token = "true")方式开始token鉴权,不要配置文件里全局配置,因为我们还有面对外部的接口。
  • 3,判断token是否存在只能用[ConfigUtils.isNotEmpty]方法,是dubbo自带的。
  • 4,@Activate(group = {CommonConstants.PROVIDER}, order = 2) 这个一定要有,是激活该过滤器用的。配置文件里是加载。

2,配置文件

dubbo:
  protocol:
    name: tri
    port: 10003
  provider:
    export: true
    filter:
      - "loginAuthFilter,exceptionFilter"
      - "-exception"
    validation: true
    # 这个只能针对rpc服务开启,对于web服务不能开启 所以要写在类上@DubboSevice(token="true")
    # token: true
  # consumer 的配置都可以在 @DubboReference 里修改掉
  consumer:
    #关闭服务检查 如果依赖的服务挂了 不影响调用
    check: false
    timeout: 300
    # 负载均衡策略 默认random(加权随机) 修改成轮询
    loadbalance: roundrobin
    # 重试次数 根据服务provider端服务器数量决定 用于查询场景
    # 事务场景要设置为0
    retries: 0
    validation: true
    # 集群容错模式 立即失败 用于事务模式
    cluster: failfast

说明

  • 1,filter配置,这里一定要写 - "loginAuthFilter,exceptionFilter" 说明我加载了两个自定义的过滤器
    • "-exception" 我卸载了一个dubbo自带的过滤器
  • 2,# token: true 不要配置全局token

3,org.apache.dubbo.rpc.Filter 文件

位置

src
 |-main
    |-java
        |-com
            |-xxx
                |-XxxFilter.java (实现Filter接口)
    |-resources
        |-META-INF
            |-dubbo
                |-org.apache.dubbo.rpc.Filter (纯文本文件,内容为:xxx=com.xxx.XxxFilter)

内容

loginAuthFilter=com.zx.frame.filter.LoginAuthFilter
exceptionFilter=com.zx.frame.filter.ExceptionFilter

至此,自定义过滤器完成

4,自定义接口,方法鉴权

我没有用框架,是用自定义注解的方式完成的

4.1,自定义注解
package com.zx.common.auth;

import org.springframework.lang.NonNull;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 用于权限验证, 如果没有配置的话不认证
 * 配置在Controller 类上时 以类的code为基础认证
 * 配置在method 上时 以方法为code基础认证
 */
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface PreAuth {

    /**
     * 是否开启权限认证
     * @author xufei
     * @since 2024/7/25
     */
    boolean value() default true;

    /**
     * 权限编码
     * @author xufei
     * @since 2024/7/25
     */
    @NonNull String code();
}

4.2,用法示例

dubbo的话一定要接口定义上,不能是实现类上
spring boot 项目可以写在实现类上

@PreAuth(code="s:auth")
public interface AuthRpc {
    @PreAuth(code="s:auth:test")
    void test();
}
4.3,验证方法

因为spring boot的拦截器是可以直接拿到实际调用的方法的,而dubbo不行
所以需要一个工具类

@Slf4j
public class DubboRpcUtils extends RpcUtils {

    public static Method getMethod(Invocation invocation) {
        try {
            if (invocation != null && invocation.getInvoker() != null && invocation.getInvoker().getUrl() != null && invocation.getInvoker().getInterface() != GenericService.class && !invocation.getMethodName().startsWith("$")) {
                String service = invocation.getInvoker().getUrl().getServiceInterface();
                if (StringUtils.isNotEmpty(service)) {
                    return getMethodByService(invocation, service);
                }
            }
        } catch (Throwable var3) {
            log.error("Dubbo 接口方法取得失败", var3);
        }

        return null;
    }

    public static Method getMethodByService(Invocation invocation, String service) throws NoSuchMethodException {
        Class<?> invokerInterface = invocation.getInvoker().getInterface();
        Class<?> cls = invokerInterface != null ? ReflectUtils.forName(invokerInterface.getClassLoader(), service) : ReflectUtils.forName(service);
        Method method = cls.getMethod(invocation.getMethodName(), invocation.getParameterTypes());
        return method.getReturnType() == Void.TYPE ? null : method;
    }
}

校验类

/**
 * 接口权限校验类
 */
@Component
@Slf4j
public class MethodAuth {

    /**
     * 校验接口权限 spring boot 调用这个方法
     * 没配权限注解 认为不需要校验接口权限
     * 公共接口(AuthCode.COMMON)不需要校验权限
     * @param handlerMethod org. springframework. web. method
     * @param perms 权限编码列表
     */
    public boolean hasMethodAuth(HandlerMethod handlerMethod, List<String> perms) {
        Method method = handlerMethod.getMethod();

        PreAuth annotation = getPreAuth(method, handlerMethod);

        return checkAuth(annotation, perms);

    }

    /**
     * 校验接口权限
     * 没配权限注解 认为不需要校验接口权限
     * 公共接口(AuthCode.COMMON)不需要校验权限
     * @param perms 权限编码列表
     */
    public boolean hasMethodAuth(Method method, List<String> perms) {
        PreAuth annotation = getPreAuth(method, null);

        return checkAuth(annotation, perms);

    }

    /**
     * 取得权限注解 顺序 方法-》当前类-》方法所在类
     * spring web 调用时使用的是HandlerMethod
     * @author xufei
     * @since 2024/7/25
     */
    private static PreAuth getPreAuth(Method method, HandlerMethod handlerMethod) {
        PreAuth annotation = null;
        // 接口上有权限注解
        if (method.isAnnotationPresent(PreAuth.class)){
            annotation = method.getAnnotation(PreAuth.class);
        } else if (handlerMethod != null && handlerMethod.getBeanType().isAnnotationPresent(PreAuth.class)) {
            // 当前类上有权限注解
            annotation = handlerMethod.getBeanType().getAnnotation(PreAuth.class);
        } else if (method.getDeclaringClass().isAnnotationPresent(PreAuth.class)) {
            // 方法所在类/接口上有权限注解
            annotation = method.getDeclaringClass().getAnnotation(PreAuth.class);
        }

        return annotation;
    }

    /**
     * 查询数据库 判断该用户是否有该接口权限
     * @author xufei
     * @since 2024/7/25
     */
    private boolean checkAuth(PreAuth annotation, List<String> perms) {
        // 没配权限注解 认为不需要校验接口权限
        if (annotation == null) {
            return true;
        }

        String authCode = annotation.code();
        // 公共接口不需要校验权限
        if (StringUtils.isEmpty(authCode) || Objects.equals(AuthCode.COMMON, authCode) || !annotation.value()) {
            return true;
        }

        if (!CollectionUtils.isEmpty(perms)) {
            for(String perm : perms) {
                if (Objects.equals(perm, authCode)) {
                    return true;
                }
            }
        }

        log.info("用户没有该接口权限,接口权限编码:{}", annotation.code());

        return false;
    }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容