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;
    }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,635评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,628评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,971评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,986评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,006评论 6 394
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,784评论 1 307
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,475评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,364评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,860评论 1 317
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,008评论 3 338
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,152评论 1 351
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,829评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,490评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,035评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,156评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,428评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,127评论 2 356

推荐阅读更多精彩内容