介绍一种springboot项目接口安全的自定义策略

背景

        项目部分开放接口需要进行安全控制。这时针对部分接口名单进行安全校验。

思路

       1、 前端拿指定的秘钥加密一截动态文本(前半段为时间戳,后半段为固定标识),接着同时间戳一起给后端。

        2、后端接收到时间戳,进行使用同样的算法解密密文,并校验密文与预期是否相同。

        3、相同则放过,否则认定非法。

前端实现方式

关键代码如下:

// 导入 CryptoJS 库
import CryptoJS from "crypto-js";

// 定义时间戳和动态字段
const timestamp = new Date().getTime().toString();
const privateKey = "MTIAdJi2Nzg5MEFC"; //秘钥,写死
const plaText = timestamp + "DSJ@2023"; //DSJ@2023为固定后缀,代码中写死。

console.log("Timestamp: " + timestamp); // 输出时间戳的值到控制台

//加密逻辑:1、先生成key
const apiKeyBytes = CryptoJS.enc.Utf8.parse(privateKey);
const keyBytes = apiKeyBytes.sigBytes > 16 ? apiKeyBytes.clone().resize(16) : apiKeyBytes;
const key = CryptoJS.enc.Hex.parse(keyBytes.toString());

//加密逻辑:2、使用私钥生成密文  使用ECB模式,Pkcs7算法
const encryptedBytes = CryptoJS.AES.encrypt(plaText, key, {
  mode: CryptoJS.mode.ECB,
  padding: CryptoJS.pad.Pkcs7,
});

// 将加密结果转换为字符串
const encryptedText = encryptedBytes.toString();

export default {
  "API-SECRET-KEY": encryptedText,
  timestamp,
};
//构造请求头,必须包含两个参数
//1、API-SECRET-KEY = encryptedText
//2、timestamp = 上述定义的timestamp值

后端实现方式

关键代码如下

1、拦截器 ApiSecurityAuthenticationInterceptor

package com.dsj.prod.port.biz.interceptor;

import com.alibaba.fastjson.JSONObject;
import com.dsj.plf.arch.tool.api.R;
import com.dsj.prod.common.constants.CommonConstants;
import com.dsj.prod.common.utils.SecretKeyUtils;
import com.dsj.prod.port.biz.properties.PortSecurityProperties;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@Component
public class ApiSecurityAuthenticationInterceptor implements HandlerInterceptor {

	@Resource
	private PortSecurityProperties portSecurityProperties;

	@Override
	public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
		// 从请求头中获取秘钥
		String privateKey = portSecurityProperties.getSecurityPrivateKey();
		String securitySuffix = portSecurityProperties.getSecuritySuffix();
		String apiSecretKey = request.getHeader(CommonConstants.BizConst.API_SECRET_KEY);
		String secretSignal = request.getHeader(CommonConstants.BizConst.API_TIMESTAMP) + securitySuffix;

		// 进行秘钥验证
		if (secretSignal.equals(SecretKeyUtils.decryptWithDynamicField(apiSecretKey, privateKey))) {
			// 秘钥验证通过,放行请求
			return true;
		} else {
			// 秘钥验证不通过,返回错误信息
			response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
			response.getWriter().write(JSONObject.toJSONString(R.fail("Unauthorized request , Please check the secret key")));
			return false;
		}
	}
}

2、Web配置 PortWebConfig

package com.dsj.prod.port.biz.interceptor;

import com.dsj.prod.port.biz.properties.PortSecurityProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import javax.annotation.Resource;

@Configuration
public class PortWebConfig implements WebMvcConfigurer {
	@Resource
	private PortSecurityProperties portSecurityProperties;

	@Resource
	private ApiSecurityAuthenticationInterceptor registerAuthenticationInterceptor;

	@Override
	public void addInterceptors(InterceptorRegistry registry) {
		// 注册拦截器,并指定拦截的 URI
		registry.addInterceptor(registerAuthenticationInterceptor)
			.addPathPatterns(portSecurityProperties.getCheckUrlList()); // 可以根据需要指定需要拦截的 URI
	}
}

3、加密解密工具类 SecretKeyUtils

package com.dsj.prod.common.utils;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.Key;
import java.util.Base64;

/**
 * The type Secret key utils.
 */

