JMicro是基于Java实现的微服务平台,最近花了两个周未实现服务间安全调用支持。

JMicro服务调用分两个部份,分别为内部服务间相互调用和外部客户端通过API网关调用JMicro集群内部服务,前者支持双向加密加签,并且支持全RSA加密(效率底,安全性高)及RSA+AES混合加密解密,后者只支持RSA+AES混合加密解密,类似于HTTPS的功能。

内部RPC实现安全通信配置

内部服务间安全通信配置比较简单,如下为JMicro系统登陆接口:

upSsl=true表示客户端请求登陆时,上行数据需要做安全加密处理,客户端使用自己的私钥签名,用服务端公钥对数据或AES密钥做加密处理;

downSsl=true表示服务端返回登陆成功数据时(任何情况下RPC调用失败都不做加密处理),下行数据需要做安全加密处理,服务端使用己方私钥加签,用客户端公钥或AES动态密钥对下行数据做加密处理。

EncType=0表示RSA+AES混合加密解密,数据发送方生成动态密钥S并用对方公钥对S进行RSA加密,S的密文和数据一起发送给对方。用密钥S对数据做AES加密。此方式加密解密效率非常高,但要保证密钥足够复杂,不易被暴力破解。

EncType=1表示全RSA加密,直接用对方公钥对参数做RSA非对称加密,用自己的私钥加签,对方收到数据时,用对方自己私钥解密数据,用发送方公钥验签,此方式安全性高,但效率较底,使用于对安全性要求非常高,数据量少的场景。

JMicro密码生成算法

完整代码:

https://github.com/mynewworldyyl/jmicro/blob/master/apics/src/main/java/cn/jmicro/api/rsa/EncryptUtils.java

JMicro实现一套动态密码生成算法,可以根据需要生成指定长度的密码或IV值。

首先定义3个常量

	//一级随机种子
private static final Random RSEED = new Random(System.currentTimeMillis());
//密码表长度
public static final int CHAR_TABLE_LEN = 512;
//密码表
public static final char[] USABLE_CHAR = new char[CHAR_TABLE_LEN];

系统启动时,生成密码表,代码如下

static {
createPwdTable();
}
private static void createPwdTable() {
int len = CHAR_TABLE_LEN;
Random r = new Random(RSEED.nextInt());
//StringBuffer sb = new StringBuffer();
for(int i = 0; i < len; i++) {
int rv = r.nextInt();
if(rv < 0) {
rv = -rv;
}
int c = (rv % 85) + 33;
USABLE_CHAR[i] = (char)c;
//sb.append((char)c);
}
}

以一级随机数生成随机种子再实例化一个随机数,这样保证每调用一次createPwdTable方法,生成的随机密码表都不一样,系统按照一定时间间隔调用createPwdTable方法刷新密码表。

基于以上的密码表,生成指定长度密码,代码如下:

public static String generatorStrPwd(int len) {
StringBuffer data = new StringBuffer();
Random r = new Random(System.currentTimeMillis());
for(int i = 0; i < len; i++) {
int idx = r.nextInt(1024)%CHAR_TABLE_LEN;
data.append(USABLE_CHAR[idx]);
}
return data.toString();
}

公钥管理

JMicro支持N个动态服务系统,每个系统都可以有其独立的私钥和公钥,且每个系统的每个服务在被调用之前都不知道当前系统有多少个系统多少个服务在运行,所以不可能预先在每个系统中配置好对方的公钥,因此公钥的安全分发成为JMicro安全系统的一个核心问题。正如HTTPS证书一样,需要权威的证书中心来维护,JMicro目前不支持从公开的证书中心取证书,一方面因为成本原因,另一方面,JMicro是一个由N个JVM的M个服务组成的微服务系统,需要N个证书,并且支持证书动态更新,因此JMicro实现一个独特的证书管理系统,并且抽象出标准的获取证书的RPC接口。接口定义如下:

