Vue + Springboot 前后端完整使用国密算法 SM2 数据加密 传输 交互 完整解决方案

项目外网部署的时候经常会有要求数据加密传输的情况,特别是企事业单位的项目,另为安全或者红头文件计,经常要求使用国密算法,因为涉及交互,所以使用SM2非对称加密。

后端(Springboot)

(1)所需主要依赖(其他如有缺失自行百度即可):

        <!-- hutool工具类 -->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.3.10</version>
        </dependency>

        <!-- 第三方加密功能依赖 -->
        <dependency>
            <groupId>org.bouncycastle</groupId>
            <artifactId>bcprov-jdk15to18</artifactId>
            <version>1.68</version>
        </dependency>

(2)SM2功能方法的创建

博主这里在后端把相关的加解密功能做成了功能接口,所以这里直接展示service实现类的代码,里边的统一返回参数改成自己的类型即可


import cn.hutool.core.util.CharsetUtil;
import cn.hutool.core.util.HexUtil;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.BCUtil;
import cn.hutool.crypto.SmUtil;
import cn.hutool.crypto.asymmetric.KeyType;
import cn.hutool.crypto.asymmetric.SM2;
import cn.hutool.crypto.symmetric.SymmetricCrypto;
import com.ruoyi.common.constant.EncryptConstant;
import com.ruoyi.common.exception.base.BaseException;
import com.ruoyi.common.utils.encrypt.dto.ApiEncryptInfoDTO;
import com.ruoyi.common.utils.encrypt.dto.Result;
import com.ruoyi.common.utils.encrypt.service.ApiEncryptService;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.bouncycastle.crypto.engines.SM2Engine;
import org.bouncycastle.crypto.params.ECPrivateKeyParameters;
import org.bouncycastle.crypto.params.ECPublicKeyParameters;
import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey;
import org.springframework.stereotype.Service;

/**
 * 加解密相关功能实现类
 *
 * @author gbx
 */
@Service
@Slf4j
public class ApiEncryptServiceImpl implements ApiEncryptService {

    /**
     * SM2加密
     *
     * @param dto 包含加解密相关参数信息的实体
     * @return 处理结果
     */
    @Override
    public Result encrypt2Data(ApiEncryptInfoDTO dto) {
        String publicKey = dto.getPublicKey();
        // 若为空,使用默认
        if (StringUtils.isBlank(publicKey)) {
            publicKey = EncryptConstant.PUBLIC_KEY;
        }
        String data = dto.getData();
        //创建sm2 对象
        SM2 sm2 = getSM2(null, publicKey);
        String dataHex = sm2.encryptBcd(data, KeyType.PublicKey);
        dto.setDataHex(dataHex);
        return new Result().ok(dto);
    }

    /**
     * SM2解密
     *
     * @param dto 包含加解密相关参数信息的实体
     * @return 处理结果
     */
    @Override
    public Result decrypt2Data(ApiEncryptInfoDTO dto) {
        String privateKey = dto.getPrivateKey();
        // 若为空,使用默认
        if (StringUtils.isBlank(privateKey)) {
            privateKey = EncryptConstant.PRIVATE_KEY;
        }
        String dataHex = dto.getDataHex();
        try {
            //创建sm2 对象
            SM2 sm2 = getSM2(privateKey, null);
            String data = StrUtil.utf8Str(sm2.decryptFromBcd(dataHex, KeyType.PrivateKey));
            dto.setData(data);
        } catch (Exception e) {
            log.error("SM2解密失败", e);
            throw new BaseException("SM2解密失败");
        }
        return new Result().ok(dto);
    }

    /**
     * SM4加密
     *
     * @param dto 包含加解密相关参数信息的实体
     * @return 处理结果
     */
    @Override
    public Result encrypt4Data(ApiEncryptInfoDTO dto) {
        //指定的密钥
        String key = dto.getKey();
        // 若为空,使用默认
        if (StringUtils.isBlank(key)) {
            key = EncryptConstant.SM4_KEY;
        }
        String data = dto.getData();
        try {
            SymmetricCrypto sm4 = SmUtil.sm4(key.getBytes(CharsetUtil.CHARSET_UTF_8));
            String dataHex = sm4.encryptHex(data);
            dto.setDataHex(dataHex);
        } catch (Exception e) {
            log.error("加密数据异常,异常数据:" + data, e);
            throw new BaseException("SM4加密数据异常");
        }
        return new Result().ok(dto);
    }