public class SecretKeyUtils {
	private static final String ENCRYPTION_ALGORITHM = "AES";
	private static final String CIPHER_TRANSFORMATION = "AES/ECB/PKCS5Padding";
	private static final Logger log = LoggerFactory.getLogger(SecretKeyUtils.class);

	/**
	 * 调用解密动态字段生成的加密文本方法
	 *
	 * @param encryptedText the encrypted text
	 * @param dynamicField  the dynamic field
	 * @return the string
	 * @throws Exception the exception
	 */
	public static String decryptWithDynamicField(String encryptedText, String dynamicField) throws Exception {
		try {
			// 将动态字段作为密钥生成 Key 对象
			Key key = generateSecretKeySpec(dynamicField);

			// 创建 Cipher 对象并进行解密
			Cipher cipher = Cipher.getInstance(CIPHER_TRANSFORMATION);
			cipher.init(Cipher.DECRYPT_MODE, key);

			// 将 Base64 编码的加密文本解码为字节数组
			byte[] encryptedBytes = Base64.getDecoder().decode(encryptedText);

			// 进行解密并将结果转换为字符串
			byte[] decryptedBytes = cipher.doFinal(encryptedBytes);
			return new String(decryptedBytes, StandardCharsets.UTF_8);
		} catch (Exception e) {
			log.error("解密失败", e);
			return "";
		}

	}

	/**
	 * 调用加密动态字段生成的明文方法
	 *
	 * @param plaintext    the plaintext
	 * @param dynamicField the dynamic field
	 * @return the string
	 * @throws Exception the exception
	 */
	public static String encryptWithDynamicField(String plaintext, String dynamicField) throws Exception {
		try {
			// 将动态字段作为密钥生成 Key 对象
			Key key = generateSecretKeySpec(dynamicField);

			// 创建 Cipher 对象并进行加密
			Cipher cipher = Cipher.getInstance(CIPHER_TRANSFORMATION);
			cipher.init(Cipher.ENCRYPT_MODE, key);

			// 将明文转换为字节数组
			byte[] plaintextBytes = plaintext.getBytes(StandardCharsets.UTF_8);

			// 进行加密并将结果转换为 Base64 编码的字符串
			byte[] encryptedBytes = cipher.doFinal(plaintextBytes);
			return Base64.getEncoder().encodeToString(encryptedBytes);
		} catch (Exception e) {
			log.error("解密失败", e);
			return "";
		}
	}

	/**
	 * 生成 SecretKeySpec
	 *
	 * @param apiKey apiKey
	 * @return {@link SecretKeySpec}
	 * @author chentl
	 * @version v1.0.0
	 * @since 10:41 AM 2023/4/17
	 **/
	private static SecretKeySpec generateSecretKeySpec(String apiKey) {
		try {
			byte[] apiKeyBytes = apiKey.getBytes(StandardCharsets.UTF_8);
			byte[] keyBytes = new byte[16];
			System.arraycopy(apiKeyBytes, 0, keyBytes, 0, Math.min(apiKeyBytes.length, keyBytes.length));
			return new SecretKeySpec(keyBytes, ENCRYPTION_ALGORITHM);
		} catch (Exception e) {
			log.error("生成SecretKeySpec失败", e);
			return null;
		}

	}

	/**
	 * The entry point of application.
	 *
	 * @param args the input arguments
	 */
	public static void main(String[] args) {
		try {
			// 加密的文本和动态字段 加密
			String encryptedText = "arEmLP6GLGH73nVIUFsKyVnY/6baiGTbJZw+tlibVgE=";
			String dynamicField = "MTIAdJi2Nzg5MEFC";
			System.out.println("Encrypted text: " + encryptWithDynamicField("1681700145873DSJ@2023", dynamicField));
			// 调用解密方法
			String decryptedText = decryptWithDynamicField(encryptedText, dynamicField);
			System.out.println("Decrypted text: " + decryptedText);
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
}

4、yml 接口清单配置

# 口岸服务私有配置
port:
  security:
    security-private-key: MTIAdJi2Nzg5MEFC
    security-suffix: DSJ@2023
    check-url-list:
      - /port-ent-register/international-logistics-register
      - /port-ent-register/charge-main-body-register

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值