一、场景
出于对于服务器与客户端数据传输安全的需要,将整个请求的入参出参按照约定方式加密,这里选用 AES
,入选理由是安全性高、效率高。唯一的问题是需要不通过网络传输秘钥,且加密双方能动态约定秘钥,来完成加密/解密。于是乎这成了本文的话题。
二、秘钥交换算法
这个算法简称 DH(Diffie-Hellman Key Exchange/Agreement Algorithm)
,算法的原理自行百度吧,这不是本文讨论的话题。
要知道的是,我们可以基于这个 DH
算法解决上面的场景问题,即按照某种方式给加密双方一个约定的秘钥。
三、实现
这里我就直接上代码了,有兴趣的直接粘贴复制就完事了。
-
DHProperties
定义一个属性文件来约束DH
@Data
@Component
@ConfigurationProperties(prefix = "info.dh")
public class DHProperties {
/**
* 加密开关,true表示开启加密,默认是true
*/
private Boolean open = true;
/**
* redis 存储协商中的前缀
*/
private String conferPrefix;
/**
* 协商中过期时间,单位(秒)
*/
private Long conferOverdueTime;
/**
* redis 存储协商完成的前缀
*/
private String keyPrefix;
/**
* 协商结果过期时间,单位(秒)
*/
private Long overdueTime;
}
-
Locks
定义一个基于redis
的分布式锁
@Slf4j
@Component
public class Locks {
@Autowired
StringRedisTemplate redisTemplate;
public <T> void consumer(String key, T t, Consumer<T> fun) {
log.info("获取分布式锁: {}", key);
try {
if (!redisTemplate.opsForValue().setIfAbsent(key, "1")) throw new RuntimeException("获取分布式锁异常");
fun.accept(t);
} finally {
redisTemplate.delete(key);
}
}
}
-
DHService
秘钥算法的具体实现类
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.flashwhale.cloud.gateway.common.DHProperties;
import com.flashwhale.cloud.gateway.utlis.Locks;
import com.flashwhale.cloud.gateway.utlis.dh.ClientDTO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
/***
* 基于dh的协商实现 key的方案
*/
@Component
public class DHService {
@Autowired
StringRedisTemplate redisTemplate;
@Autowired
ObjectMapper objectMapper;
@Autowired
DHProperties dhProperties;
@Autowired
Locks locks;
/**
* 协商中
* 客户端用来获取协商信息
*
* @return 返回协商中的信息到客户端
*/
public Map<String, String> getBaseData() {
DH dh = new DH();
Map<String, String> baseData = dh.init();
String uuid = UUID.randomUUID().toString();
redisTemplate.opsForValue().set(dhProperties.getConferPrefix() + uuid, serialization(baseData),
dhProperties.getConferOverdueTime(), TimeUnit.SECONDS);
//服务端的server_number是不能直接暴露给客户端的 我们给客户端的应该是processed_server_number
String processedServerNum = baseData.get("processed_server_number");
baseData.remove("processed_server_number");
baseData.put("server_number", processedServerNum);
baseData.put("uuid", uuid);
return baseData;
}
/**
* 即将协商完成
* 客户端回发 协商信息 到服务端, 服务端生成协商秘钥
*
* @param clientDTO 客户端传过来的参数
*/
public void postClientData(ClientDTO clientDTO) {
if (null == clientDTO
|| !StringUtils.hasText(clientDTO.getClientNumber())
|| !StringUtils.hasText(clientDTO.getUuid())) return;
locks.consumer(clientDTO.getUuid(), clientDTO.getClientNumber(), (x) -> {
// 需要根据客户端传来的 uuid 取出上一个接口中协商好的server_number和p
String json = redisTemplate.opsForValue().get(dhProperties.getConferPrefix() + clientDTO.getUuid());
if (!StringUtils.hasText(json)) return;
Map<String, String> ret = deserialization(json);
//删除这个id
redisTemplate.delete(clientDTO.getUuid());
String serverNum = ret.get("server_number");
String p = ret.get("p");
DH dh = new DH();
String key = dh.computeShareKey(clientDTO.getClientNumber(), serverNum, p);
System.out.println(key);
redisTemplate.opsForValue().set(dhProperties.getKeyPrefix() + clientDTO.getUuid(), key, dhProperties.getOverdueTime(), TimeUnit.SECONDS);
});
}
/**
* 获取协商好的key
*
* @param uuid 协商会话标记
* @return 返回协商的key 如果为null 表示没有协商key
*/
public String getKey(String uuid) {
if (!redisTemplate.hasKey(dhProperties.getKeyPrefix() + uuid)) return null;
return redisTemplate.opsForValue().get(dhProperties.getKeyPrefix() + uuid);
}
/**
* 反序列化方法
*
* @param json json字符串
* @return 返回一个map ,由于使用的是 StringRedisTemplate 存储 ,通常这里是一个 Map<String, String>
*/
HashMap deserialization(String json) {
try {
return objectMapper.readValue(json, HashMap.class);
} catch (JsonProcessingException e) {
throw new RuntimeException("反序列化失败");
}
}
/**
* 序列化方法
*
* @param dataMap 要序列化的字符串, 通常这里是一个 Map<String, String>
* @return 返回一个json字符串
*/
String serialization(Map<String, String> dataMap) {
try {
return objectMapper.writeValueAsString(dataMap);
} catch (JsonProcessingException e) {
throw new RuntimeException("序列化失败");
}
}
}
-
ClientDTO
在提供接口前构造一个类用于封装客户端入参
@Data
public class ClientDTO implements Serializable {
/**
* 客户端生成的 clientNumber
*/
String clientNumber;
/**
* 第一步返回给客户端的 redis缓存标记
*/
String uuid;
}
- 构建用于客户端请求服务器的
DH
服务接口
/**
* 给客户端提供的协商keyapi
*/
@RequestMapping("dh")
@RestController
public class DHController {
@Autowired
DHService dhService;
/**
* 给客户端提供一个可以使用的 server_number 相关信息
*/
@GetMapping("basedata")
Map<String, String> dhBaseData() {
return dhService.getBaseData();
}
/***
* 传入客户端生成的 clientNumber 来生成协商的 key
*/
@PostMapping("clientdata")
void dhClientData(@RequestBody ClientDTO clientDTO) {
dhService.postClientData(clientDTO);
}
}
接口说明:
这里我们提供了2个接口
-
basedata
用于客户端找服务器第一次获取协商数据 -
clientdata
用户客户端将自己预创建的key
发送到服务器端(以便服务器算出最终秘钥)
四、测试
编写一个客户端来测试,预期结果是不将秘钥通过网络传输,各自算出相同的秘钥。
@Component
public class DHClient {
private static int mClientNum = (new Random()).nextInt(89999) + 10000;
@Autowired
RestTemplate restTemplate;
public String getKey() {
Map<String, String> resMap = restTemplate.getForObject("http://localhost:8080/dh/basedata", Map.class);
String p = resMap.get("p");
String g = resMap.get("g");
String serverNumber = resMap.get("server_number");
String uuid = resMap.get("uuid");
String processClientNumber = processNum(g, mClientNum + "", p);
System.out.println("客户端生成的 clientNumber 是: " + processClientNumber);
MultiValueMap<String, Object> paramMap = new LinkedMultiValueMap<>();
paramMap.put("clientNumber", Collections.singletonList(processClientNumber));
paramMap.put("uuid", Collections.singletonList(uuid));
restTemplate.postForObject("http://localhost:8080/dh/clientdata", paramMap, Void.class);
String mClientKey = processNum(serverNumber, mClientNum + "", p);
System.out.println("客户端协商的key 是: " + mClientKey);
test(mClientKey);
return mClientKey;
}
static String processNum(String g, String e, String p) {
BigInteger mP = new BigInteger(p);
BigInteger mG = new BigInteger(g);
return mG.modPow(new BigInteger(e), mP).toString();
}
void test(String key) {
System.out.println("客户端加密内容:" + AESUtil.encrypt("你好 我是测试内容 nihao,woshicesneir 1234567890", key));
}
}
这里要感谢 ti-dh 提供开源项目
欢迎关注我的个人公众号
网友评论