    /**
     * SM4解密
     *
     * @param dto 包含加解密相关参数信息的实体
     * @return 处理结果
     */
    @Override
    public Result decrypt4Data(ApiEncryptInfoDTO dto) {
        //指定的密钥
        String key = dto.getKey();
        // 若为空,使用默认
        if (StringUtils.isBlank(key)) {
            key = EncryptConstant.SM4_KEY;
        }
        String dataHex = dto.getDataHex();
        try {
            SymmetricCrypto sm4 = SmUtil.sm4(key.getBytes(CharsetUtil.CHARSET_UTF_8));
            String data = sm4.decryptStr(dataHex);
            dto.setData(data);
        } catch (Exception e) {
            log.error("解密数据异常,异常数据:" + dataHex, e);
            throw new BaseException("SM4解密数据异常");
        }
        return new Result().ok(dto);
    }

    /**
     * 生成一对 C1C2C3 格式的SM2密钥
     *
     * @return 处理结果
     */
    @Override
    public Result getSM2Key() {
        ApiEncryptInfoDTO dto = new ApiEncryptInfoDTO();
        //创建sm2 对象
        SM2 sm2 = SmUtil.sm2();
        byte[] privateKeyByte = BCUtil.encodeECPrivateKey(sm2.getPrivateKey());
        //这里公钥不压缩  公钥的第一个字节用于表示是否压缩  可以不要
        byte[] publicKeyByte = ((BCECPublicKey) sm2.getPublicKey()).getQ().getEncoded(false);
        try {
            String privateKey = HexUtil.encodeHexStr(privateKeyByte);
            String publicKey = HexUtil.encodeHexStr(publicKeyByte);
            dto.setPublicKey(publicKey);
            dto.setPrivateKey(privateKey);
        } catch (Exception e) {
            log.error("获取SM2密钥出错", e);
            throw new BaseException("获取SM2密钥出错");
        }
        return new Result().ok(dto);
    }

    /**
     * 获取SM2加密工具对象
     *
     * @param privateKey 加密私钥
     * @param publicKey  加密公钥
     * @return 处理结果
     */
    private SM2 getSM2(String privateKey, String publicKey) {
        ECPrivateKeyParameters ecPrivateKeyParameters = null;
        ECPublicKeyParameters ecPublicKeyParameters = null;
        if (StringUtils.isNotBlank(privateKey)) {
            ecPrivateKeyParameters = BCUtil.toSm2Params(privateKey);
        }
        if (StringUtils.isNotBlank(publicKey)) {
            if (publicKey.length() == 130) {
                //这里需要去掉开始第一个字节 第一个字节表示标记
                publicKey = publicKey.substring(2);
            }
            String xhex = publicKey.substring(0, 64);
            String yhex = publicKey.substring(64, 128);
            ecPublicKeyParameters = BCUtil.toSm2Params(xhex, yhex);
        }
        //创建sm2 对象
        SM2 sm2 = new SM2(ecPrivateKeyParameters, ecPublicKeyParameters);
        sm2.usePlainEncoding();
        sm2.setMode(SM2Engine.Mode.C1C2C3);
        return sm2;
    }

    /**
     * 获取一个随机的SM4密钥
     *
     * @return 处理结果
     */
    @Override
    public Result getSM4Key() {
        String sm4Key = RandomUtil.randomString(RandomUtil.BASE_CHAR_NUMBER, 16);
        ApiEncryptInfoDTO dto = new ApiEncryptInfoDTO();
        dto.setKey(sm4Key);
        return new Result().ok(dto);
    }
}

储存数据信息的类(这里使用了lombok的依赖,如果没有的话自己加上get set)

package com.ruoyi.common.utils.encrypt.dto;

import lombok.Data;
import lombok.EqualsAndHashCode;


/**
 * 数据加密信息DTO
 *
 * @author gbx
 * @since 2021-05-27 10:55:32
 */
@Data
@EqualsAndHashCode(callSuper = false)
public class ApiEncryptInfoDTO implements Serializable {
    private static final long serialVersionUID = 693123123935846450L;

    /**
     * 加密类型(2:sm2加密,4:sm4加密)
     */
    private String type;

    /**
     * 非对称加密私钥
     */
    private String privateKey;

