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 访问流程

image

流程

  1. serviceA 发起对 serviceB 的服务接口访问之前, 向 iAuth 申请对应的 token。 serviceA 将自身 appId、serviceB 的资源代码列表(scopeList)、serviceB 的识别 ID (sid) 提交给 iAuth。

  2. iAuth 对收到的 token 请求进行验证,验证通过后取出 DB 中的对称加密秘钥 serviceKey,用 serviceKey 将请求的 appId、scopeList、sid 和当前的时间戳 time 进行 AES 加密,生成 token。

  3. iAuth 返回 token 给 serviceA。

  4. serviceA 拿到 token 后,对 serviceB 的服务接口访问之时, 附加自身 ID(appId) 和对应的 token 到请求参数中。

  5. serviceB 向 iAuth 请求自己的对称加密秘钥 serviceKey。

  6. iAuth 鉴定是合法的请求后,将秘钥 serviceKey 返给 serviceB。

  7. serviceB 用拿到的秘钥 serviceKey 解开 token,解开后验证 token 是否超时(超时时间由 serviceB 设定)。

  8. serviceB 将 scopeList 代表的资源返给 serviceA。

2. web 界面的使用

新版IAuth 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

  1. 需要 sdk version >= 2.5.9,通过变量去获取域名,域名写死在代码中

  2. 可以配置在 System Property 或者 iauth 配置文件中

  3. 读取变量优先级: 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 参数篡改。

UML 图

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 params, String serviceKey)

获取 token 的地址:/token/getToken

Sign 的生成流程:

  1. 新建 List 加入method=GET,uri=/token/getToken,param(排序),以及 appKey。最终采用 & 将各个 String 连接形成最初的 data。

  2. 对 data 数据进行 SHA1 加密和 Base64 编码。

6.1.3 sign v2

对应代码:SignUtil.genSignature(String method, String uriPath, List params, String secretKey)

获取 token 的地址:/token/V2/getToken

Sign 的生成流程:

  1. 生成 data 的数据与 V1 类似,不同点是 data 并没有将 appKey 集成。

  2. 对 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 身份

  1. IAuth 验证该 {sid, scopes, appId} 是否存在授权关系。(MySQL中)

  2. 验证 nonce 是否过期

  3. 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 步骤

  1. 获取 tokenKey(DB中)。

  2. 随机生成 ssecurity(securityKey),并生成 Token。ssecurity 在对 url 进行签名的时候会使用到。

  3. 使用 tokenKey 加密 Token。

  4. 将加密后的 token 和 ssecurity 打入到返回结果中。

6.3 Client 端访问 Service 端

Client 端发送请求

  1. Client 端从 IAuth 服务端获取 token(根据 sid)

  2. 访问 Service 端(携带 appId, token)

Service 端收到请求

  1. 拦截器根据 url 拦截请求

  2. 判断访问的接口/方法是否有 @IAuth 注解,有注解获取注解的 scope,没有则放行。

  3. 获取 tokenKey 解密 token

  4. 验证 token 是否合法

    1. 对 token 中的 content 进行 sign 验签
    2. 是否过期
    3. 请求参数 appId 和 token 中 appId 是否一致
    4. Token 中携带的 scopes 和接口所需 scope 是否有交集
  5. 若需校验 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 服务端收到请求后

  1. 用私钥解出 KeyData(包含 nonce, aesKey)

  2. 验证 Nonce (nonce 有效期 5min)

  3. 根据 Sid 从 DB 中取出 tokenKey

  4. 根据 KeyData 中的 AesKey 加密 TokenKey

  5. 生成 Sign (sign = base64(sah1(serviceKey=${encryptedTokenKey}&aesKey)))

  6. 将 encryptTokenKey 和 Sign 传回。

Service 端收到响应后

  1. 取出 encryptedTokenKey(aes 加密后的)

  2. 取出 sign, 验证签名

  3. 用 aesKey 解密 encryptedTokenKey 获取 tokenKey

6.4.2 sign v2

Service 端发送请求时

向 IAuth 发送请求时携带

  • sid

  • key= new AES(serviceSecret).encrypt(randomAesKey)

  • nonce

  • Sign = HmacSha1(上述参数按一定规则拼接后的字符串,serviceSecret)