package cn.jmicro.api.security;

import java.util.List;

import cn.jmicro.api.Resp;
import cn.jmicro.codegenerator.AsyncClientProxy; @AsyncClientProxy
public interface ISecretService {
/**
* 根据实例前缀拿取公钥
* @param instancePrefix 服务前缀,不同的前缀有不同的公钥,相同前缀只能一个公钥启用
* @return
*/
Resp<String> getPublicKeyByInstance(String instancePrefix); /**
* 我的公钥列表,只能拿取当前账号下的公钥列表
* @return
*/
Resp<List<JmicroPublicKey>> publicKeysList(); /**
* 创建一个公私钥,同时可选给私钥加密保护
* 如果创建成功,返回公钥和私给调用者,服务端只保留公钥,不存储私钥,所以创建者一定要保留好私钥及私钥密码,私钥一旦丢失,
* 将无法找回,只能重新生成。
* @param instancePrefix 服务前缀
* @param password 私钥密码
* @return
*/
Resp<JmicroPublicKey> createSecret(String instancePrefix,String password); /**
* 删除未启用的公钥,删除后将无法恢复
* 启用中的公钥不能删除
* @param id
* @return
*/
Resp<Boolean> deletePublicKey(Long id); /**
* 更新公钥前缀
* 启用中的公钥不能更新
* @param id
* @param instancePrefix
* @return
*/
Resp<Boolean> updateInstancePrefix(Long id,String instancePrefix); /**
* 增加一个线下生成的公钥到系统中
* @param instancePrefix
* @param publicKey
* @return
*/
Resp<JmicroPublicKey> addPublicKeyForInstancePrefix(String instancePrefix, String publicKey); /**
* 启用公钥,公钥启用后,其他系统就可以根据前缀取得公钥,从而可以与前缀所对应的系统做安全通信
* 同一个前缀同一时刻只能有一个公钥被启用
* @param id
* @param enStatus
* @return
*/
Resp<Boolean> enablePublicKey(Long id,boolean enStatus);
}

除了getPublicKeyByInstance RPC方法之外,其他方法为可选实现,用于对公钥做维护。getPublicKeyByInstance用于服务请求方与服务提供方做交互之前获取对方的公钥,以便能与对方做安全通信。如果是双向安全通信,服务方收到客户端请求后,也要根据客户端实例前缀获取客户端的公钥做返回数据加密及上行数据验签。

下图为JMicro系统实现代码

可以看出,此方法本身也是双向安全通信的RSA+AES混合加密模式,从而保证任何调用此方法取得的公钥都是可信任的,不可能存在“中间人”修改作假的问题。

调用此方法的前提条件是安全中心的公钥需要预配置到客户端系统中,因为安全中心只需要一对公私钥,任何系统都可以提前获得此公钥。以下为JMicro默认两个预先配

置好的公钥,APICS模块为JMicro的最基础模块,所以JMicro微服务应用都可以安全地获得公钥接口权限。我们使用的电脑的操作系统是不是预先保存有权威证书管理中心的公钥?

注意:因为考虑到JMicro系统安全原因,JMicro公钥管理系统没有做开源,但是只需要根据以上接口的定义实现相关逻辑,就可以做同样的功能。

Java客户端通过Api网关调用JMicro服务

前面说过,API网关与外部系统通信,只支持RSA+AES混合模式,严格来讲,是API网关与基于Web浏览器为基础的网页端通信,只支持RSA+AES混合模式,而Java客户端与API网关通信,可以支持全RSA加密模式。因为在WEB应用中,如果需要支持全RSA模式,则需要为WEB端分配一对公私钥,而WEB端的全部资源,需要从服务端加载,我们不可能把私钥加载到用户的浏览器吧,如果私钥这样加载,那还有什么安全性可言呢?所以在WEB端不支持全RSA加密模式或双向RSA加密模式,并非技术上实现不了,而是RSA加密算法本身的特性所决定其没有意义,甚至存在涉密风险!

