本文正在参加「金石计划」
系列文章目录
如果你看到了这里,那么接下来你将会认识Dubbo3的诞生将如何引领微服务领域更进一步,从而迈入云原生的领域,这当然不仅仅是Dubbo3,之前也介绍了Java生态另外一个云原生领域的技术Quarkus等技术,而本文内容侧重点去介绍Dubbo3迈向云原生 的技术分析和探索,如果有不正确的地方,还需要大家多多指正。
通过令牌进行服务验证
令牌Token的验证方式主要通过客户端和服务端的令牌验证定义并且通过注册中心层面进行维护和存储和管理的实现机制。
使用场景
在一定程度上实现客户端和服务端的可信鉴权,避免任意客户端都可以访问,降低出现安全问题的风险。
主要原理
通过令牌验证在注册中心控制权限,以决定要不要下发令牌给消费者, 可以防止消费者绕过注册中心访问提供者, 另外通过注册中心可灵活改变授权方式,而不需修改或升级提供者。如下图所示。
运作流程和实现原理
服务提供者
- 创建对应的令牌Token,通过我们定义的对应token配置,进行读取的参数信息从而进行相关的Generate Token。
注册中心
- 接收到了服务提供者传递上报过来的token值,作为该对应服务接口或者整个服务提供者的token值进行管理维护。
- 针对于相关的传递过来的token值进行校验和核对工作之后,没有问题则会进行存储到注册中心。
服务消费者
- 服务消费者可以从注册中心上面获取token令牌值
- 服务消费者会将获取到的token数据值,伴随着调用接口的同时传递给服务提供者。
服务提供者
- 服务提供者接收到了对应的服务消费者传递过来的token数据值,进行校验和核对是否属于我们认可的token值,从而实现了控制制定我们服务体系内部的消费者的调用请求。如果不一致则直接会返回失败。
配置方式(服务提供者)
配置对应的token值的范围有几种方式,我们常用的token配置主要有服务级别和接口级别等。
令牌验证,为空表示不开启,如果为true,表示随机生成动态令牌,否则使用静态令牌,令牌的作用是防止消费者绕过注册中心直接访问,保证注册中心的授权功能有效,如果使用点对点调用,需关闭令牌功能
服务级别
可以设置对应的整个服务应用级别的配置,但是优先级会被接口级别的覆盖,可以作为全局的默认值所使用。
随机token令牌,使用UUID生成
该参数接收两种类型值:boolean类型-True则生成UUID随机令牌,若为String则自定义令牌。
xml配置模式进行控制
<dubbo:provider token="true" />
SpringBoot的配置模式进行控制
使用 Spring Boot 减少非必要配置,结合 Annotation 与 application.properties/application.yml 开发 Dubbo 应用
dubbo.provider.token=true
或者
dubbo:
provider:
token: true
使用这种方式的安全级别好一些,因为每次生产的都是uuid,无规律话,不容易被第三方客户端进行破解从而进行调用。
固定token令牌,相当于密码
定义了全局provider的token数据uuid模式,对所有的接口和服务实现均起作用!定义了全局provider的token数据-123456,对所有的接口和服务实现均起作用!
xml配置模式进行控制
<dubbo:provider token="123456" />
SpringBoot的配置模式进行控制
dubbo.provider.token=123456
或者
dubbo:
provider:
token: 123456
接口类级别
随机token令牌,使用UUID生成
实现方式和效果与服务级别相同。
<dubbo:service interface="com.xxx.TestService" token="true" />
或者可以采用@DubboService注解中的token属性进行标识。
定义了该service接口的token数据,对该接口的所有方法实现均起作用!
固定token令牌,相当于密码
<dubbo:service interface="com.xxx.TestService" token="123456" />
或者可以采用@DubboService注解中的token属性进行标识。
定义了该service接口的token数据,对该接口的所有方法实现均起作用!
配置方式(服务消费者)
Dubbo官方并未直接暴漏对应的对于消费者端的DubboReference或者ReferenceConfig上配置token,但是通过源码可以知道,Dubbo采用隐式参数传递token,通过attachment进行携带进行传输。
RpcContext.getContext().setAttachment("token","123456"):
注意要点
-
token的配置也可以在,协议级别,使用的spring boot的starter配置中未找到协议级别如何配置。
-
配置Token的生产者的服务,只会允许消费者通过注册中心注册后,才可以获取到对应的token数据,再消费的数据才能够访问,否则会出现出现无效token的错误。
-
由上面的介绍,数据token是由注册中心下发拉取到的。
实际案例
建立API接口
public interface CommonRpcApi {
RpcResponse<Boolean> tokenAuth(RpcRequest<String> requestParam);
}
建立服务端的案例
@DubboService(token = "token")
public class DefaultCommonRpcApi implements CommonRpcApi {
@Override
public RpcResponse<Boolean> tokenAuth(RpcRequest<String> requestParam) {
return RpcResponse.success();
}
}
建立消费端的案例
注册中心模式进行调用
@DubboReference
CommonRpcApi commonRpcApi;
@GetMapping("/token")
public ResponseEntity<RpcResponse<String>> doTokenAuth(){
return ResponseEntity.ok(commonRpcApi.tokenAuth(new RpcRequest(RpcContext.getContext().getAttachment("token"))));
}
验证结果没有任何问题和错误异常。
直连模式进行调用
模拟非注册中心过来的外部rpc调用,用于校验token不同或者不进行token鉴权的场景!
@DubboReference(url = "dubbo://${dubbo.address:localhost}:28081")
CommonRpcApi commonRpcApi;
@GetMapping("/token")
public ResponseEntity<RpcResponse<String>> doTokenAuth(){
return ResponseEntity.ok(commonRpcApi.tokenAuth(new RpcRequest(RpcContext.getContext().getAttachment("token"))));
}
出现了报错!
org.apache.dubbo.rpc.RpcException: Invalid token! Forbid invoke remote service interface com.dubbo.shopping.api.oss.CommonRpcApi method tokenAuth() from consumer 192.168.1.104 to provider 192.168.1.104, consumer incorrect token is null
consumer incorrect token is null
出现了不一致的问题在,当存在这种场景我们很难捕捉到对应的uuid模式的token值,那么我们可以指定token值进行测试效果。
建立服务端的案例2
@DubboService(token = "123456")
public class DefaultCommonRpcApi implements CommonRpcApi {
@Override
public RpcResponse<Boolean> tokenAuth(RpcRequest<String> requestParam) {
return RpcResponse.success();
}
}
直连模式进行调用
我们手动注入token进行控制校验模式
@DubboReference(url = "dubbo://${dubbo.address:localhost}:28081")
CommonRpcApi commonRpcApi;
@GetMapping("/token")
public ResponseEntity<RpcResponse<String>> doTokenAuth(){
RpcContext.getContext().setAttachment("token","123456");
return ResponseEntity.ok(commonRpcApi.tokenAuth(new RpcRequest(RpcContext.getContext().getAttachment("token"))));
}
发现调用结果又变的正常了!
通过服务鉴权控制调用
基于上面的【通过令牌进行服务验证】的控制实现,对于安全性而言还是缺乏了机动性、可配置、灵活性等。所以接下来引入通过了【服务鉴权控制】从而增加安全性和机动性以及可配置化等功能实现。
服务鉴权-特性说明
Dubbo3服务鉴权类似支付之类的对安全性敏感的业务可能会有限制匿名调用的需求。在加固安全性方面,2.7.5引入了基于AK/SK机制的认证鉴权机制,并且引入了鉴权服务中。
鉴权服务中心
主要原理是消费端在请求需要鉴权的服务时,会通过SK、请求元数据、时间戳、参数等信息来生成对应的请求签名,通过Dubbo3的Attachment机制携带到对端进行验签,验签通过才进行业务逻辑处理。如下图所示:
总体流程图
使用场景
针对于调用方进行相关的服务调用鉴权。
使用方式
接入方式
-
使用者需要在微服务站点上填写自己的应用信息,并为该应用生成唯一的证书凭证。
-
在管理站点上提交工单,申请某个敏感业务服务的使用权限,并由对应业务管理者进行审批,审批通过之后,会生成对应的 AK/SK到鉴权服务中心。
-
导入该证书到对应的应用下,并且进行配置。配置方式也十分简单,以注解方式为例:
实现案例
定义鉴权服务中心服务
建立Dubbo3的服务容器作为鉴权服务实现
Undertow容器处理功能,用于接收对应的Http鉴权请求接口服务,用于处理来资源服务提供者端的Http请求。
数据模型
定义对应的实现鉴权中心鉴权匹配实现类
定义Hutools的数据源DB
在对应的resources下的config文件下建立db.setting文件,之后配置对应的数据源
#中括表示一个分组,其下面的所有属性归属于这个分组,在此分组名为ds1,也可以没有分组
[ds1]
#自定义数据源设置文件,这个文件会针对当前分组生效,用于给当前分组配置单独的数据库连接池参数,没有则使用全局的配置
driver = com.mysql.jdbc.Driver
#JDBC url,必须
url = jdbc:mysql://127.0.0.1:3306/dubbo-shopping
#用户名,必须
user = root
#密码,必须,如果密码为空,请填写 pass =
pass = root$
实现查询数据库的Hutool操作
List<Entity> authData = Db.use(dataSource).query("select * from auth_data");
if(CollectionUtil.isNotEmpty(authData)){
Entity entity = authData.get(0);
String ak = entity.getStr("ak");
String sk = entity.getStr("sk");
}
实现传递过来的数据库的Hutool操作
@Data
@Slf4j
@NoArgsConstructor
public class AuthService {
DataSource dataSource = DSFactory.get("ds1");
/**
* 匹配ak和sk的值
* @param appCode
* @param appKey
* @param secretKey
* @return
*/
public boolean matchSecretKey(String appCode,String appKey,String secretKey){
log.info("appCode:{} - local-appkey:{} - local-secretKey:{}",appCode,appKey,secretKey);
if(StringUtils.isEmpty(appKey)){
return Boolean.FALSE;
}
try {
List<Entity> authData = Db.use(dataSource).query("select * from auth_data where code = ?",appCode);
if(CollectionUtil.isNotEmpty(authData)){
Entity entity = authData.get(0);
String ak = entity.getStr("ak");
String sk = entity.getStr("sk");
log.info("remote-appkey:{} - remote-secretKey:{}",ak,sk);
if(ak.equals(ak)){
if(StringUtils.isEmpty(sk)){
return Boolean.FALSE;
}
else if(!SecureUtil.md5(sk).equals(secretKey)){
return Boolean.FALSE;
}
}else{
return Boolean.FALSE;
}
return Boolean.TRUE;
}
return Boolean.FALSE;
} catch (SQLException e) {
log.error("auth is error!",e);
return Boolean.FALSE;
}
}
}
鉴权服务 UndertowContainer
采用Undertow容器服务机制,在之前的章节已经介绍和说明了如何实现对应的dubbo的自定义容器实现,在这里我们使用的是UndertowContainer。如果想要学习可以关注之前的文章章节。
package com.hyts.assemble.dubbo3.comp.container;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.util.NumberUtil;
import com.hyts.assemble.dubbo3.comp.auth.AuthService;
import io.undertow.Undertow;
import io.undertow.util.Headers;
import org.apache.dubbo.common.config.ConfigurationUtils;
import org.apache.dubbo.container.Container;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.stream.Collectors;
public class UnderTowContainer implements Container {
//定义容器端口
public static final String UNDERTOW_PORT = "dubbo.undertow.port";
//定义容器contextPath
public static final String UNDERTOW_DEFAULT_PATH = "dubbo.undertow.path";
private AuthService authService = new AuthService();
//设置HttpHandler回调方法
final Undertow server = Undertow.builder()
.addHttpListener(NumberUtil.parseInt(ConfigurationUtils.getProperty(UNDERTOW_PORT)), "localhost")
.setHandler(exchange -> {
if(exchange.getRequestPath().equals(ConfigurationUtils.getProperty(UNDERTOW_DEFAULT_PATH))){
String appKey = String.valueOf(exchange.getQueryParameters().get("appKey").poll());
String secretKey = String.valueOf(exchange.getQueryParameters().get("secretKey").poll());
String appCode = String.valueOf(exchange.getQueryParameters().get("appCode").poll());
boolean result = authService.matchSecretKey(appCode,appKey,secretKey);
exchange.getResponseSender().send(String.valueOf(result));
}
}).build();
// 定义启动方法
@Override
public void start() {
server.start();
}
// 定义停止方法
@Override
public void stop() {
server.stop();
}
}
定义容器的dubbo.properties
dubbo.container=spring,jetty,log4j,undertow
dubbo.undertow.path=/auth
dubbo.undertow.port=8081
服务提供端
只需要设置 service.auth 为 true,表示该服务的调用需要鉴权认证通过。param.sign为true表示需要对参数也进行校验,之前的章节的内容我们已经介绍了对应的如何建立校验功能的实现机制控制。
// (注解方式)
@DubboService(parameters = {"service.auth","true"})
public class AuthServiceImpl implements AuthService {
}
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:dubbo="http://dubbo.apache.org/schema/dubbo"
xmlns="http://www.springframework.org/schema/beans" xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://dubbo.apache.org/schema/dubbo http://dubbo.apache.org/schema/dubbo/dubbo.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
<context:property-placeholder/>
<dubbo:application name="direct-consumer"/>
<dubbo:registry address="zookeeper://${zookeeper.address:127.0.0.1}:2181" check="false"/>
<dubbo:protocol name="dubbo" port="20880"/>
<bean id="rpcAuthSampleApi" class="com.dubbo.shopping.commodity.rpc.auth.AuthServiceImpl"/>
<dubbo:service id="rpcAuthSampleApiHandler" interface="com.dubbo.shopping.commodity.api.AuthService"
ref="rpcAuthSampleApi" version="0.0.0" filter="auth">
<dubbo:parameter key="service.auth" value="true"/>
</dubbo:service>
</beans>
服务提供端-建立服务鉴权过滤器
在之前的章节文章中介绍了对应的META-INF/dubbo下建立org.apache.dubbo.rpc.Filter文件,之后进行auth=com.dubbo.shopping.common.auth.filter.AuthFilter,之后会进行定义我们的authFilter过滤器实现类。
建立dubbo.properties配置信息读取相关的鉴权服务的地址
resources文件下建立dubbo.properties之后添加对应的内容
dubbo.auth.url=http://localhost:8081/auth
可以使用对应的配置工具进行获取配置信息。
ConfigurationUtils.getProperty("dubbo.auth.url")
定义对应的服务提供端-建立服务鉴权过滤器
@Override
public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
boolean authNeed = Boolean.valueOf(invoker.getUrl().getParameter("service.auth"));
String ak = invocation.getAttachment("AK");
String sk = invocation.getAttachment("SK");
String authUrl = ConfigurationUtils.getProperty("dubbo.auth.url");
if(authNeed){
log.info("PROCESS AUTH THE INVOKE!APPKEY {} , SECRETKEY {} : authUrl:{}",ak,sk,authUrl);
String result = HttpUtil.get(authUrl+"?appCode="+"dubbo-shopping"+"&appKey="+ak+"&secretKey="+sk);
if(!Boolean.valueOf(result)){
log.error("NOT AUTH THE INVOKE! APPKEY {} , SECRETKEY {}",ak,sk);
throw new RpcException("NOT AUTH THE INVOKE!"); }
}
log.info("PASS AUTH THE INVOKE!APPKEY {} , SECRETKEY {}",ak,sk);
// if(paramSign){
// }
return invoker.invoke(invocation);
}
服务消费端
只需要配置好对应的证书等信息即可,之后会自动地在对这些需要认证的接口发起调用前进行签名操作,通过与鉴权服务的交互,用户无需在代码中配置 AK/SK 这些敏感信息,并且在不重启应用的情况下刷新 AK/SK,达到权限动态下发的目的。
该方案目前已经提交给 Dubbo 开源社区,并且完成了基本框架的合并,除了 AK/SK 的鉴权方式之外,通过 SPI 机制支持用户可定制化的鉴权认证以及适配公司内部基础设施的密钥存储。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:dubbo="http://dubbo.apache.org/schema/dubbo"
xmlns="http://www.springframework.org/schema/beans" xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://dubbo.apache.org/schema/dubbo http://dubbo.apache.org/schema/dubbo/dubbo.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
<context:property-placeholder/>
<dubbo:application name="direct-consumer"/>
<dubbo:registry address="zookeeper://${zookeeper.address:127.0.0.1}:2181" check="false"/>
<dubbo:provider token="true"/>
<dubbo:protocol name="dubbo" port="20880"/>
<dubbo:reference id="authSampleApi" check="false" interface="com.dubbo.shopping.commodity.api.AuthSampleApi" version="*"/>
</beans>
模拟远程调用
package com.dubbo.shopping.commodity.controller;
import cn.hutool.crypto.SecureUtil;
import com.dubbo.shopping.commodity.api.AnnotationConstants;
import com.dubbo.shopping.commodity.api.AuthSampleApi;
import com.dubbo.shopping.commodity.api.CommodityQueryApi;
import com.dubbo.shopping.commodity.entity.BaseInfo;
import com.dubbo.shopping.commodity.model.CommodityQueryDTO;
import com.dubbo.shopping.model.rpc.RpcRequest;
import io.swagger.annotations.ApiOperation;
import org.apache.dubbo.config.annotation.DubboReference;
import org.apache.dubbo.rpc.RpcContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
/**
* <p>
* 商品主题信息 前端控制器
* </p>
*
* @author libo
* @since 2022-05-08
*/
@RestController
@RequestMapping("/commodity/base-info")
public class BaseInfoController {
@Autowired
AuthSampleApi authSampleApi;
@ApiOperation("权限控制调用")
@RequestMapping("/auth")
public ResponseEntity auth(){
RpcContext.getClientAttachment().setAttachment("AK","dubbo3");
RpcContext.getClientAttachment().setAttachment("SK", SecureUtil.md5("123456"));
return ResponseEntity.ok(authSampleApi.executeAuth("test parameter"));
}
}
既可以实现鉴权服务机制,大家还可以自己进行扩展实现。
本文正在参加「金石计划」