IAuth 服务端收到请求后

  1. 验证 Nonce (nonce 有效期 5min)

  2. 根据 sid 取出 serviceSecret

  3. 验证 sign 是否合法

  4. 从 DB 中取出 tokenKey

  5. 根据 serviceSecret 对客户端传递过来的 key(aesKey) 进行解密

  6. 根据解密后的 AesKey 加密 TokenKey

  7. 生成 sign(sign = base64(sah1(serviceKey=${encryptedTokenKey}&serviceSecret)))

  8. 返回 encryptedTokenKey 和 Sign。

Service 端收到响应后

  1. 取出 encryptedTokenKey(aes 加密后的)

  2. 取出 sign, 验证签名

  3. 用 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 次网络。会引发以下问题:

  • 出现访问故障的风险增大。

  • 访问延迟变大。

  • 耗费内网带宽。

于是引入缓存机制,来解决上述三个问题。

AA

加入缓存机制后,tokenKey 的更新是保证 IAuth 安全的重要机制

7.2 实现方案

  1. serviceB 持有一个 Key pair,持有 old tokenKey 和 new tokenKey,缓存在内存中。

  2. IAuth 持有一个 tokenKey List,保存在 DB 中,对应 key_sequence 表,持有 old tokenKey 和 new tokenKey, 当访问自己的 serviceB 集群全部获取过 new tokenKey 后,延迟一段时间后,将 old tokenKey 标记为失效。

  3. serviceA 去 IAuth 获取 token 时,IAuth 总是用有效的 tokenKey List 中次最新的 tokenKey 去加密 token,当 tokenKey List 中只有一个 tokenKey 时,就用唯一的这个 tokenKey 加密。

  4. serviceB 去 IAuth 获取 tokenKey 时,IAuth 总是返回最新的两个 tokenKey ,如果有效的 tokenKey List 长度为 1 时,需要根据数据库中最后一个标记为无效的 tokenKey Invalid 来判断:

    1. Invalid tokenKey 为空,或者当前时间离 Invalid tokenKey 标记的时间的间隔大于 T(IAuth),则返回这个 tokenKey 和一个假的 tokenKey(解任何token都会失败)。
    2. 否则返回 Invalid tokenKey 和 数据库中唯一有效的 tokenKey 。
  5. serviceA 访问 serviceB 如果不能用 old tokenKey 解开,则用 new tokenKey 去解,再解不开则返回错误。

  6. serviceA、 serviceB、 IAuth 的更新周期满足 T(ServiceA) <= T(ServiceB) <=T(IAuth)

  7. T(IAuth) 指的是轮询周期(定时任务周期),不是 tokenKey 的更新周期。

7.3 时间轴

BB

时间点说明:

  1. E0 (T0 < E0 < T1) 时刻更新 tokenKey。更新的是 DB 中 service_conf 表里的 tokenKey 字段,但此时 tokenKey 并未更新到 key_sequence 表中,而 key_sequence 表才是 IAuth server 获取 tokenKey 的源。

  2. T1 时 IAuth 的定时任务发起一次访问,检测到 service_conf 表中 serviceKey 已经更新,将新的 serviceKey 插入到 key_sequence 表,这时 IAuth 中有效的 Key 为 old tokenKey 和 new tokenKey。

  3. S1 (T1 < S1 < T2) 时刻 Service B 从 IAuth Center 获取到的 Key 列表为 new tokenKey 和 old tokenKey。从 S1 时刻开始,Service B 缓存变为 new tokenKey 和 old tokenKey。

  4. T2 时 IAuth 的定时任务再次发起一次访问,此时调用此 tokenKey 的 serviceB 均已获得最新的 tokenKey,因为 T(ServiceB) <= T(ServiceA)。在此次访问结束时 IAuth 会将 service_conf 表中的 old tokenKey 标记为无效,从 T2 时刻开始, IAuth Center 只有一个有效的有效的 new tokenKey。

  5. P0 (T2 < P0 < T3) 时 ServiceA 从 IAuth Center 获取的 token 是 new tokenKey 加密的token。

  6. T3 时所有的 serviceA 均已经拿到用 new tokenKey 生成的 token(不考虑网络耗时,内网通常在毫秒级)。

  7. 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) 参数介绍

  1. configName:从 zk 拉取配置时的配置文件名字。

  2. localIpAddr:ip 地址,为 null 自动获取。

  3. listConfigs:是否打印 zk 配置信息。

  4. registerZkNode:服务启动后自动向 zk 注册信息。

  5. 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 状态需要保持一致,否则会报错(字节流与预期不一致,读取字节流时会报错)。