还有一个很有意思的问题,有很多没搞明白RSA加密算法原理的同学经常会有用私钥加密公钥解密这种想法,这也是没有意义的?为什么呢?因为公钥是公开的,大家都可以获取到,用私钥加密和没加密就没什么区别了!

对于Java客户端与服务网关通信,通过以下配置即可实现双向安全配置,encType可以是0或1:

	@BeforeClass
public static void setUp() { ApiGatewayConfig config = new ApiGatewayConfig(Constants.TYPE_SOCKET,"192.168.56.1",9092);
config.setUpSsl(true);//上行加密
config.setDownSsl(true);//下行加密
config.setEncType(0);//AES加密数据 ApiGatewayClient.initClient(config);
socketClient = ApiGatewayClient.getClient();
}

浏览器WEB客户端通过Api网关调用JMicro服务

在网页初始化时,通过以下代码启用安全通信支持,客户端请求Api网关的全部请求参数将被RSA+AES加密,Api网关返回数据也将被加密并签名,客户端做解密并验签。

    window.jm.config.sslEnable = true;
window.jm.rpc.init(window.jm.config.ip,window.jm.config.port);

下面说下JMicro使用JSEncrypt和CryptoJS方式,有需要的可以直接复制代码使用,否则很多细节调起来相当麻烦。

首先是客户端请求加密,通过JSEncrypt做RSA非对称加密AES密钥,然后用AES密钥加密数据

encrypt: function (msg){
if(!this.pwdTable) {
this.init();
} msg.setUpSsl(true);
msg.setDownSsl(true);
msg.setEncType(false); let opts = {
mode : CryptoJS.mode.CBC ,
padding : CryptoJS.pad.Pkcs7,
keySize : this.keySize,
iv :null ,
salt : null
}; let iv = jm.eu.genStrPwd(16); //通过密码表生成16个字节的动态偏移量
opts.iv = CryptoJS.enc.Utf8.parse(iv); //将偏移量转为UTF8编码,服务端使用时也要相应地使用utf8字节数组
msg.salt = jm.utils.toUTF8Array(iv); //将偏移量和数据一起发送给服务端,注意是utf8字节编码 if(!this.pwd || new Date().getTime() - this.lastUpdatePwdTime > 1000*60*5 ) {
        //首次进来或超过5分钟更新一次密码
this.pwd = jm.eu.genStrPwd(16);//生成密码,方式和IV相同,但是功能不一样,参考前面关于密码表的说明
msg.setSec(true);//告诉服务端,有AES密码更新
msg.sec = this.encryptRas(this.pwd);//对AES密码做RSA加密
} let b64Str = jm.utils.byteArr2Base64(msg.payload);//将要发送的字节数据转为base64字符串格式,因为AES只接受字符串加密,同时方便服务器更好地处理解码
var encrypted = CryptoJS.AES.encrypt(b64Str, CryptoJS.enc.Utf8.parse(this.pwd),opts);//开始加密,密钥转为UTF8格式,保证Java服务端相同编码
msg.payload = this.wordToByteBuffer(encrypted.ciphertext);//encrypted.ciphertext是一个以WordArray,也就是一个整数数组,要将此整数数据转为字节数组
},

  

RAS加密密钥encryptRas

encryptRas:function(strContent) {
if(!this.pwdTable) {
        //初始化密码表
this.init();
}
let rst = this.rsae.encrypt(strContent);
return jm.utils.toUTF8Array(rst); //rst是base64编码后的十六进制字符串,此处对这个base64字符串做utf8编码转为字节数组
},

  

对应Java端的解密密钥

//对密钥做utf8解码为字符串,结果是密码密文的base64编码
String b64Str = new String(msg.getSec(),Constants.CHARSET);
//对密文做base64解码,得到密文的字节数组,
byte[] sec = Base64.getDecoder().decode(b64Str);
//解密密文,得到字节码形式的AES密码明文,字节码是密码的UTF8编码后的字节数组
secrect = EncryptUtils.decryptRsa(myPriKey,sec, 0, sec.length);