    /**
     * 非对称加密公钥
     */
    private String publicKey;

    /**
     * 对称加密密钥
     */
    private String key;

    /**
     * 原始数据
     */
    private String data;

    /**
     * 加密后数据
     */
    private String dataHex;

    /**
     * 非对称加密签名
     */
    private String sign;
}

工具类


import com.ruoyi.common.utils.encrypt.dto.ApiEncryptInfoDTO;
import com.ruoyi.common.utils.encrypt.service.ApiEncryptService;

/**
 * 加解密工具类
 *
 * @author gbx
 */
public class EncryptUtils {

    /**
     * 使用SM2密钥解密数据
     *
     * @param sm2DataHex 密文数据
     * @return 明文数据
     */
    public static String getSm2Data(String sm2DataHex, ApiEncryptService encryptService, String privateKey) {
        ApiEncryptInfoDTO dto = new ApiEncryptInfoDTO();
        dto.setPrivateKey(privateKey);
        dto.setDataHex(sm2DataHex);
        dto.setType("2");
        encryptService.decrypt2Data(dto);
        return dto.getData();
    }

    /**
     * 使用SM2密钥加密数据
     *
     * @param sm2Data 明文数据
     * @return 密文数据
     */
    public static String getSm2DataHex(String sm2Data, ApiEncryptService encryptService, String publicKey) {
        ApiEncryptInfoDTO dto = new ApiEncryptInfoDTO();
        dto.setPublicKey(publicKey);
        dto.setData(sm2Data);
        dto.setType("2");
        encryptService.encrypt2Data(dto);
        return dto.getDataHex();
    }

    /**
     * 使用SM4密钥解密数据
     *
     * @param sm4DataHex 密文数据
     * @return 明文数据
     */
    public static String getSm4Data(String sm4DataHex, ApiEncryptService encryptService, String key) {
        ApiEncryptInfoDTO dto = new ApiEncryptInfoDTO();
        dto.setKey(key);
        dto.setDataHex(sm4DataHex);
        dto.setType("4");
        encryptService.decrypt4Data(dto);
        return dto.getData();
    }

    /**
     * 使用SM4密钥加密数据
     *
     * @param sm4Data 明文数据
     * @return 密文数据
     */
    public static String getSm4DataHex(String sm4Data, ApiEncryptService encryptService, String key) {
        ApiEncryptInfoDTO dto = new ApiEncryptInfoDTO();
        dto.setKey(key);
        dto.setData(sm4Data);
        dto.setType("4");
        encryptService.encrypt4Data(dto);
        return dto.getDataHex();
    }
}

至此,引入功能接口,即可使用对应的加解密方法。

(3)对springboot后端指定的接口使用sm2解密请求数据,加密响应数据

ps:SM2是非对称算法,公私密钥有一对,调用之前的生成SM2密钥对的功能,生成两对密钥,一对后端使用,一对前端使用,双方各自把自己的公钥给予对方,让对方给自己传输时使用自己的公钥加密。

后端这里主要是通过添加拦截器对指定路径接口进行拦截的方式实现对前端数据的加解密的,这里前端和后端约定所有的数据(对象转为json字符串)加密后以 “data” 为key进行传输,如:

{"data": "K3KD89ASK2JKN8923JNKJF"}

这里的需要拦截加解密的接口路径是配置在yml文件内的,通过静态常量初始化类进行加载,后续可能会在之后博文中讲解具体方法,此处暂不涉及。

具体拦截器设置
(a)请求拦截解密


import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.ruoyi.common.exception.base.BaseException;
import com.ruoyi.common.utils.encrypt.service.ApiEncryptService;
import com.ruoyi.common.utils.encrypt.utils.EncryptDefaultUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.http.MediaType;

import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.*;
import java.net.URLDecoder;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.Vector;

/**
 * 请求参数解密处理
 *
 * @author gbx
 */
@Slf4j
public class DataEncryptionWrapper extends HttpServletRequestWrapper {
    private ApiEncryptService encryptService;
    private final ObjectMapper objectMapper = new ObjectMapper();
    private String body = "";

    /**
     * 存储param,formdata,body等参数,用以获取参数的重新
     */
    private Map params = new HashMap();

    /**
     * 统一加密请求参数的键
     */
    private static final String REQ_BODY_KEY = "data";

