版本
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;
}
}