解密出密码明文后,开始用这个密码解密数据

SecretKey originalKey = new SecretKeySpec(secrect, 0, secrect.length, EncryptUtils.KEY_AES);
ByteBuffer bb = (ByteBuffer) msg.getPayload();
//msg.getSalt() 是客户端传过来的IV值的utf8编码后的数组,
byte[] d = EncryptUtils.decryptAes(bb.array(), 0, bb.limit(), msg.getSalt(), k.key);
if(msg.isFromWeb()) {
//因为WEB端是将数据做Base64编码为字符串后做的加密,所以Java端同样要将结果做Base64解码
  d = Base64.getDecoder().decode(d);
}

Java端对返回给WEB端的数据做加密

//因为WEB端验签时只认字符串,所以加签前把数据转为Base64字符串
byte[] b64Data = Base64.getEncoder().encode(bb.array());
sign = EncryptUtils.sign(b64Data, 0, b64Data.length, this.myPriKey);

数据AES加密

byte[] edata = EncryptUtils.encryptAes(bb.array(), 0, bb.limit(), salt, sec.key);

JS端做解密

let b64str = jm.utils.byteArr2Base64(msg.payload);
var decrypted = CryptoJS.AES.decrypt(b64str,utf8pwd,opts);
let dedata = this.byteBuffer2ByteArray(this.wordToByteBuffer(decrypted));

JS验签

if(!this.rsae.verify(jm.utils.byteArr2Base64(dedata), msg.sign, CryptoJS.MD5)) {
throw "Invalid sign";
}

完整代码可以参考

https://github.com/mynewworldyyl/jmicro/blob/master/apics/src/main/java/cn/jmicro/api/rsa/EncryptUtils.java

https://github.com/mynewworldyyl/jmicro/blob/master/api/src/main/java/cn/jmicro/api/security/SecretManager.java

https://github.com/mynewworldyyl/jmicro/blob/master/mng.web/public/js/rpc.js

JMicro 微服务管理系统: http://jmicro.cn/