    public DataEncryptionWrapper(HttpServletRequest request, ApiEncryptService encryptService) throws IOException {
        super(request);
        this.encryptService = encryptService;
        String contentType = request.getContentType();
        if (org.apache.commons.lang3.StringUtils.isNotBlank(contentType)
                && (org.springframework.util.StringUtils.substringMatch(contentType, 0, MediaType.APPLICATION_FORM_URLENCODED_VALUE))
        ) {
            Map parametersMap = request.getParameterMap();
            if (parametersMap.containsKey(REQ_BODY_KEY)) {
                String data = ((String[]) parametersMap.get(REQ_BODY_KEY))[0];
                String deJson = EncryptDefaultUtils.getSm2Data(data, encryptService);
                this.params.putAll(objectMapper.readValue(deJson, Map.class));
            }
            //将其他form中的放进去
            for (Object key : parametersMap.keySet()) {
                if (!key.equals(REQ_BODY_KEY)) {
                    this.params.put(key, parametersMap.get(key));
                }
            }
        } else if (org.apache.commons.lang3.StringUtils.isNotBlank(contentType)
                && org.springframework.util.StringUtils.substringMatch(contentType, 0, MediaType.APPLICATION_JSON_VALUE)) {
            StringBuilder stringBuilder = new StringBuilder();
            BufferedReader bufferedReader = null;
            try {
                InputStream inputStream = request.getInputStream();
                if (inputStream != null) {
                    bufferedReader = new BufferedReader(new InputStreamReader(inputStream, "UTF-8"));
                    char[] charBuffer = new char[128];
                    int bytesRead = -1;
                    while ((bytesRead = bufferedReader.read(charBuffer)) > 0) {
                        stringBuilder.append(charBuffer, 0, bytesRead);
                    }
                }
            } catch (IOException ex) {
                throw ex;
            } finally {
                if (bufferedReader != null) {
                    try {
                        bufferedReader.close();
                    } catch (IOException ex) {
                        throw ex;
                    }
                }
            }

            String requestURI = request.getRequestURI();
            //获取请求参数
            String queryString = request.getQueryString();
            log.info("====》 请求路径:" + requestURI + ",原请求query参数:{}", queryString);

            String bodyStr = stringBuilder.toString();
            bodyStr = bodyStr.startsWith("[") && bodyStr.endsWith("]") ? bodyStr.substring(1, bodyStr.length() - 1) : bodyStr;
            if (StringUtils.isNotBlank(bodyStr)) {
                JSONObject jsonObject = JSONObject.parseObject(bodyStr);
                if (jsonObject != null) {
                    //获取请求body
                    log.info("====》 请求路径:" + requestURI + ",原请求body参数体:{}", JSONObject.toJSONString(jsonObject, SerializerFeature.WriteMapNullValue));
                    String enJson = jsonObject.getString(REQ_BODY_KEY);
                    if (org.apache.commons.lang3.StringUtils.isNotBlank(enJson)) {
                        String deJson = EncryptDefaultUtils.getSm2Data(enJson, encryptService);
                        body = deJson.startsWith("[") && deJson.endsWith("]") ? deJson.substring(1, deJson.length() - 1) : deJson;
                        this.params.putAll(objectMapper.readValue(body, Map.class));
                    }
                }
            }
        }

        // RequestDispatcher.forward parameter
        renewParameterMap(request);
    }

    @Override
    public String getParameter(String name) {
        String result = "";

        Object v = params.get(name);
        if (v == null && StringUtils.isNotBlank(body)) {
            JSONObject jsonObject = JSONObject.parseObject(body);
            v = jsonObject.get(name);
        }
        if (v == null) {
            result = null;
        } else if (v instanceof String[]) {
            String[] strArr = (String[]) v;
            if (strArr.length > 0) {
                try {
                    result = URLDecoder.decode(strArr[0], "utf-8");
                } catch (UnsupportedEncodingException e) {
                    e.printStackTrace();
                }
            } else {
                result = null;
            }
        } else if (v instanceof String) {
            result = (String) v;
            try {
                result = URLDecoder.decode(result, "utf-8");
            } catch (UnsupportedEncodingException e) {
                e.printStackTrace();
            }
        } else {
            result = v.toString();
        }
        return result;
    }

    @Override
    public Map getParameterMap() {
        return params;
    }

