Springboot 实现api接口(二)
1、序言
思想:先给大家讲讲我们如何来实现接口加密,我们主要通过签名验证的方式来实现接口加密,前端给后端接口传参数时,把所有参数排序,并按照“参数=参数值”的模式用“&”字符拼接成字符串,生成一个sign签名,后端写一个拦截器对其进行签名验证,后端接收到参数后,也通过同样的方法, 对其参数加密生成一个sign,两者相对比,如何相同则签名成功!
现在加密的方式有很多,比如国产哈希算法SM3等,自己根据安全要求合理选择。
2、加密示例
工具类
- 主要实现参数去除空值和参数排序
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class SignatureUtil {
/**
* 除去数组中的空值、签名参数和Token
*
* @param params 签名参数组
* @return 去掉空值与签名参数后的新签名参数组
*/
public static Map<String, Object> parameterFilter(Map<String, Object> params) {
Map<String, Object> result = new HashMap<String, Object>();
if (params == null || params.size() <= 0) {
return result;
}
for (String key : params.keySet()) {
Object value = params.get(key);
//如果取出来为数组,将数组转换为字符串
if (value instanceof String[]) {
value = ((String[]) value)[0];
}
if (value == null || value.equals("") || key.equalsIgnoreCase("sign")
|| key.equalsIgnoreCase("token")) {
continue;
}
result.put(key, value.toString());
}
return result;
}
/**
* 把数组所有元素排序,并按照“参数=参数值”的模式用“&”字符拼接成字符串
*
* @param params 需要排序并参与字符拼接的参数组
* @return 拼接后字符串
*/
public static String createLinkString(Map<String, Object> params) {
List<String> keys = new ArrayList<String>(params.keySet());
Collections.sort(keys);
String prestr = "";
for (int i = 0; i < keys.size(); i++) {
String key = keys.get(i);
String value = params.get(key).toString();
if (i == keys.size() - 1) {//拼接时,不包括最后一个&字符
prestr = prestr + key + "=" + value;
} else {
prestr = prestr + key + "=" + value + "&";
}
}
return prestr;
}
}
- 进行md5加密
import java.security.MessageDigest;
public class MD5Util {
/**
* MD5普通加密
*
* @param rawPass 明文
* @return
*/
public static String encrypt(String rawPass) {
return encrypt(rawPass, null);
}
/**
* * MD5盐值加密
*
* @param rawPass 明文
* @param salt 盐值
* @return
*/
public static String encrypt(String rawPass, Object salt) {
String saltedPassword = mergePasswordAndSalt(rawPass, salt);
try {
MessageDigest messageDigest = MessageDigest.getInstance("MD5");
byte[] digest = messageDigest.digest(saltedPassword.getBytes("UTF-8"));
return new String(encode(digest));
} catch (Exception e) {
return rawPass;
}
}
/**
* 拼接密码与盐值
*
* @param password
* @param salt
* @return 密码{盐值}
*/
private static String mergePasswordAndSalt(String password, Object salt) {
if (salt == null || "".equals(salt.toString().trim())) {
return password;
}
return password + "{" + salt.toString() + "}";
}
/**
* encrypt
*
* @param bytes
* @return
*/
private static char[] encode(byte[] bytes) {
char[] HEX = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};
int nBytes = bytes.length;
char[] result = new char[2 * nBytes];
int j = 0;
for (byte aByte : bytes) {
result[j++] = HEX[(0xF0 & aByte) >>> 4];
result[j++] = HEX[(0x0F & aByte)];
}
return result;
}
/**
* 校验明文加验证MD5后的值是否等于密文
*
* @param plainText 明文
* @param salt 盐值
* @param cipherText 密文
* @return
*/
public static boolean verify(String plainText, String salt, String cipherText) {
return (MD5Util.encrypt(plainText, salt)).equals(cipherText);
}
}
后端对接请求示例
@Test
public void testInsert() throws Exception {
Map<String, Object> param = Maps.newHashMap();
param.put("appid", "appid值");
param.put("stamp", UUID.randomUUID().toString());
param.put("参数1", "参数值1");
param.put("参数2", "参数值2");
String newSignString = SignatureUtil.createLinkString(SignatureUtil.parameterFilter(param)) + "&" + "key值";
param.put("sign", MD5Util.encrypt(newSignString));
String result = HttpUtil.post("请求地址", param);
System.out.println(result);
}
3、后端拦截器
拦截器
import com.cch.error.BaseBusinessException;
import com.cch.error.SignError;
import com.cch.util.MD5Util;
import com.cch.util.SignatureUtil;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import org.yaml.snakeyaml.util.ArrayUtils;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
/**
* @Auther: cch
* @Date: 2020/9/22 09:12
* @Description: 签名拦截器
*/
@Component
public class SignatureInterceptor extends HandlerInterceptorAdapter {
private static final Logger logger = LoggerFactory.getLogger(SignatureInterceptor.class);
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
logger.info("请求地址:" + request.getRequestURI());
//验证签名
String signStr = this.doAssembleSignString(request);
if (logger.isDebugEnabled()) {
logger.debug("signStr:" + signStr);
}
String sign = request.getParameter("sign");
boolean result = MD5Util.verify(signStr, null, sign);
if (logger.isDebugEnabled()) {
logger.debug("sign result:" + result);
}
if (!result) {
throw new BaseBusinessException(SignError.SIGNATURE_ERROR);
}
//replay防止重复调用
//拿到标识后,先判断是否有值。没有值抛异常,有值验证同一时间是否已经存在,存在抛异常,不存在保存此值
String stamp = request.getParameter("stamp");
stampService.verifyApiStamp(stamp);
return true;
}
/**
* 拼接签名字符串
*
* @param request
* @return
*/
private String doAssembleSignString(HttpServletRequest request) {
//获取待签名的字符串
String newSignString = SignatureUtil.createLinkString(SignatureUtil.parameterFilter(getRequestData(request)));
String appid = request.getParameter("appid");
if(!"123".equals(appid)){
throw new BaseBusinessException(SignError.SIGNATURE_ERROR);
}
newSignString = newSignString + "&" + "456";
//从redis中获取用户Secret
// App app = appService.getAppById(appid);
// if (app == null) {
// throw new BaseBusinessException(SignError.SIGNATURE_ERROR);
// }
// newSignString = newSignString + "&" + app.getKey();
return newSignString;
}
private Map<String,Object> getRequestData(HttpServletRequest request) {
Map<String,Object> map = new HashMap();
Enumeration enumeration = request.getParameterNames();
while (enumeration.hasMoreElements()) {
String paramName = (String) enumeration.nextElement();
String paramValue = request.getParameter(paramName);
//形成键值对应的map
map.put(paramName, paramValue);
}
return map;
}
}
将自定义的拦截器进行注册
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.util.Arrays;
import java.util.List;
/**
* @Auther: cch
* @Date: 2020/9/22 10:29
* @Description:
*/
@Configuration
public class WebAppConfigurer implements WebMvcConfigurer {
//放行了接口文档的请求
private static List<String> EXCLUDE_PATH = Arrays.asList("/image/**","/js/**","/webjars/**","/error/**","/favicon.ico","/swagger-ui/**","/v3/**","/swagger-resources/**");
@Override
public void addInterceptors(InterceptorRegistry registry) {
//添加拦截器的顺序就是拦截的顺序
registry.addInterceptor(new SignatureInterceptor()).excludePathPatterns(EXCLUDE_PATH);
}
}