【5】JMicro其于RSA及AES加密实现安全服务调用的更多相关文章

  1. c# .NET RSA结合AES加密服务端和客户端请求数据

    这几天空闲时间就想研究一下加密,环境是web程序,通过js请求后台返回数据,我想做的事js在发送请求前将数据加密,服务端收到后解密,待服务端处理完请求后,将处理结果加密返回给客户端,客户端在解密,于是 ...

  2. java使用RSA与AES加密解密

    首先了解下,什么是堆成加密,什么是非对称加密? 对称加密:加密与解密的密钥是相同的,加解密速度很快,比如AES 非对称加密:加密与解密的秘钥是不同的,速度较慢,比如RSA 先看代码(先会用在研究) 相 ...

  3. RSA、AES加密解密

    RSA #!/usr/bin/env python # -*- coding:utf-8 -*- import rsa import base64 # ######### 1. 生成公钥私钥 #### ...

  4. JAVA RSA加密AES加密

    RSA加密: import sun.misc.BASE64Decoder; import sun.misc.BASE64Encoder; import javax.crypto.Cipher; imp ...

  5. java 加密工具类(MD5、RSA、AES等加密方式)

    1.加密工具类encryption MD5加密 import org.apache.commons.codec.digest.DigestUtils; /** * MD5加密组件 * * @autho ...

  6. 你真的了解字典(Dictionary)吗? C# Memory Cache 踩坑记录 .net 泛型 结构化CSS设计思维 WinForm POST上传与后台接收 高效实用的.NET开源项目 .net 笔试面试总结(3) .net 笔试面试总结(2) 依赖注入 C# RSA 加密 C#与Java AES 加密解密

    你真的了解字典(Dictionary)吗?   从一道亲身经历的面试题说起 半年前,我参加我现在所在公司的面试,面试官给了一道题,说有一个Y形的链表,知道起始节点,找出交叉节点.为了便于描述,我把上面 ...

  7. polarssl rsa & aes 加密与解密

    上周折腾加密与解密,用了openssl, crypto++, polarssl, cyassl, 说起真的让人很沮丧,只有openssl & polarssl两个库的RSA & AES ...

  8. polarssl rsa & aes 加密与解密<转>

    上周折腾加密与解密,用了openssl, crypto++, polarssl, cyassl, 说起真的让人很沮丧,只有openssl & polarssl两个库的RSA & AES ...

  9. c#RSA的SHA1加密与AES加密、解密

    前言:公司项目对接了一个对数据保密性要求较高的java公司.api接口逻辑是这样的:他们提供 SHA1私钥 与 AES的秘钥.我们需要将 传递查询参数 通过SHA1 私钥加密再转换成 十六进制 字符串 ...

随机推荐

  1. day59:Linux:编辑工具vim&文件类型&文件属性

    目录 1.Linux编辑工具vim 2.Linux文件类型 3.Linux文件属性 4.今日份Linux练习题 Linux编辑工具vim 1.什么是vim 文本文件的编辑工具,  和windows的n ...

  2. 搭建 Spring 源码阅读环境

    前言 有一个Spring源码阅读环境是学习Spring的基础.笔者借鉴了网上很多搭建环境的方法,也尝试了很多,接下来总结两种个人认为比较简便实用的方法.读者可根据自己的需要自行选择. 方法一:搭建基础 ...

  3. FastDFS 分布式文件系统详解

    什么是文件系统 文件系统是操作系统用于在磁盘或分区上组织文件的方法和数据结构.磁盘空间是什么样的我们并不清楚,但文件系统可以给我们呈现一个非常清晰的表象,我们可以创建.删除.修改和复制这些文件,而实现 ...

  4. Book of Shaders 01 - 关于函数造型能力的理解

    0x00 从函数出发 Shader 中的很多效果都是由函数计算得出的,如何更好地理解二者的关系呢.不妨先看看函数是什么?函数的定义可以简单地描述为:给定一个集合 A,对于其中的元素施加法则 f,则可以 ...

  5. Java知识系统回顾整理01基础04操作符06三元运算符

    一.三元运算符 表达式?值1:值2 如果表达式为真 返回值1 如果表达式为假 返回值2 if语句学习链接:if语句 public class HelloWorld { public static vo ...

  6. 01 百度 AI Studio 基础操作记录(一) Notebook

    转载参考: AI Studio基本操作(一) Notebook篇 一.基础 1.新建文件: 可以使用命令, !cat <<newfile > newfile.py 在项目空间内直接创 ...

  7. P3660 [USACO17FEB]Why Did the Cow Cross the Road III G

    Link 题意: 给定长度为 \(2N\) 的序列,\(1~N\) 各处现过 \(2\) 次,i第一次出现位置记为\(ai\),第二次记为\(bi\),求满足\(ai<aj<bi<b ...

  8. 【题解】[AHOI2013]作业

    Link 题目大意:\(n\)个数,\(m\)个询问,每次四个参数,\(l,r,a,b\),问区间\([l,r]\)中出现过的,数值在\([a,b]\)区间中的数的个数以及区间\([l,r]\)中数值 ...

  9. 看动画学算法之:linkedList

    目录 简介 linkedList的构建 linkedList的操作 头部插入 尾部插入 中间插入 删除节点 简介 linkedList应该是一种非常非常简单的数据结构了.节点一个一个的连接起来,就成了 ...

  10. 如何选择JVM垃圾回收器?

    明确垃圾回收器组合 -XX:+UseSerialGC 年轻代和老年代都用串行收集器 -XX:+UseParNewGC 年轻代使用ParNew,老年代使用 Serial Old -XX:+UsePara ...