    @Override
    public Enumeration<String> getParameterNames() {
        return new Vector<String>(params.keySet()).elements();
    }

    @Override
    public String[] getParameterValues(String name) {
        String[] result = null;
        Object v = params.get(name);
        if (v == null && StringUtils.isNotBlank(body)) {
            JSONObject jsonObject = JSONObject.parseObject(body);
            v = jsonObject.get(name);
        }
        if (v == null) {
            result = null;
        } else if (v instanceof String[]) {
            result = (String[]) v;
            for (int i = 0; i < result.length; i++) {
                try {
                    result[i] = URLDecoder.decode(result[i], "utf-8");
                } catch (UnsupportedEncodingException e) {
                    e.printStackTrace();
                }
                //增加解密param参数的操作
                if (StringUtils.isNotBlank(result[i])) {
                    try {
                        result[i] = EncryptDefaultUtils.getSm2Data(result[i], encryptService);
                    } catch (Exception e) {
                        throw new BaseException("解密param请求参数失败");
                    }
                }
            }
        } else if (v instanceof String) {
            try {
                result = new String[]{URLDecoder.decode((String) v, "utf-8")};
            } catch (UnsupportedEncodingException e) {
                e.printStackTrace();
            }
            //增加解密param参数的操作
            if (StringUtils.isNotBlank(result[0])) {
                try {
                    result[0] = EncryptDefaultUtils.getSm2Data(result[0], encryptService);
                } catch (Exception e) {
                    throw new BaseException("解密param请求参数失败");
                }
            }
        } else {
            try {
                result = new String[]{URLDecoder.decode(v.toString(), "utf-8")};
            } catch (UnsupportedEncodingException e) {
                e.printStackTrace();
            }
            //增加解密param参数的操作
            if (StringUtils.isNotBlank(result[0])) {
                try {
                    result[0] = EncryptDefaultUtils.getSm2Data(result[0], encryptService);
                } catch (Exception e) {
                    throw new BaseException("解密param请求参数失败");
                }
            }
        }
        return result;
    }

    @Override
    public ServletInputStream getInputStream() throws IOException {
        final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(body.getBytes("UTF-8"));
        ServletInputStream servletInputStream = new ServletInputStream() {
            @Override
            public boolean isFinished() {
                return false;
            }

            @Override
            public boolean isReady() {
                return false;
            }

            @Override
            public void setReadListener(ReadListener readListener) {

            }

            @Override
            public int read() throws IOException {
                return byteArrayInputStream.read();
            }
        };
        return servletInputStream;
    }

    @Override
    public BufferedReader getReader() throws IOException {
        return new BufferedReader(new InputStreamReader(this.getInputStream(), "UTF-8"));
    }

