您的位置:首页 > 产品设计 > UI/UE

基于口令和证书认证(TrueLicense)的接口调用工具库的封装设计 By 嗡汤圆

2016-07-28 17:42 826 查看

安全性

用户名、口令认证

用户名、口令泄露,没有证书也无法完成认证

用户名、口令与证书绑定,用户、证书与硬件MAC地址绑定。

用户名、口令、证书均泄露后,若硬件MAC地址不在认证范围,则仍无法使用服务。

用户名、口令、证书泄露,盗用者通过解密获取MAC地址并伪造后,服务端仍可以通过修改配置实时变更认证MAC地址,甚至使证书失效。

需求&优点

需求

接口使用者无需知道接口地址和传输实现,仅需知道传入参数和传出传出数据的结构。

接口使用者必须拥有合法的用户名、口令以及证书。

接口使用者的项目中必须包含颁发给该使用者的证书,才能正常使用接口。

接口使用者通过口令和证书认证后获取到Token,然后通过Token调用其它功能接口。

优点

对接口使用者隐藏了接口的真实地址和实际调用传参规范。

使用证书认证,即使用户名和口令泄露,无证书的用户仍然无法使用接口功能。

证书可以限制接口使用者的使用权限有效时间。一旦证书过期,使用者将无法使用接口。

证书可以包含加密的额外认证信息,比如设备MAC地址认证信息等,这样即使证书和口令一起泄露,使用者仍然无法使用未经认证的机器来调用接口。服务端也可及时修改配置使泄露的证书失效。

使用者用法说明

对于使用者的个人项目而言,仅需引入接口调用工具库(假设为:dev-tool.jar)后即可使用。假设工具库中约定了如下元素:

1. 接口调用类HttpsClient。

2. 接口地址字典Endpoints。

2. 接口返回通用结构RespResult。

3. 错误码字典ResultState。

那么,用户仅需知道接口的名称即可通过类似如下的代码获取接口信息:

HashMap<String, Object>paramMap = new HashMap<String, Object>();
paramMap.put("paramName", "paramVale");
try {
RespResult res = HttpsClient.invoke(Endpoints.get("interfaceName"), paramMap);
if(res.statusId == ResultState.SUCCESS)
Object actualData = res.getData();
else
logger.error("interface return error:"+res.errMsg);
} catch (Exception e){
logger.error("httpsclient error:"+e)
}


整个调用过程中的接口地址、调用实现、调用传参的具体细节用户可以一概不管。

接口工具库设计

概述

接口工具是和接口服务对应的,因此又写代码应该与接口服务公用,比如接口返回的数据结构
RespResult
以及错误字典
ResultState
。其次,接口工具库不应该对外暴露太多方法,比如此处仅需暴露用户会使用到的
invoke(String url, HashMap<String, Object> param)
方法即可。

证书验证

我们使用trueLicense来实现证书的验证,服务端如何产生私钥、公钥以及如何根据用户信息生成license文件此处一概忽略,感兴趣的同学可以参考这篇博文:使用truelicense实现用于JAVA工程license机制(包括license生成和验证)

证书验证的时机

本文将证书认证包含于接口调用
invoke
之初,只有在证书认证通过后,才能从证书中解密出其他信息,一并发给服务接口,进行进一步认证。伪代码如下:

public static RespResult invoke(String url, HashMap<String, Object> params) throws Exception{

//step1:从证书中提取额外信息
HashMap<String, Object> extraLicenseInfo = LicenseUtil.verify();
//step2:将额外信息与用于原参数合并
params.putAll(extraLicenseInfo);
//step3:调用https客户端发送数据,简化写,省略HTTP CODE 200神马的判断
return JSON.toJSONString(Https.post(url, params));
}


基本证书验证的实现

STEP1:首先需要在工具项目中需要有如下信息:公钥库文件路径PUBLICSTOREPATH 、公钥库访问密码STOREPASSWORD、key访问密码KEYPASSWORD、别名ALIAS、解密主题SUBJECT即可。具体用到的license由使用者放置在外部项目中。

STEP2:编写解密代码,实例化一个LicenseManager。根据trueLicense的用例参考,我们需要设置LicenseManager、LicenseParam、CiperParam即可,代码大致入下

LicenseManager licenseManager = new LiceseManager(new DefaultLicenseParam(
SUBJECT,
Preferences.userNodeForPackage(LicenseUtil.class),
new DefaultKeyStoreParam(LicenseUtil.class, PUBLICSTOREPATH, ALIAS, STOREPASSWORD,KEYPASSWORD),
new DefaultCipherParam(PUBLICSTOREPASSWORD)));


STEP3:安装证书,此步骤用到licenseManager示例以及证书license文件。license文件有使用者提供,因此我们只能规定该文件必须命名为license.lic之类的,这样代码就可以直接运行了。

//比如我们规定证书文件名为license.lic,且安置于classpath下
licenseManager.install(new File("license.lic"))


STEP4:解密出证书内容LicenseContent,安装过证书的licenseManager,就可以通过veryfy()方法获取证书内容了。

LicenseContent content = licenseManager.verify();


额外证书认证

证书license.lic中包含的内容就是服务端生成的内容,可以包含的内容和内容示例如下:

SUBJECT=DECODESUBJECT
ISSUEDATE=2016-07-25
NOTBEFORE=2016-07-25
NOTAFTER=2017-07-24
CONSUMERTYPE=user
CONSUMERAMOUNT=1
INFO=This is a license for user1
EXTRA=This is extra infomation


解密出的LicenseContent即可读出上边的内容,其中验证日期如果不在NOTBEFORE和NOTAFTER之间的话,就不能通过verify()验证。

证书中包含的EXTRA字段是个Object,我们可以通过EXTRA来进行我们的自定义验证过程。

extra字段的格式设置

为了实现extra合法性的自验证,我们设置extra样例如下:

extra=2<<>>1#mac#0C45DCC63C3E3##0#licenseuser#user1


各位置功能说明如下:

extra=paircount<<>>doverifyflag#pairkey#pairvalue##...


其中:

<<>>
分割验证项目总数和具体验证项目的字符串

##
分割验证项目

#
分割验证项目中的具体项目

paircount
为验证项目总数的个数

doverifyflag
为是否在本地进行该验证

pairkey
验证项目名称

pairvalue
验证项目值

如上,extra字段值不符合上述规范的,直接视为验证失败。

额外验证的实现

PART 1:验证总流程

该流程目前为写死在工具库代码中的,通过
##
分割的项目通过各自的顺序依次进行,同时将额外验证项目合并到用户需要POST的参数项目中去,以便服务端进行进一步处理。

//伪代码
//1:验证extra串是否合法
validateExtra(extra);
//2:拆出验证信息
String[] extraPairs = getExtraPair(extra);
//3:每个验证信息都要经过验证
for(int i =0;i < extraPairs.length; i++){
//这里还要加上doVerifyFlag的处理,这里略去了
doVerify(i,getKey(extraPairs[i]), getValue(extraPairs[i]));
paramMap.put(getKey(extraPairs[i]), getValue(extraPairs[i]));
}


PART 2:验证的调度

通过项目所处的位置以及key,value来进行验证,不同位置的项目采用不同的方法

private void doVerify(int index, String key, String value) throws Exception {
switch(index){
case:0
//具体的verify方法具体实现,比如假设这里是验证设备mac地址的地方
//之所以要把key传进来,就可以做到顺序和key一一对应的限制了
doVerify_0(key,value);
break;
case:1
doVerify_1(key,value);
break;
default:
//如果extra符合规范,但是出现了多余的项目也不能通过验证
throw new Exception("invalid pair index, no method found");
}
}


PART 3:以MAC地址认证为例,说明具体认证方法

private void verifyMAC(String key, String value)throws Exception {
if(!key.equals("mac"))
throw new Exception("key not match");
List<String> macsInDevice = HardwareManager.getMacs();
for(String mac : macsInDevice){
if(value.equalsIgnoreCase(mac))
return;
}
throw new Exception("mac address valid fail");
}


PART 4:客户端服务端双认证的实现

仍然以MAC地址为例:

1-首先用户需要向管理员提交自己的设备MAC地址,管理员将用户和MAC对应关系记下,同时将MAC地址写入extra字段,再生成证书。

2-用户拿到证书后,使用工具库调用invoke()方法时,工具库在本地验证设备mac是否匹配,同时将mac参数附加在https请求的参数中去。

3-通过本地验证后,工具库将用户提交的参数以及附加参数(即MAC地址)一起发给服务端的接口。

4-服务端接收到参数后,验证该MAC地址是否是该用户注册的地址,如果是,则进行服务操作,否则拒绝服务。

服务端与客户端的密码设置与LicenseManager设置

密码

管理员拥有:私钥库密码
PRIVATE_STORE_PWD
、私钥密码
PRIVATE_KEY_STORE
、证书加密密码
CIPHER_PWD


用户拥有:公钥库密码
PUBLIC_STORE_PWD
、证书加密密码
CIPHER_PWD


LicenseManager

分别声明KeyStoreParam, Cipher后声明LicenseParam,然后再声明LicenseManager。

服务端:

KeyStoreParam ksParam = new DefaultKeyStoreParam(class, resource, alias, PRIVATE_STORE_PWD,PRIVATE_KEY_STORE);
CipherParam cipher = new DefaultCipher(CIPHER_PWD);
LicenseParam licenseParam=new LicenseParam(SUBJECT, PREFERENCE, ksParam, cipher);
LicenseManager manager = new LicenseManager(licenseParam);


客户端:

CipherParam用
CIPHER_PWD
实例化,KeyStoreParam仅需公钥库
PUBLIC_STORE_PWD
,key密码为null即可。

本地代码的安全性

由于工具库需要提交给使用者,若使用者通过反向工程,获取代码则可能让使用者了解到工具库的内部逻辑。所以需要做一些加密处理。这里不做太多介绍:

1、可以使用proguard进行代码混淆。

2、代码中保存的公钥库密码,公钥库配置信息等尽量不要直接写为String常量等,否则公钥密码直接就能通过反编译获得(虽然被用户知道公钥密码也没什么关系)。可以进一步通过混淆方式间接生成密码串。这里可以参考truLicense的ObfuscatedString类的实现方式,通过long[]数组间接计算还原原来的String。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息