1. IAuth 简介
1.1 基本概念
IAuth 是一个完善的基于接口/方法的权限检验工具。
相关概念
-
Client 端:请求方
- AppId: 你的 Client 在 IAuth 系统上的唯一标识。独一无二。
- AppKey: 你的 Client 的身份密钥,这东西用来向 IAuth 证明是你,而非他人冒充。
-
Service 端:被请求方,提供接口服务方
- ServiceID: 你的服务在 IAuth 系统上的唯一标识。独一无二的。
- ServerKey: 你的服务的身份密钥,用来向 IAuth 证明是你,而非他人冒充。
- Scopes: 如果你的服务对外不仅仅提供一个接口,如果你想具有更细粒度的保护接口,scope 就用到了。例如 Controller 中的每个 GET、POST 都可以作为一个 scope。
-
token 是服务调用方从 IAuth 服务拿到的加密后的凭证,服务调用方在调用服务时,必须提供 token(为满足无灰度升级需求,Service 端可以通过 iauth.sdk.service.allowNoToken=true 开启无 token 模式,相当于没有使用 IAuth)。
-
serviceKey(tokenKey) 是 Service 端向 IAuth 服务获取的 key ,用来解密token。只有能解开token, token 才是有效的。解开 token 以后,还需要验证 scopeList 等信息。
1.2 IAuth 访问流程
流程
-
serviceA 发起对 serviceB 的服务接口访问之前, 向 iAuth 申请对应的 token。 serviceA 将自身 appId、serviceB 的资源代码列表(scopeList)、serviceB 的识别 ID (sid) 提交给 iAuth。
-
iAuth 对收到的 token 请求进行验证,验证通过后取出 DB 中的对称加密秘钥 serviceKey,用 serviceKey 将请求的 appId、scopeList、sid 和当前的时间戳 time 进行 AES 加密,生成 token。
-
iAuth 返回 token 给 serviceA。
-
serviceA 拿到 token 后,对 serviceB 的服务接口访问之时, 附加自身 ID(appId) 和对应的 token 到请求参数中。
-
serviceB 向 iAuth 请求自己的对称加密秘钥 serviceKey。
-
iAuth 鉴定是合法的请求后,将秘钥 serviceKey 返给 serviceB。
-
serviceB 用拿到的秘钥 serviceKey 解开 token,解开后验证 token 是否超时(超时时间由 serviceB 设定)。
-
serviceB 将 scopeList 代表的资源返给 serviceA。
2. web 界面的使用
3. Http 协议接入(SpringBoot 服务)
3.1 Service 端
3.1.1 配置项目依赖
<dependency>
<groupId>com.xiaomi</groupId>
<artifactId>xiaomi-iauth-java-sdk</artifactId>
<version>{lastest version}</version>
</dependency>
3.1.2 配置拦截器
@Configuration
public class IAuthConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 对来自/demo/** 这个链接来的请求进行拦截
registry.addInterceptor(new IAuthSpringInterceptor()).addPathPatterns("/demo/**");
}
}
3.1.3 添加配置文件
zookeeper.properties
zookeeper.host=STAGING
iauth.properties
iauth.sdk.service.mode=true
iauth.sdk.service.name=server2001
iauth.sdk.service.serverKey=dvi2vfPOUn2nm4Y88Go7pg==
iauth.sdk.service.signVersion=2
新接入的 Service,signVersion 必须为 2
3.1.4 添加注解
如:@IAuth(scopeList = “3001 3002”)
@RestController
@EnableAutoConfiguration
public class DemoController {
private static final Logger LOGGER = LoggerFactory.getLogger(DemoController.class);
@IAuth(scopeList = "3001 3002")
@RequestMapping(value = "/demo/list", method = RequestMethod.GET)
public String getList() {
LOGGER.debug("invoke list success!");
JSONObject result = new JSONObject();
result.put("result", "ok");
result.put("message", "you successfully accessed docker_test");
return "@json:" + result.toString();
}
}
IAuth 首先通过拦截器(SpringMvc 框架机制)拦截 url, 然后判断 url 对应的方法是否有 @IAuth 注解实现方法拦截。
思考:为什么即需要拦截器又需要注解,不能通过 AOP 实现对带注解的方法拦截吗?(用户就可以不用配置拦截器了)
IAuth 并非专门为 spring/springBoot 服务,查看源码发现,IAuth sdk 只依赖了 springMvc jar 包,定义了一个继承了 HandlerInterceptorAdapter 类的拦截器,需要业务方手动将此拦截器加入 ioc 容器,Iauth 并不依赖 spring 框架,不支持 aop,也可以说官方并未实现通过 aop 对方法进行拦截。
3.2 Client 端
3.2.1 配置项目依赖
<dependency>
<groupId>com.xiaomi</groupId>
<artifactId>xiaomi-iauth-java-sdk</artifactId>
<version>{lastest version}</version>
</dependency>
<dependency>
<groupId>com.xiaomi</groupId>
<artifactId>xiaomi-common-zookeeper</artifactId>
<version>3.0.6</version>
</dependency>
3.2.2 添加配置文件
zookeeper.properties
zookeeper.host=staging
iauth.properties
iauth.sdk.app.mode=true
iauth.sdk.app.appId=test
iauth.sdk.app.appKey=test
#请求的服务的serviceId
iauth.sdk.app.serviceId=test
#请求的服务的scope
iauth.sdk.app.scope=3001 3002
#可以配置多个请求的service,每一个service都有对应的serviceID和对应的scope。
#iauth.sdk.app.serviceId.1=test_2
#iauth.sdk.app.scope.1=2003
#iauth.sdk.app.serviceId.2=test_3
#iauth.sdk.app.scope.2=2004
#这里有一个选填的项signVersion,没有设置,默认为1,signVersion =2 主要提升了签名算法的安全性。推荐设置为2
iauth.sdk.app.signVersion=2
3.2.3 编写代码
package com.xiaomi.iauth.demo.client;
import com.xiaomi.iauth.java.sdk.app.IAuthAppSDKTool;
import com.xiaomi.iauth.java.sdk.common.IAuthTokenInfo;
import com.xiaomi.iauth.java.sdk.constants.IAuthConstants;
import com.xiaomi.iauth.java.sdk.utils.HttpUtil;
import org.apache.http.client.utils.URIBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class ClientDemo {
private static final Logger LOGGER = LoggerFactory.getLogger(ClientDemo.class);
private static final String REMOTE_SERVER = "http://你要访问的服务地址:端口";
private static final String SERVICE_ID = 你要访问的SERVICE_ID;
public static void main(String[] args) {
String tokenStr;
// 1. 从IAuth获取Token
try {
IAuthTokenInfo info = IAuthAppSDKTool.getInstance().getIAuthToken(SERVICE_ID, false);
tokenStr = info.getToken();
} catch (Exception e) {
LOGGER.error("get token from iauth error. configuration is not correctly set", e);
return;
}
// 2. 请求原本的Service资源,如passportWithIAuth/micloud等等
try {
URIBuilder uriBuilder = new URIBuilder(REMOTE_SERVER);
uriBuilder.setPath("/demo/list");
uriBuilder.setParameter(IAuthConstants.APP_ID, "你自己的appId");
// token 中有特殊字符,如果自行拼接URL,注意urlencode
uriBuilder.setParameter(IAuthConstants.TOKEN, tokenStr);
// 生产环境中,不建议使用iauth携带的HttpUtil工具类,本工具类仅满足iauth自身调用需求。
String res = HttpUtil.readHttpResponse(HttpUtil.doGet(uriBuilder.build().toASCIIString()));
LOGGER.info("get response from service is [{}]", res);
} catch (Exception e) {
LOGGER.error("get resource from demo-server error.", e);
return;
}
LOGGER.info("--- end to visit {}", SERVICE_ID);
}
}
获取 token 需要传入要访问的 sid。
访问 service 端接口时,需要携带 token 和 appId。
IAuthAppSDKTool.getInstance().getIAuthToken(SERVICE_ID, false); 第二个参数代表是否强制刷新 token, 自 1.5.0 之后,Iauth sdk 增加了缓存机制,此参数已不再使用,无需关注。
4. 配置方式
4.1 获取 IAuth 主服务地址
4.1.1 通过 zk 获取
如上所述,通过配置 zookeeper.properties,IAuth SDK 从 ZK 上取出配置的 IAuth 主服务地址,用于 Client/Server 端访问。
4.1.2 不依赖 zk
需要 sdk version >= 2.5.9,通过变量去获取域名,域名写死在代码中
可以配置在 System Property 或者 iauth 配置文件中
读取变量优先级: System Property > iauth 配置文件 > zk
Client 端
iauth.sdk.app.env=staging
Service 端
iauth.sdk.service.env=staging
4.2 使用代码方式配置 IAuth
代码配置的优先级高于配置文件的配置,会将配置文件的配置覆盖掉
4.2.1 Client 端
4.2.1.1 传递 appInfo
在配置文件中, 随便填一个 appId 和 appKey,先让加载器进行加载。然后在代码中传递 appInfo 进行替换(仅会替换重复项)。
// 设置 APP相关信息
AppInfo appInfo = new AppInfo();
appInfo.setAppId(2882303761517122425L);
appInfo.setAppSecret("v4uprbwig/TWaQ0diQIL1g==");
// 补充请求相关信息, 不推荐这样使用
// List<Integer> scopeList = new ArrayList<Integer>();
// scopeList.add(3001);
// scopeList.add(3002);
// RequestServiceInfoHolder.put("demo_server_test", scopeList);
// 从IAuth获取Token
IAuthTokenInfo info = IAuthAppSDKTool.getInstance(appInfo).getIAuthToken(SERVICE_ID, false);
4.2.1.2 通过反射
首先配置 iauth.properties
iauth.sdk.app.mode=true
iauth.sdk.app.serviceId=demo_server_test
iauth.sdk.app.scope=3001 3002
# 选填的出场了
iauth.sdk.app.load.appInfo.class=com.xiaomi.iauth.sdk.util.TestAppLoader
然后实现加载类
package com.xiaomi.iauth.sdk.util;
import com.xiaomi.iauth.java.sdk.common.AppInfo;
import com.xiaomi.iauth.java.sdk.utils.AppLoaderInterface;
public class TestAppLoader implements AppLoaderInterface {
public AppInfo getAppInfo() {
AppInfo appInfo = new AppInfo();
appInfo.setAppId(2882303761517122425L);
appInfo.setAppSecret("v4uprbwig/TWaQ0diQIL1g==");
return appInfo;
}
}
最后获取 token
// 从IAuth获取Token
IAuthTokenInfo info = IAuthAppSDKTool.getInstance().getIAuthToken(SERVICE_ID, false);
4.2.2 Service 端
首先配置 iauth.properties
iauth.sdk.service.mode=true
iauth.sdk.service.signVersion=2
iauth.sdk.service.isAuthIp=true
# 自定义
iauth.sdk.service.load.serverInfo.class=com.xiaomi.iauth.sdk.util.TestServiceLoader
实现 ServerInfoLoaderInterface
import com.xiaomi.iauth.java.sdk.common.ServerInfo;
import com.xiaomi.iauth.java.sdk.utils.ServerInfoLoaderInterface;
public class TestServiceLoader implements ServerInfoLoaderInterface {
@Override
public ServerInfo getServerInfo() {
ServerInfo serverInfo = new ServerInfo();
serverInfo.setServerName("test");
serverInfo.setServerSecret("MyRi1qiEKQ3aB8wyg5bxPA==");
return serverInfo;
}
}
5. url 签名机制
IAuth 支持对 url 进行签名,防止用户对 url 参数篡改。
Client 端添加 sign
URIBuilder uriBuilder = new URIBuilder(host);
uriBuilder.setPath(path);
//get请求如果需要添加其他参数在这里添加
uriBuilder.setParameter("example1","001");
uriBuilder.setParameter("example2","002");
uriBuilder.setParameter("example3","003");
uriBuilder.setParameter(IAuthConstants.APP_ID, APP_IDD);
uriBuilder.setParameter(IAuthConstants.TOKEN, info.getToken());
//如果需要使用url签名,最后添加url签名,否则无效,操作方式如下:
uriBuilder.setParameter(IAuthConstants.IAUTH_URL_SIGN, UrlSignature.genUrlSign(uriBuilder.toString(), info.getSsecurity()));
//然后发送相应请求
Service 端添加配置
# 可选项,expandUrlVerity, true 代表使用url验证,只允许带url签名的请求通过。
iauth.sdk.service.expandUrlVerity=true
签名的计算方式:将 url 参数和 ssecurity 格式化为 “%s=%s”,用 “&” 拼接后,计算 sha1
源码: com.xiaomi.iauth.java.sdk.security.UrlSignature.genUrlSign(String url, String ssecurity)
6. 访问流程详解
6.1 Client 请求 token
Client 请求 token 会携带以下参数:
-
sid
-
appId
-
scope
-
nonce
-
sign
Nonce 的生成原理
Nonce 长度为 base64(96 bit)=base64(12byte)=16
public static String generateNonce() {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(bos);
try {
dos.writeLong(random.nextLong()); // 写入64位随机数
dos.writeInt((int) (System.currentTimeMillis() / (1000 * 60))); // 写入32位的分钟数
dos.flush();
} catch (IOException e) {
logger.error("never happend", e);
}
return Coder.encryptBASE64(bos.toByteArray()); // Base64(UTF-8)编码
}
sign 的生成方式有两种(v1 和 v2),通过 iauth.sdk.app.signVersion 控制使用哪种算法
#这里有一个选填的项signVersion,没有设置,默认为1,signVersion =2 主要提升了签名算法的安全性。推荐设置为2
iauth.sdk.app.signVersion=2
6.1.1 sha1 和 HmacSHA1 的区别
-
sha1 是一种摘要算法,比 md5 更加安全,同样的 content 计算出的结果相同。
-
HmacSHA1 和 sha1 不同,HMAC 计算哈希时需要一个密钥,不同密钥算出的结果不同。
6.1.2 sign v1
对应代码 SignatureCoder.genServiceSignature(String method, String uriPath, List
获取 token 的地址:/token/getToken
Sign 的生成流程:
-
新建 List 加入method=GET,uri=/token/getToken,param(排序),以及 appKey。最终采用 & 将各个 String 连接形成最初的 data。
-
对 data 数据进行 SHA1 加密和 Base64 编码。
6.1.3 sign v2
对应代码:SignUtil.genSignature(String method, String uriPath, List
获取 token 的地址:/token/V2/getToken
Sign 的生成流程:
-
生成 data 的数据与 V1 类似,不同点是 data 并没有将 appKey 集成。
-
对 data 数据进行 HmacSHA1 加密(appKey 作为密钥)和 Base64 编码。
6.1.4 总结
通过以上介绍,我们可以看出 sign v1 和 sign v2 的区别:
-
v1 将 appKey 加入到了 data 中进行传输,有一定泄露风险。v2 仅 Client 和 Service 端持有 appKey, 不进行传输,降低了被中间人破解的风险。
-
v1 使用 sha1 进行加密;v2 使用 HmacSHA1 算法,appKey 作为加密秘钥。
6.2 IAuth 发放 token
6.2.1 IAuth 验证 Client 身份
-
IAuth 验证该 {sid, scopes, appId} 是否存在授权关系。(MySQL中)
-
验证 nonce 是否过期
-
IAuth 根据 appId 从 DB 中取出该 AppKey,并用之验证签名 sign。
6.2.2 IAuth 下发 Token
Token 的组成
token = version + sid + content + sign
content = encrypt({versionKey:1.0,securityKey:randomAESKey,time:currentTimeMillis,appIdKey:appId,scopeKey:scopes})
sign = base64(SHA(content))
content 使用 aes 加密,密钥为 tokenKey(上文提到的 serviceKey, 保存在 IAuth 的数据库中)
使用 sign 更加安全,但好像没啥必要?
randomAESKey(128bit) 长度为 base64(byte[16])=24, 生成代码如下:
public static String generateRandomAESKey() {
Random random = new SecureRandom();
byte[] bytes = new byte[16];
random.nextBytes(bytes);
String aesKey = Coder.encryptBASE64(bytes);
Arrays.fill(bytes, (byte) 0);
return aesKey;
}
下发 token 步骤
-
获取 tokenKey(DB中)。
-
随机生成 ssecurity(securityKey),并生成 Token。ssecurity 在对 url 进行签名的时候会使用到。
-
使用 tokenKey 加密 Token。
-
将加密后的 token 和 ssecurity 打入到返回结果中。
6.3 Client 端访问 Service 端
Client 端发送请求
-
Client 端从 IAuth 服务端获取 token(根据 sid)
-
访问 Service 端(携带 appId, token)
Service 端收到请求
-
拦截器根据 url 拦截请求
-
判断访问的接口/方法是否有 @IAuth 注解,有注解获取注解的 scope,没有则放行。
-
获取 tokenKey 解密 token
-
验证 token 是否合法
- 对 token 中的 content 进行 sign 验签
- 是否过期
- 请求参数 appId 和 token 中 appId 是否一致
- Token 中携带的 scopes 和接口所需 scope 是否有交集
-
若需校验 url sign, 则进行 url sign 校验
6.4 Service 获取 TokenKey
-
Service 端获取 TokenKey 的起点为拦截器 IAuthSpringInterceptor.
-
Service 端请求 tokenKey 时也有两种签名方式,通过 iauth.sdk.service.signVersion 控制
6.4.1 sign v1
新接入的 Service,signVersion 必须为 2, v1 已经不再使用。
使用 v1 需要配置 iauth.sdk.service.public.key.path 公钥文件路径,公钥文件由 IAuth 发放。
Service 端发送请求时
向 IAuth 发送请求时携带
-
Sid
-
key_data
key_data = base64(encrypt({nonce:nonce, key: randomAesKey, serverSDKVersion:2}, publicKey))
encrypt 使用公钥文件中声明的非对称加密算法
IAuth 服务端收到请求后
-
用私钥解出 KeyData(包含 nonce, aesKey)
-
验证 Nonce (nonce 有效期 5min)
-
根据 Sid 从 DB 中取出 tokenKey
-
根据 KeyData 中的 AesKey 加密 TokenKey
-
生成 Sign (sign = base64(sah1(serviceKey=${encryptedTokenKey}&aesKey)))
-
将 encryptTokenKey 和 Sign 传回。
Service 端收到响应后
-
取出 encryptedTokenKey(aes 加密后的)
-
取出 sign, 验证签名
-
用 aesKey 解密 encryptedTokenKey 获取 tokenKey
6.4.2 sign v2
Service 端发送请求时
向 IAuth 发送请求时携带
-
sid
-
key= new AES(serviceSecret).encrypt(randomAesKey)
-
nonce
-
Sign = HmacSha1(上述参数按一定规则拼接后的字符串,serviceSecret)
IAuth 服务端收到请求后
-
验证 Nonce (nonce 有效期 5min)
-
根据 sid 取出 serviceSecret
-
验证 sign 是否合法
-
从 DB 中取出 tokenKey
-
根据 serviceSecret 对客户端传递过来的 key(aesKey) 进行解密
-
根据解密后的 AesKey 加密 TokenKey
-
生成 sign(sign = base64(sah1(serviceKey=${encryptedTokenKey}&serviceSecret)))
-
返回 encryptedTokenKey 和 Sign。
Service 端收到响应后
-
取出 encryptedTokenKey(aes 加密后的)
-
取出 sign, 验证签名
-
用 aesKey 解密 encryptedTokenKey 获取 tokenKey
6.4.3 总结
Client 的 sign v1/v2 和 Service 端的 sign v1/v2 有什么关系
两者之间没有任何关联。
Client sign 指的是 Client 端访问 IAuth 服务器时使用的签名方式。
Service sign 指的是 Service 端访问 IAuth 服务器时使用的签名方式。
Client 端和 Service 端的签名版本不一致对服务没有任何影响。
Client 和 Service 访问 IAuth 服务器对比
-
IAuth 返回给 Client 端 token 后,没有 sign 校验机制,返回后可以直接使用,如果被中间人篡改,则 token 无效,访问接口失败。
-
IAuth 返回给 Service 端的 tokenKey 使用了 aes 加密,对于 IAuth 服务器的响应结果,Service 端需要进行 sign 验证。
Service 端 sign v1 和 sign v2 对比
-
v1 使用非对称加密算法,IAuth 服务端保存私钥,Service 端保存公钥。
-
v2 删除了非对称加密算法,Service 端和 IAuth 保存 serviceSecret,使用了签名验证的方式。
-
v1 方式 IAuth 响应体生成的 sign 使用了 aesKey, 这个 aesKey 是 Service 端发送请求时传给 IAuth 服务器的,假如中间人拦截了请求,就可以根据 aesKey 伪造响应,有安全风险。
-
v2 方式 IAuth 响应体生成的 sign 使用了 serviceSecret,仅保存在 Service 端和 IAuth 服务器端,没有传输过程中被泄露的风险。
7. IAuth key 更新机制
7.1 缓存机制
IAuth token
接入 IAuth 后,原来 serviceA 到 serviceB 的访问由经过 1 次网络,变成了经过 3 次网络。会引发以下问题:
-
出现访问故障的风险增大。
-
访问延迟变大。
-
耗费内网带宽。
于是引入缓存机制,来解决上述三个问题。
加入缓存机制后,tokenKey 的更新是保证 IAuth 安全的重要机制。
7.2 实现方案
-
serviceB 持有一个 Key pair,持有 old tokenKey 和 new tokenKey,缓存在内存中。
-
IAuth 持有一个 tokenKey List,保存在 DB 中,对应 key_sequence 表,持有 old tokenKey 和 new tokenKey, 当访问自己的 serviceB 集群全部获取过 new tokenKey 后,延迟一段时间后,将 old tokenKey 标记为失效。
-
serviceA 去 IAuth 获取 token 时,IAuth 总是用有效的 tokenKey List 中次最新的 tokenKey 去加密 token,当 tokenKey List 中只有一个 tokenKey 时,就用唯一的这个 tokenKey 加密。
-
serviceB 去 IAuth 获取 tokenKey 时,IAuth 总是返回最新的两个 tokenKey ,如果有效的 tokenKey List 长度为 1 时,需要根据数据库中最后一个标记为无效的 tokenKey Invalid 来判断:
- Invalid tokenKey 为空,或者当前时间离 Invalid tokenKey 标记的时间的间隔大于 T(IAuth),则返回这个 tokenKey 和一个假的 tokenKey(解任何token都会失败)。
- 否则返回 Invalid tokenKey 和 数据库中唯一有效的 tokenKey 。
-
serviceA 访问 serviceB 如果不能用 old tokenKey 解开,则用 new tokenKey 去解,再解不开则返回错误。
-
serviceA、 serviceB、 IAuth 的更新周期满足 T(ServiceA) <= T(ServiceB) <=T(IAuth)
-
T(IAuth) 指的是轮询周期(定时任务周期),不是 tokenKey 的更新周期。
7.3 时间轴
时间点说明:
-
E0 (T0 < E0 < T1) 时刻更新 tokenKey。更新的是 DB 中 service_conf 表里的 tokenKey 字段,但此时 tokenKey 并未更新到 key_sequence 表中,而 key_sequence 表才是 IAuth server 获取 tokenKey 的源。
-
T1 时 IAuth 的定时任务发起一次访问,检测到 service_conf 表中 serviceKey 已经更新,将新的 serviceKey 插入到 key_sequence 表,这时 IAuth 中有效的 Key 为 old tokenKey 和 new tokenKey。
-
S1 (T1 < S1 < T2) 时刻 Service B 从 IAuth Center 获取到的 Key 列表为 new tokenKey 和 old tokenKey。从 S1 时刻开始,Service B 缓存变为 new tokenKey 和 old tokenKey。
-
T2 时 IAuth 的定时任务再次发起一次访问,此时调用此 tokenKey 的 serviceB 均已获得最新的 tokenKey,因为 T(ServiceB) <= T(ServiceA)。在此次访问结束时 IAuth 会将 service_conf 表中的 old tokenKey 标记为无效,从 T2 时刻开始, IAuth Center 只有一个有效的有效的 new tokenKey。
-
P0 (T2 < P0 < T3) 时 ServiceA 从 IAuth Center 获取的 token 是 new tokenKey 加密的token。
-
T3 时所有的 serviceA 均已经拿到用 new tokenKey 生成的 token(不考虑网络耗时,内网通常在毫秒级)。
-
S2 (T3 < S2 < T4)时, ServiceB 从 IAuth Center 获取到的 Key 列表为 INVALID tokenKey 和 new tokenKey。
根据时间点,IAuth 标记 old keySequence 的时间在 KeySequence List 更新一个周期之后,IAuth 的最小更新周期为 4T(IAuth)。
8. 项目接入 Thrift
8.1 xiaomi-common-thrift
xiaomi-common-thrift 是对 apache libthrift 的封装,屏蔽了实现细节,提供统一接口,有以下功能。
-
从 zk 获取配置信息, zk 注册
-
server 分组
-
路由
-
连接池
-
monitor 监控
-
PerfCounter 统计
所使用的协议及服务模型:
-
通信协议:TBinaryProtocol,一种简单的二进制格式,简单,但没有为空间效率而优化。比文本协议处理起来更快。
-
传输协议:TFramedTransport,非阻塞服务器所使用的传输协议,按帧来发送数据。
-
服务模型:THsHaCompleteServer,一个由小米实现的非阻塞、半同步半异步的服务模型。
8.1.1 所需依赖
<!-- thrift -->
<dependency>
<groupId>com.xiaomi</groupId>
<artifactId>xiaomi-common-thrift</artifactId>
<version>2.8.22</version>
</dependency>
<dependency>
<groupId>org.apache.thrift</groupId>
<artifactId>thrift</artifactId>
<version>0.5.0-mdf2.0.9</version>
</dependency>
<!-- common zk -->
<dependency>
<groupId>com.xiaomi</groupId>
<artifactId>xiaomi-common-zookeeper</artifactId>
<version>3.0.6</version>
</dependency>
8.1.2 Service 端
8.1.2.1 编写 IDL
namespace java com.xiaomi.mict.thrift
service Hello{
string helloString(1:string param)
}
8.1.2.2 实现接口
public class HelloService extends MiliaoSharedServiceBase implements Hello.Iface {
@Override
public String helloString(String param) {
return "hello " + param;
}
@Override
public void stop() {
}
}
8.1.2.3 启动 Service 服务
// 端口属性名
String PORT = "port";
// 线程池大小属性名
String THREAD_POOL_SIZE = "threadpoolsize";
// thrift service config
ThriftServiceStartupConfig config = new ThriftServiceStartupConfig(null, null, false, true, false);
// local settings
System.setProperty(PORT, "9002");
System.setProperty(THREAD_POOL_SIZE, "2");
// start thrift server
ThriftServiceRunner.startThriftServer(config, HelloService.class, PORT, THREAD_POOL_SIZE, null);
ThriftServiceStartupConfig(String configName, String localIpAddr, boolean listConfigs, boolean registerZkNode, boolean loadConfiguration) 参数介绍
-
configName:从 zk 拉取配置时的配置文件名字。
-
localIpAddr:ip 地址,为 null 自动获取。
-
listConfigs:是否打印 zk 配置信息。
-
registerZkNode:服务启动后自动向 zk 注册信息。
-
loadConfiguration:是否从 zk 拉取配置信息,会覆盖本地配置。
服务启动后,若开启 registerZkNode 会自动向 ZK 注册节点信息
NodePath: /services/com.xiaomi.mict.thrift.Hello/Pool/10.221.136.111:9002
内容
#Generated by ZKClient -- PropertiesSerializer
#Tue Mar 14 18:20:54 CST 2023
port=9002
implementation=com.xiaomi.mict.thrift.HelloService
client.service.level=10
weight=10
thrift.partition.group=default
start_time=1678789254158
server.service.level=10
version=1
thrift.runner.zookeeper.config=
host=10.221.136.111
8.1.2.4 拉取 ZK 配置
我们可以通过开启 loadConfiguration 参数,从 ZK 拉取配置来填充/覆盖本地 properties。
若需要从 zk 拉取配置,用户需要提前在 ZK 上进行配置。
Zk 配置文件所在路径为 /services/com.xxxx.YYService/Configuration/${configName}。
- configName 默认值为 Default
8.1.3 Client 端
ClientFactory.ClientBuilder<Hello.Iface> builder = new ClientFactory.ClientBuilder<>(Hello.Iface.class);
Hello.Iface iauthClient = builder.build();
// 调用方法
iauthClient.helloString("rainsheep");
Client 怎么找到要访问的结点信息?
Client 端根据 Hello 类(thrift 文件对应类)所在路径(包名+类名),按照指定规则进行拼接,得到 zkPath,从 zk 上拉取端点信息。
比如: Hello 类所在的包名为 com.xiaomi.mict.thrift,那么 zkPath 即为 /services/com.xiaomi.mict.thrift.Hello/Pool/
xiaomi-common-thrift 怎么获取 Client 对象的?
xiaomi-common-thrift 使用了代理模式,我们 build() 出的是一个实现了 Iface 接口的代理对象,当我们调用此代理对象的方法时,就会触发 com.xiaomi.miliao.thrift.ClientProxy.invoke(Object proxy, Method method, Object[] args) 方法,会从连接池中获取一个 Client 对象调用远端方法。
8.2 Thrift 接入 IAuth
8.2.1 所需依赖
<dependency>
<groupId>com.xiaomi</groupId>
<artifactId>rpc-security-impl</artifactId>
<version>${iauth.version}</version>
</dependency>
8.2.2 Service 端
8.2.2.1 设置 property
System.setProperty("server.security.iauth", "true");
Xiaomi-thrift 通过此配置判断 Service 端是否开启 IAuth。
若开启了 IAuth, 则会在 FrameBuffer(可以理解为字节流的缓冲区) 中添加一个拦截器 (org.apache.thrift.util.IAuthInterceptor)。
拦截去会去读取指定区间的二进制数组,从中获取 appId/token,然后调用 IAuthSdk 验证,后续流程和 http 协议一致。
Service 端与 IAuth 服务器依旧使用 http 进行交互。
server.security.iauth 和 iauth.sdk.service.mode 区别
-
server.security.iauth 是 xiaomi-common-thrift 判断是否开启 IAuth 的依据。
-
iauth.sdk.service.mode 是 iauth-sdk 判断是否开启 iauth 的依据,sdk 会进行定时拉取 tokenKey 等操作。
8.2.2.2 配置 iauth.properties
iauth.sdk.service.mode=true
iauth.sdk.service.name=demo_server_test
iauth.sdk.service.serverKey=MyRi1qiEKQ3aB8wyg5bxPA==
iauth.sdk.service.signVersion=2
8.2.2.3 添加注解
public class HelloService extends MiliaoSharedServiceBase implements Hello.Iface {
@Override
@IAuthScope(0)
public String helloString(String param) {
return "hello " + param;
}
@Override
public void stop() {
}
}
8.2.3 Client 端
8.2.3.1 配置 iauth.properties
iauth.sdk.app.mode=true
iauth.sdk.app.appId=2882303761517122425
iauth.sdk.app.appKey=v4uprbwig/TWaQ0diQIL1g==
iauth.sdk.app.serviceId=demo_server_test
iauth.sdk.app.scope=3001 3002
8.2.3.2 使用 api 开启 iauth
ClientFactory.ClientBuilder<Hello.Iface> builder = new ClientFactory.ClientBuilder<>(
Hello.Iface.class);
// startIAuth 可省略
Hello.Iface iauthClient = builder.startIAuth(true).serviceId("rainsheep").build();
客户端开启 IAuth 后,获取 thrift client 时,会调用 IAuth sdk 获取 token,并将获取到的 token(data) 信息写入到输出流(TByteArrayOutputStream) 中,字节如下:
magic | appid(long) | data size(int) | data
0 | 1 2 3 4 5 6 7 8 | 9 10 11 12 | data
8.3 thrift IAuth 对比 http IAuth
-
在 Cient 端/Service 端与 IAuth 服务器交互时,两种接入方式一致,都是调用 IAuth sdk 完成,sdk 与 IAuth 服务器的交互使用 http 协议完成。
-
在 Client 端与 Service 端交互时,两种接入方式使用的协议不一致。
-
使用 http 协议接入时,若 Client 端开启 IAuth,Service 端关闭 IAuth,则请求可以通过,Service 会忽略所携带的 token。
-
使用 thrift 协议接入时,Client 和 Service 的 IAuth 状态需要保持一致,否则会报错(字节流与预期不一致,读取字节流时会报错)。