    private void renewParameterMap(HttpServletRequest req) {

        String queryString = req.getQueryString();

        if (queryString != null && queryString.trim().length() > 0) {
            String[] params = queryString.split("&");

            for (int i = 0; i < params.length; i++) {
                int splitIndex = params[i].indexOf("=");
                if (splitIndex == -1) {
                    continue;
                }

                String key = params[i].substring(0, splitIndex);

                if (!this.params.containsKey(key)) {
                    if (splitIndex < params[i].length()) {
                        String value = params[i].substring(splitIndex + 1);
                        try {
                            this.params.put(key, new String[]{URLDecoder.decode(value, "utf-8")});
                        } catch (UnsupportedEncodingException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        }
    }
}


import com.ruoyi.common.utils.encrypt.service.ApiEncryptService;
import com.ruoyi.common.utils.interceptor.EncryptInterceptorUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.web.servlet.DispatcherServlet;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpServletResponse;

/**
 * 自定义 DispatcherServlet 来分派 GbxHttpServletRequestWrapper
 *
 * @author gbx
 * @date 2021-08-30 14:25
 */
public class GbxDispatcherServlet extends DispatcherServlet {

    /**
     * 加密功能服务接口类
     */
    private ApiEncryptService encryptService;

    public GbxDispatcherServlet() {
    }

    public GbxDispatcherServlet(ApiEncryptService encryptService) {
        this.encryptService = encryptService;
    }

    /**
     * 包装自定义的 request
     *
     * @param request
     * @param response
     * @throws Exception
     */
    @Override
    protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
        String requestURI = request.getRequestURI();
        // 对符合要求的指定的请求路径接口进行解密操作
        if (StringUtils.isNotBlank(requestURI) && EncryptInterceptorUtils.checkApi(requestURI)) {
            super.doDispatch(new DataEncryptionWrapper(request, encryptService), response);
        } else {
            super.doDispatch(new HttpServletRequestWrapper(request), response);
        }

    }
}


import com.ruoyi.common.utils.encrypt.service.ApiEncryptService;
import com.ruoyi.framework.interceptor.request.GbxDispatcherServlet;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * WebMVC配置,集中配置拦截器、过滤器、静态资源缓存
 *
 * @author gbx
 * @date 2021-08-30 10:15
 */
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
    @Autowired
    private ApiEncryptService encryptService;

    /**
     * 使用自定义的请求转发
     *
     * @return
     */
    @Bean
    @Qualifier(DispatcherServletAutoConfiguration.DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)
    public DispatcherServlet dispatcherServlet() {
        return new GbxDispatcherServlet(encryptService);
    }
}

至此,通过此拦截器的数据到达 Controller Api 接口时,已经是正常的解密后的数据了,可以在相关接口内对参数进行日志打印,查看具体参数。

(b)响应拦截加密

响应参数加密比较简单,因为使用统一的返回参数体,用body方式返回,所以可以实现Springboot自带的一个返回body参数拦截接口来进行操作


import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.ruoyi.common.constant.EncryptConstant;
import com.ruoyi.common.utils.encrypt.service.ApiEncryptService;
import com.ruoyi.common.utils.encrypt.utils.EncryptUtils;
import com.ruoyi.common.utils.interceptor.EncryptInterceptorUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;

import java.util.HashMap;
import java.util.Map;

/**
 * response 响应参数统一加密处理拦截器
 *
 * @author gbx
 */
@ControllerAdvice
@Slf4j
public class EncryptResponseBodyAdvice implements ResponseBodyAdvice<Object> {
    @Autowired
    private ApiEncryptService encryptService;

    @Override
    public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> aClass) {
        return true;
    }

    @Override
    public Object beforeBodyWrite(Object body, MethodParameter methodParameter, MediaType mediaType, Class<? extends HttpMessageConverter<?>> aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
        Map<String, Object> resMap = new HashMap<>(16);
        try {
            String path = serverHttpRequest.getURI().getPath();
            if (StringUtils.isNotBlank(path) && EncryptInterceptorUtils.checkApi(path)) {
                ObjectMapper objectMapper = new ObjectMapper();
                String json = objectMapper.writeValueAsString(body);
                log.info("====》 响应路径:" + path + ",原实际返回参数:" + json);
                if (StringUtils.isNotBlank(json)) {
                    String dataHex = EncryptUtils.getSm2DataHex(json, encryptService, EncryptConstant.UI_PUBLIC_KEY);
                    resMap.put("data", dataHex);
                }
            }
            log.info("====》 响应路径:" + path + ",实际响应参数:{}", JSONObject.toJSONString(resMap, SerializerFeature.WriteMapNullValue));
        } catch (JsonProcessingException e) {
            e.printStackTrace();
        }

        if (ObjectUtils.isNotEmpty(resMap)) {
            return resMap;
        }
        return body;
    }
}

至此,响应参数加密返回处理完毕。

前端传输,对象统一转为json串,对应"data"为key传输,param参数单独加密传输,如:

/login?validateCode=3JKD3NJKNDKJ3

前端(Vue)

之所以把前后端放一块,是因为前后端的很多设置是可变的,必须配套统一才能正常交互,比如加密策略,这里后端使用了 C1C2C3,则前端也必须使用对应的策略,另外前后端的加解密工具类还因为默认的密钥头不同而需要差异化调整。

前端引入依赖:

npm install --save sm-crypto

因为前端vue基本上使用统一的request.js,在里边的 interceptor 内处理相关参数的加解密比较简单,这里不再赘述,只把容易出问题的前端加解密方法做封装展示

前端统一加解密工具类:

const sm2 = require('sm-crypto').sm2 // 获取sm2对象
const cipherMode = 0 // 选择加密策略,1 - C1C3C2,0 - C1C2C3,默认为1
const sysPublicKey = '你对应的后台的公钥' // 系统后台公钥
const uiPrivateKey = '你自己前端的私钥' // 前端UI私钥

/**
 * SM2加密string数据
 * @param {string} data 原始数据
 * @returns {string} 加密后数据
 */
export function getSm2DataHexByString(data) {
  if (data && (typeof data === 'string') && data.constructor === String) {
    return '04' + sm2.doEncrypt(data, sysPublicKey, cipherMode)
  }
  return null
}

/**
 * SM2加密object数据
 * @param {Object} data 原始数据
 * @returns {string} 加密后数据
 */
export function getSm2DataHexByObject(data) {
  if (data) {
    return '04' + sm2.doEncrypt(JSON.stringify(data), sysPublicKey, cipherMode)
  }
  return null
}

/**
 * SM2解密数据
 * @param {string} dataHex 原始加密数据
 * @returns {string} 解密后数据
 */
export function getSm2DataByString(dataHex) {
  if (dataHex && (typeof dataHex === 'string') && dataHex.constructor === String) {
    dataHex = dataHex.substring(2).toLocaleLowerCase()
    return sm2.doDecrypt(dataHex, uiPrivateKey, cipherMode)
  }
}

至此,前后端国密SM2进行数据加密传输完毕,大多数依赖都是通用依赖,如果还有哪里依赖报红,那就哪里报红搜哪里吧,毕竟咱们面向百度编程嘛(手动滑稽)!

