[Spring Cloud] (5)gateway前后端公私钥与认证信息

发布于:2024-04-30 ⋅ 阅读:(35) ⋅ 点赞:(0)

简述

本文gateway,微服务,vue已开源到gitee
杉极简/gateway网关阶段学习
实现目的:
前端请求后端接口获得到服务端公钥。

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/khttp://localhost:51001/cn接口,之后在进行登录操作,
image.png
image.png

image.png
后端也根据sessionId存储认证信息
image.png

后端

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)
            })
        },