文章目录
简述
本文涉及代码已开源
本文网关gateway,微服务,vue已开源到gitee
杉极简/gateway网关阶段学习
https://gitee.com/dong-puen/gateway-stages
Fir Cloud 完整项目
该内容完整项目如下
Fir Cloud v1.0.0
https://gitee.com/dong-puen/fir-cloud
https://github.com/firLucky/fir-cloud
实现目的:
前端请求后端接口获得到服务端公钥。
http://localhost:51001/k
得到服务端公钥后,客户端生成自己的公钥与私钥,并将自己的公钥加密发送给服务端。
再次请求接口得到认证信息。
http://localhost:51001/cn
前端得到以下认证信息,之后将基于这些认证信息进行安全通信。
@ApiModelProperty("加密密钥")
private String secretKey;
@ApiModelProperty("会话id")
private String sessionId;
@ApiModelProperty("盐")
private String salt;
@ApiModelProperty("服务端通信公钥")
private String publicKey;
@ApiModelProperty("客户端端通信公钥")
private String clientPublicKey;
将两个接口融入到登录接口当中去
点击登录后,先请求http://localhost:51001/k与http://localhost:51001/cn接口,之后在进行登录操作,
后端也根据sessionId存储认证信息
后端
pom
增加hutool工具类
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.20</version>
</dependency>
nacos
增加登录过期时间配置
新增connectExpirationTime字段
global:
# 全局异常捕捉-打印堆栈异常
printStackTrace: true
# 令牌头变量名称
tokenHeader: Authorization
# 令牌校验
tokenCheck: true
# 通信过期时间(秒)
connectExpirationTime: 1800
# 不需要进行过滤的白名单
whiteUrls:
- /tick/auth/login
- /ws
修改全局配置文件
随之修改全局配置文件
package com.fir.gateway.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
* @author fir
* @date 2023/7/28 17:53
*/
@Data
@Component
@ConfigurationProperties(prefix = "global")
public class GlobalConfig {
/**
* 全局异常捕捉-打印堆栈异常
*/
private boolean printStackTrace;
/**
* 令牌头变量名称
*/
private String tokenHeader;
/**
* 令牌校验
*/
private boolean tokenCheck;
/**
* 登录过期时间
*/
private Integer loginExpirationTime;
/**
* 设置每次登录的过期时间单位(秒)
*/
private TimeUnit loginExpirationTimeUNIT = TimeUnit.SECONDS;
/**
* 白名单路由-不进行网关校验直接放过
*/
private List<String> whiteUrls;
}
安全通信认证接口
后端增加接口,用于服务器与客户端交换公钥等。
控制层
package com.fir.gateway.controller;
import com.fir.gateway.config.result.AjaxResult;
import com.fir.gateway.dto.ConnectDTO;
import com.fir.gateway.service.IAuthService;
import com.fir.gateway.utils.RSAUtils;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
/**
* 系统登录验证
*
* @author dpe
* @date 2022/8/4 22:58
*/
@Api(tags = "系统登录接口")
@Slf4j
@RestController
@RefreshScope
public class AuthController {
/**
* 系统登录验证 接口层
*/
@Resource
private IAuthService iAuthService;
@ApiOperation("客户端与服务端建立连接")
@RequestMapping("/k")
public AjaxResult info() {
String publicKey = iAuthService.getPublicKey();
return AjaxResult.success(publicKey);
}
@ApiOperation("客户端与服务端建立连接")
@ApiImplicitParams({
@ApiImplicitParam(name = "k", value = "公钥字符串"),
@ApiImplicitParam(name = "k", value = "客户端公钥字符串"),
})
@RequestMapping("/cn")
public AjaxResult connect(String k, String ck) {
ConnectDTO connectDTO = iAuthService.info(k, ck);
String content = RSAUtils.encryptSection(connectDTO, connectDTO.getClientPublicKey());
return AjaxResult.success(content);
}
}
接口层
package com.fir.gateway.service;
import com.fir.gateway.dto.ConnectDTO;
/**
* @author fir
* @date 2023/4/23 17:03
*/
public interface IAuthService {
/**
* 获取公钥
*
* @return 公钥
*/
String getPublicKey();
/**
* 获取通信认证信息
*
* @param publicKeyMd5 RSA公钥md5取值
* @param clientPublicKey 客户端公钥
* @return 认证信息
*/
ConnectDTO info(String publicKeyMd5, String clientPublicKey);
}
实现层
package com.fir.gateway.service.impl;
import com.alibaba.fastjson.JSONObject;
import com.fir.gateway.config.GlobalConfig;
import com.fir.gateway.config.exception.CustomException;
import com.fir.gateway.config.result.AjaxStatus;
import com.fir.gateway.dto.ConnectDTO;
import com.fir.gateway.dto.RsaKeyDTO;
import com.fir.gateway.service.IAuthService;
import com.fir.gateway.utils.AESUtils;
import com.fir.gateway.utils.MD5Utils;
import com.fir.gateway.utils.RSAUtils;
import com.fir.gateway.utils.SaltedHashUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.security.SecureRandom;
import java.util.Base64;
import java.util.Map;
/**
* @author fir
* @date 2023/4/23 17:03
*/
@Slf4j
@Service
public class AuthServiceImpl implements IAuthService {
@Resource
private RedisTemplate<String, Object> redisTemplate;
/**
* 网关参数配置
*/
@Resource
private GlobalConfig globalConfig;
/**
* 获取公钥
*
* @return 公钥
*/
@Override
public String getPublicKey() {
// 生成本此次连接的公钥与私钥对存储,将公钥发送到前端作为加密通信签名的方式
Map<String, String> map = RSAUtils.generateKey();
String publicKey = map.get(RSAUtils.PUBLIC_KEY);
String privateKey = map.get(RSAUtils.PRIVATE_KEY);
// 将公钥取md5后,作为key存入redis中
String publicKeyMd5 = MD5Utils.generateMd5ForString(publicKey);
RsaKeyDTO rsaKeyDTO = new RsaKeyDTO();
rsaKeyDTO.setPublicKey(publicKey);
rsaKeyDTO.setPrivateKey(privateKey);
Object obj = JSONObject.toJSON(rsaKeyDTO);
redisTemplate.opsForValue().set(publicKeyMd5, obj,
globalConfig.getConnectExpirationTime(), globalConfig.getConnectExpirationTimeUNIT());
return publicKey;
}
/**
* 获取通信认证信息
*
* @param publicKeyMd5 RSA公钥
* @param clientPublicKey 客户端公钥
* @return 认证信息
*/
@Override
public ConnectDTO info(String publicKeyMd5, String clientPublicKey) {
ConnectDTO connectDTO;
// 将公钥取md5后,作为key存入redis中
JSONObject jsonObject = (JSONObject) redisTemplate.opsForValue().get(publicKeyMd5);
if (jsonObject != null) {
RsaKeyDTO rsaKeyDTO = jsonObject.toJavaObject(RsaKeyDTO.class);
String publicKey = rsaKeyDTO.getPublicKey();
String privateKey = rsaKeyDTO.getPrivateKey();
clientPublicKey = RSAUtils.decryptSection(clientPublicKey, privateKey);
String secretKey = AESUtils.generateKeyAES();
String sessionId = generateSessionId();
String salt = SaltedHashUtils.generateSalt();
connectDTO = ConnectDTO.builder()
.secretKey(secretKey)
.sessionId(sessionId)
.salt(salt)
.privateKey(privateKey)
.clientPublicKey(clientPublicKey)
.build();
redisTemplate.opsForValue()
.set(sessionId, JSONObject.toJSON(connectDTO),
globalConfig.getConnectExpirationTime(), globalConfig.getConnectExpirationTimeUNIT());
connectDTO = ConnectDTO.builder()
.secretKey(secretKey)
.sessionId(sessionId)
.salt(salt)
.publicKey(publicKey)
.clientPublicKey(clientPublicKey)
.build();
return connectDTO;
} else {
throw new CustomException(AjaxStatus.FAILED_COMMUNICATION);
}
}
private static final SecureRandom RANDOM = new SecureRandom();
public static String generateSessionId() {
byte[] bytes = new byte[32];
RANDOM.nextBytes(bytes);
return Base64.getEncoder().encodeToString(bytes);
}
}
工具类
AES 对称加密工具类
package com.fir.gateway.utils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.binary.Base64;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
/**
* 功能:AES 工具类
* 说明:对称分组密码算法
* @author fir
* @date 2020-5-20 11:25
*/
@Slf4j
@SuppressWarnings("all")
public class AESUtils {
private static final Logger logger = LoggerFactory.getLogger(AESUtils.class);
public final static String KEY_ALGORITHMS = "AES";
public final static int KEY_SIZE = 128;
/**
* 生成AES密钥,base64编码格式 (128)
* @return
* @throws Exception
*/
public static String getKeyAES_128() throws Exception{
KeyGenerator keyGen = KeyGenerator.getInstance(KEY_ALGORITHMS);
keyGen.init(KEY_SIZE);
SecretKey key = keyGen.generateKey();
String base64str = Base64.encodeBase64String(key.getEncoded());
return base64str;
}
/**
* 生成AES密钥,base64编码格式 (256)
* @return
* @throws Exception
*/
public static String getKeyAES_256() throws Exception{
// 256需要换jar包暂时用128
String base64str = getKeyAES_128();
return base64str;
}
/**
* 根据base64Key获取SecretKey对象
* @param base64Key
* @return
*/
public static SecretKey loadKeyAES(String base64Key) {
byte[] bytes = Base64.decodeBase64(base64Key);
SecretKeySpec secretKeySpec = new SecretKeySpec(bytes, KEY_ALGORITHMS);
return secretKeySpec;
}
/**
* 生成SecretKey, base64对象
*
* @return
*/
public static String generateKeyAES() {
String keyBase64Str = null;
String base64Key = "";
try {
base64Key = AESUtils.getKeyAES_128();
} catch (Exception e) {
e.printStackTrace();
log.error("AES密钥生成失败");
}
if(!base64Key.equals("")){
byte[] bytes = Base64.decodeBase64(base64Key);
SecretKeySpec secretKeySpec = new SecretKeySpec(bytes, KEY_ALGORITHMS);
keyBase64Str = Base64.encodeBase64String(secretKeySpec.getEncoded());
}
return keyBase64Str;
}
/**
* AES 加密字符串,SecretKey对象
*
* @param encryptData
* @param key
* @param encode
* @return
*/
public static String encrypt(String encryptData, SecretKey key, String encode) {
try {
final Cipher cipher = Cipher.getInstance(KEY_ALGORITHMS);
cipher.init(Cipher.ENCRYPT_MODE, key);
byte[] encryptBytes = encryptData.getBytes(encode);
byte[] result = cipher.doFinal(encryptBytes);
return Base64.encodeBase64String(result);
} catch (Exception e) {
logger.error("加密异常:" + e.getMessage());
return null;
}
}
/**
* AES 加密字符串,base64Key对象
*
* @param encryptData
* @param base64Key
* @param encode
* @return
*/
public static String encrypt(String encryptData, String base64Key, String encode) {
SecretKey key = loadKeyAES(base64Key);
try {
final Cipher cipher = Cipher.getInstance(KEY_ALGORITHMS);
cipher.init(Cipher.ENCRYPT_MODE, key);
byte[] encryptBytes = encryptData.getBytes(encode);
byte[] result = cipher.doFinal(encryptBytes);
return Base64.encodeBase64String(result);
} catch (Exception e) {
logger.error("加密异常:" + e.getMessage());
return null;
}
}
/**
* AES 加密字符串,base64Key对象
*
* @param encryptData
* @param base64Key
* @return
*/
public static String encrypt(String encryptData, String base64Key) {
SecretKey key = loadKeyAES(base64Key);
try {
final Cipher cipher = Cipher.getInstance(KEY_ALGORITHMS);
cipher.init(Cipher.ENCRYPT_MODE, key);
byte[] encryptBytes = encryptData.getBytes(String.valueOf(StandardCharsets.UTF_8));
byte[] result = cipher.doFinal(encryptBytes);
return Base64.encodeBase64String(result);
} catch (Exception e) {
logger.error("加密异常:" + e.getMessage());
return null;
}
}
/**
* AES 解密字符串,SecretKey对象
*
* @param decryptData
* @param key
* @param encode
* @return
*/
public static String decrypt(String decryptData, SecretKey key, String encode) {
try {
final Cipher cipher = Cipher.getInstance(KEY_ALGORITHMS);
cipher.init(Cipher.DECRYPT_MODE, key);
byte[] decryptBytes = Base64.decodeBase64(decryptData);
byte[] result = cipher.doFinal(decryptBytes);
return new String(result, encode);
} catch (Exception e) {
logger.error("加密异常:" + e.getMessage());
return null;
}
}
/**
* AES 解密字符串,base64Key对象
*
* @param decryptData
* @param base64Key
* @param encode
* @return
*/
public static String decrypt(String decryptData, String base64Key, String encode) {
SecretKey key = loadKeyAES(base64Key);
try {
final Cipher cipher = Cipher.getInstance(KEY_ALGORITHMS);
cipher.init(Cipher.DECRYPT_MODE, key);
byte[] decryptBytes = Base64.decodeBase64(decryptData);
byte[] result = cipher.doFinal(decryptBytes);
return new String(result, encode);
} catch (Exception e) {
logger.error("加密异常:" + e.getMessage());
return null;
}
}
/**
* AES 解密字符串,base64Key对象
*
* @param decryptData
* @param base64Key
* @return
*/
public static String decrypt(String decryptData, String base64Key) {
SecretKey key = loadKeyAES(base64Key);
try {
final Cipher cipher = Cipher.getInstance(KEY_ALGORITHMS);
cipher.init(Cipher.DECRYPT_MODE, key);
byte[] decryptBytes = Base64.decodeBase64(decryptData);
byte[] result = cipher.doFinal(decryptBytes);
return new String(result, String.valueOf(StandardCharsets.UTF_8));
} catch (Exception e) {
logger.error("解密异常:" + e.getMessage());
return null;
}
}
}
MD5工具类
package com.fir.gateway.utils;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
/**
* @author fir
*/
public class MD5Utils {
private static MessageDigest md;
static {
try {
//初始化摘要对象
md = MessageDigest.getInstance("md5");
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
}
/**
* 获得字符串的md5值
*
* @param str 字符串
* @return MD5值
*/
public static String generateMd5ForString(String str){
//更新摘要数据
md.update(str.getBytes());
//生成摘要数组
byte[] digest = md.digest();
//清空摘要数据,以便下次使用
md.reset();
return formatByteArrayToString(digest);
}
/**
* 获得文件的md5值
*
* @param file 文件对象
* @return 文件MD5值
* @throws IOException 文件流读取异常
*/
public static String generateMd5ForFile(File file) throws IOException {
//创建文件输入流
FileInputStream fis = new FileInputStream(file);
//将文件中的数据写入md对象
byte[] buffer = new byte[1024];
int len;
while ((len = fis.read(buffer)) != -1) {
md.update(buffer, 0, len);
}
fis.close();
//生成摘要数组
byte[] digest = md.digest();
//清空摘要数据,以便下次使用
md.reset();
return formatByteArrayToString(digest);
}
/**
* 将摘要字节数组转换为md5值
*
* @param digest 字节数组
* @return MD5数值
*/
public static String formatByteArrayToString(byte[] digest) {
//创建sb用于保存md5值
StringBuilder sb = new StringBuilder();
int temp;
for (byte b : digest) {
//将数据转化为0到255之间的数据
temp = b & 0xff;
if (temp < 16) {
sb.append(0);
}
//Integer.toHexString(temp)将10进制数字转换为16进制
sb.append(Integer.toHexString(temp));
}
return sb.toString();
}
}
RSA非对称加密工具类
package com.fir.gateway.utils;
import cn.hutool.core.io.file.FileReader;
import cn.hutool.core.io.file.FileWriter;
import cn.hutool.crypto.SecureUtil;
import cn.hutool.crypto.asymmetric.KeyType;
import cn.hutool.crypto.asymmetric.RSA;
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.binary.Base64;
import sun.misc.BASE64Encoder;
import java.nio.charset.StandardCharsets;
import java.security.*;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.HashMap;
import java.util.Map;
/**
* RSA非对称加密工具方法
*
* @author fir
*/
@Slf4j
public class RSAUtils {
private static final String SIGNATURE_ALGORITHM = "SHA256withRSA";
/**
* 类型
*/
public static final String ENCRYPT_TYPE = "RSA";
/**
* 公钥名称
*/
public static final String PUBLIC_KEY = "publicKey";
/**
* 私钥名称
*/
public static final String PRIVATE_KEY = "privateKey";
/**
* 生成公钥私钥对
*
* @return 公钥私钥对
*/
public static Map<String, String> generateKey() {
Map<String, String> map = new HashMap<>();
KeyPair pair = SecureUtil.generateKeyPair(ENCRYPT_TYPE);
PrivateKey privateKey = pair.getPrivate();
PublicKey publicKey = pair.getPublic();
String publicKeyStr = Base64.encodeBase64String(publicKey.getEncoded());
String privateKeyStr = Base64.encodeBase64String(privateKey.getEncoded());
map.put(PUBLIC_KEY, publicKeyStr);
map.put(PRIVATE_KEY, privateKeyStr);
return map;
}
/**
* 从文件中读取公钥
*
* @param filename 公钥保存路径
* @return 公钥字符串
*/
public static String getPublicKey(String filename) {
//默认UTF-8编码,可以在构造中传入第二个参数做为编码
FileReader fileReader = new FileReader(filename);
return fileReader.readString();
}
/**
* 从文件中读取密钥
*
* @param filename 私钥保存路径
* @return 私钥字符串
*/
public static String getPrivateKey(String filename) {
//默认UTF-8编码,可以在构造中传入第二个参数做为编码
FileReader fileReader = new FileReader(filename);
return fileReader.readString();
}
/**
* 公钥加密
*
* @param content 要加密的内容
* @param publicKey 公钥
*/
public static String encrypt(String content, PublicKey publicKey) {
try {
RSA rsa = new RSA(null, publicKey);
return rsa.encryptBase64(content, KeyType.PublicKey);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* 公钥加密
*
* @param content 要加密的内容
* @param publicKey 公钥(base64字符串)
*/
public static String encrypt(String content, String publicKey) {
try {
RSA rsa = new RSA(null, publicKey);
return rsa.encryptBase64(content, KeyType.PublicKey);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* 公钥加密-分段加密(解密时需要分段解密)
*
* @param t 要加密的内容(泛型对象,转化为Json字符串加密)
* @param publicKey 公钥(base64字符串)
*/
public static <T> String encryptSection(T t, String publicKey) {
String content = JSONObject.toJSONString(t);
try {
return encryptSection(content, publicKey);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* 公钥加密-分段加密(解密时需要分段解密)
*
* @param content 要加密的内容
* @param publicKey 公钥(base64字符串)
*/
public static String encryptSection(String content, String publicKey) {
try {
RSA rsa = new RSA(null, publicKey);
int blockSize = 117;
int encryptedLength = content.length();
StringBuilder decryptedBlocks = new StringBuilder();
// 拆分加密文本为块并逐个解密
for (int i = 0; i < encryptedLength; i += blockSize) {
int b = i + blockSize;
if (b > encryptedLength) {
b = encryptedLength;
}
String block = content.substring(i, b);
String decryptedBlock = rsa.encryptBase64(block, KeyType.PublicKey);
decryptedBlocks.append(decryptedBlock);
}
return decryptedBlocks.toString();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* 私钥解密
*
* @param content 要解密的内容
* @param privateKey 私钥
*/
public static String decrypt(String content, PrivateKey privateKey) {
try {
RSA rsa = new RSA(privateKey, null);
return rsa.decryptStr(content, KeyType.PrivateKey);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* 私钥解密
*
* @param content 要解密的内容
* @param privateKey 私钥(base64字符串)
*/
public static String decrypt(String content, String privateKey) {
try {
RSA rsa = new RSA(privateKey, null);
return rsa.decryptStr(content, KeyType.PrivateKey);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* 私钥解密-(只能解密分段解密的数据)
*
* @param content 要解密的内容 (只能解密分段解密的数据)
* @param privateKey 私钥(base64字符串)
*/
public static String decryptSection(String content, String privateKey) {
try {
RSA rsa = new RSA(privateKey, null);
int blockSize = 172;
int encryptedLength = content.length();
StringBuilder decryptedBlocks = new StringBuilder();
// 拆分加密文本为块并逐个解密
for (int i = 0; i < encryptedLength; i += blockSize) {
int b = i + blockSize;
if (b > encryptedLength) {
b = encryptedLength;
}
String block = content.substring(i, b);
String decryptedBlock = rsa.decryptStr(block, KeyType.PrivateKey);
decryptedBlocks.append(decryptedBlock);
}
return decryptedBlocks.toString();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* 获取公私钥-请获取一次后保存公私钥使用
*
* @param publicKeyFilename 公钥生成的路径
* @param privateKeyFilename 私钥生成的路径
*/
public static void generateKeyPair(String publicKeyFilename, String privateKeyFilename) {
try {
KeyPair pair = SecureUtil.generateKeyPair(ENCRYPT_TYPE);
PrivateKey privateKey = pair.getPrivate();
PublicKey publicKey = pair.getPublic();
// 获取 公钥和私钥 的 编码格式(通过该 编码格式 可以反过来 生成公钥和私钥对象)
byte[] pubEncBytes = publicKey.getEncoded();
byte[] priEncBytes = privateKey.getEncoded();
// 把 公钥和私钥 的 编码格式 转换为 Base64文本 方便保存
String pubEncBase64 = new BASE64Encoder().encode(pubEncBytes);
String priEncBase64 = new BASE64Encoder().encode(priEncBytes);
FileWriter pub = new FileWriter(publicKeyFilename);
FileWriter pri = new FileWriter(privateKeyFilename);
pub.write(pubEncBase64);
pri.write(priEncBase64);
} catch (Exception e) {
e.printStackTrace();
}
}
// - - - - - - - - - - - - - - - - - - - - SIGN 签名,验签 - - - - - - - - - - - - - - - - - - - - //
/**
* 加签:生成报文签名
*
* @param content 报文内容
* @param privateKey 私钥
* @param encode 编码
* @return 签名
*/
public static String doSign(String content, String privateKey, String encode) {
try {
String unSign = Base64.encodeBase64String(content.getBytes(StandardCharsets.UTF_8));
byte[] privateKeys = Base64.decodeBase64(privateKey.getBytes());
PKCS8EncodedKeySpec privateKeySpec = new PKCS8EncodedKeySpec(privateKeys);
KeyFactory mykeyFactory = KeyFactory.getInstance(ENCRYPT_TYPE);
PrivateKey psbcPrivateKey = mykeyFactory.generatePrivate(privateKeySpec);
Signature signature = Signature.getInstance(SIGNATURE_ALGORITHM);
signature.initSign(psbcPrivateKey);
signature.update(unSign.getBytes(encode));
byte[] signed = signature.sign();
return Base64.encodeBase64String(signed);
} catch (Exception e) {
log.error("生成报文签名出现异常");
}
return null;
}
/**
* 验证:验证签名信息
*
* @param content 签名报文
* @param signed 签名信息
* @param publicKey 公钥
* @param encode 编码格式
* @return 通过/失败
*/
public static boolean doCheck(String content, String signed, PublicKey publicKey, String encode) {
try {
// 解密之前先把content明文,进行base64转码
String unsigned = Base64.encodeBase64String(content.getBytes(encode));
Signature signature = Signature.getInstance(SIGNATURE_ALGORITHM);
signature.initVerify(publicKey);
signature.update(unsigned.getBytes(encode));
return signature.verify(Base64.decodeBase64(signed));
} catch (Exception e) {
log.error("报文验证签名出现异常");
}
return false;
}
}
加密盐工具类
package com.fir.gateway.utils;
import org.springframework.security.crypto.bcrypt.BCrypt;
import java.security.SecureRandom;
/**
* 盐加密函数函数
*
* @author fir
* @date 2023/7/13 21:19
*/
public class SaltedHashUtils {
/**
* 盐的长度
*/
private static final int SALT_LENGTH = 16;
/**
* 盐值生成
*
* @return 16位盐值
*/
public static String generateSalt() {
SecureRandom secureRandom = new SecureRandom();
byte[] salt = new byte[SALT_LENGTH];
secureRandom.nextBytes(salt);
return bytesToHex(salt);
}
/**
* 密码加盐
*
* @param password 密码
* @param salt 盐值
* @return 加盐数值
*/
public static String generateHash(String password, String salt) {
String saltedPassword = salt + password;
return BCrypt.hashpw(saltedPassword, BCrypt.gensalt());
}
/**
* 密码,盐 对比 盐后数值 是否相同
*
* @param password 密码
* @param salt 盐值
* @return 相同:true/不相同:false
*/
public static boolean validatePassword(String password, String salt, String hashedPassword) {
String saltedPassword = salt + password;
return BCrypt.checkpw(saltedPassword, hashedPassword);
}
/**
* 字节转16进制
*
* @param bytes 字节流
* @return 字符串
*/
private static String bytesToHex(byte[] bytes) {
StringBuilder result = new StringBuilder();
for (byte b : bytes) {
result.append(String.format("%02x", b));
}
return result.toString();
}
}
前端
引入jsencrypt
npm install jsencrypt --save
工具类
securityUtils.js
修改securityUtils.js增加加解密,公私钥生成的逻辑。
import crypto from "crypto";
import {JSEncrypt} from "jsencrypt";
const CryptoJS = require('crypto-js');
/** 全局变量配置-start **/
// url白名单设置
const whiteList = [
"/tick/auth/login",
"/k",
"/cn",
]
/** 全局变量配置-end **/
export default {
/**
* 读取信息
*/
get(key) {
return sessionStorage.getItem(key)
},
/**
* 添加信息
*/
set(key, value) {
sessionStorage.setItem(key, value)
},
/**
* 登录之后进行处理
*/
loginDeal(token){
this.set("token", token)
},
//************************************网关通信-start
// 与后台网关建立连接,需要请求 “/k” 接口, 拿到后端的公钥,存储。
// 再请求 “/cn” 接口,保存与后端建立通信所需要的请求。
/**
* 用于网关请求 “/k” 请求后的处理
*
* @returns {{ck: (string|null), k: string}}
*/
dealValidationMessage(data) {
this.set("publicKey", data)
},
/**
* gateway网关验证信息处理(请求头)
*/
gatewayRequest(request) {
let key = true;
whiteList.find(function (value) {
if (value === request.url) {
key = false;
}
});
// 对非白名单请求进行处理
if (key) {
// 请求体数据
let token = this.get("token")
// 请求中增加token
if (token) {
request.headers.Authorization = token;
}
}
return request;
},
/**
* 用于网关请求 “/cn” 请求前的处理
*
* @returns {{ck: (string|null), k: string}}
*/
secureConnectionPrepare() {
const publicKey = this.get("publicKey")
const publicKeyMd5 = this.strToMd5(publicKey)
let clientPublicKey = this.communication()
clientPublicKey = this.rsaEncrypt(clientPublicKey, publicKey)
return {
"k": publicKeyMd5,
"ck": clientPublicKey,
};
},
/**
* 用于网关请求 “/cn” 请求后的处理
*/
secureConnection(data) {
const privateKey = this.get("privateKey")
data = this.rsaDecrypt(data, privateKey)
data = JSON.parse(data)
this.set("secretKey", data.secretKey)
this.set("sessionId", data.sessionId)
this.set("serverPublicKey", data.publicKey)
},
//************************************网关通信-end
/**
* 生成公钥私钥对保存本地,并返回公钥
*
* @returns {string}
*/
communication() {
const keys = this.rsaGenerateKey();
const publicKey = keys.publicKey;
const privateKey = keys.privateKey;
this.set("privateKey", privateKey)
return publicKey
},
//************************************公用加密方法-start
/**
* 将字符串取值MD5
*
* @param string 字符串对象
* @returns {string} 字符串md5数值
*/
strToMd5(string) {
// 规定使用哈希算法中的MD5算法
const hash = crypto.createHash('md5');
// 可任意多次调用update(),效果相当于多个字符串相加
hash.update(string);
// hash.digest('hex')表示输出的格式为16进制
return hash.digest('hex');
},
//************************************公用加密方法-end
//************************************AES对称加解密-start
/**
* AES对称加密数据
*
* @param {String} data 待加密的数据
* @param {String} base64Key base64格式的密钥
* @returns {String} 加密后的数据
*/
encryptAES(data, base64Key) {
let encryptedBytes = null;
if (data != null && base64Key != null) {
const key = CryptoJS.enc.Base64.parse(base64Key);
encryptedBytes = CryptoJS.AES.encrypt(data, key, {mode: CryptoJS.mode.ECB});
encryptedBytes = encryptedBytes.toString();
}
return encryptedBytes;
},
/**
* AES对称-解密数据
*
* @param {String} data 待解密的数据
* @param {String} base64Key base64格式的密钥
* @returns {String} 解密后的数据
*/
decryptAES(data, base64Key) {
let decryptData = null;
if (data != null && base64Key != null) {
const key = CryptoJS.enc.Base64.parse(base64Key)
const decryptBytes = CryptoJS.AES.decrypt(data, key, {mode: CryptoJS.mode.ECB})
decryptData = CryptoJS.enc.Utf8.stringify(decryptBytes);
}
return decryptData
},
//************************************AES对称加解密-end
//************************************RSA非对称加解密-start
/**
* 非对称加解密-生成公钥与私钥
*/
rsaGenerateKey() {
let keys = {
"publicKey": "",
"privateKey": "",
}
// 创建 JSEncrypt 实例
const encrypt = new JSEncrypt();
// 生成密钥对(公钥和私钥)
const keyPair = encrypt.getKey();
// 获取公钥和私钥
keys.publicKey = keyPair.getPublicBaseKeyB64();
keys.privateKey = keyPair.getPrivateBaseKeyB64();
return keys
},
/**
* 非对称加解密-公钥认证信息(分段加密)
*
* @param string 内容
* @param publicKey 非对称私钥
* @returns {string | null}
*/
rsaEncrypt(string, publicKey) {
let encryptData = null;
if (string != null && publicKey != null) {
const encryptor = new JSEncrypt();
encryptor.setPublicKey(publicKey);
// 根据公钥的长度确定块大小,一般为公钥长度减去一些填充长度
const blockSize = 117;
const textLength = string.length;
let encryptedBlocks = [];
// 拆分长文本为块并逐个加密
for (let i = 0; i < textLength; i += blockSize) {
const block = string.substr(i, blockSize);
const encryptedBlock = encryptor.encrypt(block);
encryptedBlocks.push(encryptedBlock);
}
// 将加密的块合并为单个字符串
encryptData = encryptedBlocks.join('');
}
return encryptData;
},
/**
* 非对称加解密-私钥解密信息(分段解密)
*
* @param string 加密内容
* @param privateKey 非对称私钥
* @returns {string | null}
*/
rsaDecrypt(string, privateKey) {
let decryptData = null;
if (string != null && privateKey != null) {
const encryptor = new JSEncrypt();
encryptor.setPrivateKey(privateKey);
// 根据私钥的长度确定块大小,一般为私钥长度减去一些填充长度
const blockSize = 172;
const encryptedLength = string.length;
let decryptedBlocks = [];
// 拆分加密文本为块并逐个解密
for (let i = 0; i < encryptedLength; i += blockSize) {
const block = string.substr(i, blockSize);
const decryptedBlock = encryptor.decrypt(block);
decryptedBlocks.push(decryptedBlock);
}
decryptData = decryptedBlocks.join('')
}
// 将解密的块合并为单个字符串
return decryptData;
},
//************************************RSA非对称加解密-end
}
请求类
增加两个请求,用于访问后端的公钥数据与其他加密数据。
系统通信密钥接口
/** 系统通信密钥 **/
getPublicKey(obj) {
return dataInterface("/k","get", obj)
},
/** 系统通信密钥 **/
cn(obj) {
return dataInterface("/cn","get", obj)
},
登录接口增加认证数据接口
此时我们希望在登陆前,获取到与后端通信的公钥私钥以及其他的认证数据。
/**
* 获取与后端建立通信的必备信息
*/
async loginApi() {
await this.connection()
await this.$http.login(this.login).then(res => {
let code = res.code
let msg = res.msg
let data = res.data
securityUtils.loginDeal(data.token)
this.token = securityUtils.get("token")
if (code === 200) {
this.$message({message: msg, duration: 1.5, description: ''})
} else {
this.$message({message: "错误", duration: 1.5, description: ''})
}
})
},
/** 与后端建立联系 **/
async connection() {
// 获取后端RSA加密公钥
await this.$http.getPublicKey().then(res => {
let data = res.data
securityUtils.dealValidationMessage(data)
});
// 获取与后端建立通信的必备信息
let data = securityUtils.secureConnectionPrepare()
await this.$http.cn(data).then(res => {
let data = res.data
securityUtils.secureConnection(data)
})
},