  • 3
    点赞
  • 44
    收藏
    觉得还不错? 一键收藏
  • 19
    评论
### 回答1: vue springboot前后端分离开发实战pdf是一本介绍如何使用Vue和Spring Boot进行前后端分离开发的实战指南。本书详细讲解了如何使用Vue框架搭建前端应用,以及如何利用Spring Boot框架构建后端应用,旨在帮助读者掌握前后端分离开发的技术和方法。 在这本书中,作者首先介绍了前后端分离开发的概念和背景,解释了为什么前后端分离可以带来更好的开发体验和效率。接着,作者详细介绍了Vue框架的基本概念和使用方法,包括组件化开发、路由管理、状态管理等方面的内容。读者可以通过跟随书中的示例代码,逐步学习并实践Vue框架的应用。 在后半部分,作者重点介绍了Spring Boot框架的使用。读者将学习如何使用Spring Boot快速搭建后端应用,并了解如何通过RESTful API与前端应用进行数据交互。此外,作者还介绍了Spring Security、Spring Data JPA等常用的配套技术,帮助读者构建安全可靠的后端应用。 本书不仅提供了理论知识,还提供了大量的实战案例和实例代码。读者可以跟随书中的示例项目逐步实践,从而更好地掌握Vue和Spring Boot的开发技巧。此外,本书还涉及了一些项目管理和部署的内容,帮助读者了解如何将前后端分离的应用部署到生产环境中。 总而言之,vue springboot前后端分离开发实战pdf是一本适合想要学习并实践前后端分离开发的开发人员的实用指南,通过学习本书,读者将获得丰富的知识和经验,能够独立设计和开发前后端分离的应用程序。 ### 回答2: 《Vue SpringBoot前后端分离开发实战PDF》这本书是一本关于前后端分离开发实践的指南。它结合了VueSpringBoot两个流行的开发框架,帮助开发者更好地理解和应用这两个技术。 在书中,作者首先介绍了前后端分离的概念和优势。前后端分离开发可以提高开发效率和协作性,同时也能提供更好的性能和扩展性。然后,作者详细介绍了Vue框架的基本知识和使用方法,包括Vue的搭建、组件的创建和组织等。读者可以通过实际的案例来学习和练习。 接着,作者转向SpringBoot框架的介绍和使用SpringBoot是一个轻量级的Java开发框架,可以快速构建和部署应用程序。作者讲解了如何使用SpringBoot创建RESTful API,以及如何与Vue前端进行交互。 在书的后半部分,作者提供了一些实战案例和示例代码。这些案例涵盖了常见的前后端分离开发场景,如用户管理、权限控制、数据交互等。通过这些案例,读者可以了解到如何将VueSpringBoot无缝地结合起来,构建强大的应用程序。 总的来说,《Vue SpringBoot前后端分离开发实战PDF》是一本非常实用的书籍。它不仅系统地介绍了VueSpringBoot的基础知识和使用方法,还提供了丰富的实战经验和案例。对于想要掌握前后端分离开发技术的开发者来说,这本书是一个很好的学习资源。
评论 19
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值