前言
感謝我一位阿里的安全大佬朱哥,在我發第一篇博客的時候進行指點。 我對上一篇的不足之處進行總結一下:
- 加密過程太過簡單,我上一篇博客也談到需要改進,其中包括AppSecret等等也加入到簽名里面。
- 時間戳的問題也要加入進去,防止數據篡改。時間戳的問題是防止數據重放
- 加密算法需要再改進,MD5需要加點salt等等
- 參數需要排序,這個有點出入。因為一般來說內部接口使用類進行傳輸參數,可以重寫類的toString()方法來規范參數的排序。但是有個缺陷是:無法通配到不是按對象來接收的參數,這時就重寫不了toString了,只能我們手動去排序。
show you the code
get處理方法
new 攔截器
import cn.hutool.core.date.DateUnit;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.SecureUtil;
import com.alibaba.fastjson.JSON;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import JAVAx.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Date;
import java.util.Map;
public class MyBetterInterceptor implements HandlerInterceptor {
public static final ThreadLocal<String> local = new ThreadLocal<>();
public static final ThreadLocal<String> localSign = new ThreadLocal<>();
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String sign = request.getHeader("sign");
//這里對應appId,外面可以去數據庫查Secret
String appId = request.getHeader("appId");
if (valid(sign) || valid(appId)) {
throw new IllegalAccessException("驗簽非法");
}
String time = request.getHeader("createTime");
if (valid(time)) {
throw new IllegalAccessException("時間戳為空");
}
long second = DateUtil.between(new Date(), DateUtil.parse(time), DateUnit.MINUTE);
if (!(second <= 30 && DateUtil.compare(new Date(), DateUtil.parse(time)) > 0)) {
throw new IllegalAccessException("非法時間戳");
}
String sb;
if ("POST".equals(request.getMethod())) {
//System.out.println("post參數:" + new String(IoUtil.readBytes(request.getInputStream(), false)));
/*String parameter = new String(IoUtil.readBytes(request.getInputStream(), false));
sb = ParameterUtil.generateSign(ParameterUtil.postParameter(parameter, time));*/
local.set(time);
localSign.set(sign);
return true;
} else {
Map<String, String[]> map = request.getParameterMap();
System.out.println(JSON.toJSONString(map));
if (map == null || map.size() == 0) {
return false;
}
sb = ParameterUtil.generateSign(ParameterUtil.getParameter(map, time));
}
System.out.println("加密混淆之后的字符串:" + sb);
//Todo 去數據庫查詢appId對應的Sercert,比如這里固定查出來是dajitui
sb = sb + "dajitui";
System.out.println("sign:" + SecureUtil.md5(sb));
if (SecureUtil.md5(sb).equals(sign)) {
return true;
}
return true;
}
private boolean valid(String value) {
return StrUtil.isBlank(value);
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
}
}
注意點
1.攔截器:我們只能處理get請求的參數,post的話request只能讀取一次,后面無法再讀取一遍,我們使用aop去進行驗簽。 2.加密算法有所改變,添加了Sercert特有的參數,即使appid被別人拿到了,也沒有多大影響。
加密處理過程
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONArray;
import cn.hutool.json.JSONObject;
import com.alibaba.fastjson.JSON;
import org.Apache.commons.codec.digest.DigestUtils;
import java.util.*;
import java.util.stream.Collectors;
/**
* @author M
*/
public class ParameterUtil {
ParameterUtil() {
}
public static Map<String, Object> getParameter(Map<String, String[]> map, String time) {
if (map == null || map.size() == 0) {
return new HashMap<>(6);
}
Map<String, Object> resultMap = new HashMap<>(map.size());
map.forEach((k, v) -> {
if (v.length == 1) {
resultMap.put(k, v[0]);
} else {
resultMap.put(k, new ArrayList<>(Arrays.asList(v)));
}
});
resultMap.put("time",time);
System.out.println("map:"+ JSON.toJSONString(resultMap));
return resultMap;
}
public static Map<String, Object> postParameter(String msg, String time) {
if (StrUtil.isBlank(msg)) {
return new HashMap<>(6);
}
JSONArray array=new JSONArray(msg);
Map<String, Object> resultMap = new HashMap<>(25);
for(JSONObject object:array.jsonIter()){
for (String s : object.keySet()) {
resultMap.put(s, object.get(s));
}
resultMap.put("time",time);
}
System.out.println("map:"+ JSON.toJSONString(resultMap));
return resultMap;
}
public static final String SEPERATE_CHAR = "#%$";
public static String generateSign(Map<String, Object> param) {
Set<String> keys = param.keySet();
//過濾掉sign,對key進行排序
List<String> sortedKeys = keys.stream().filter(key -> !Objects.equals(key, "sign")).sorted().collect(Collectors.toList());
//加上自定義字符串混淆算法
String calcSign = sortedKeys.stream().map(param::get).map(String::valueOf).collect(Collectors.joining(SEPERATE_CHAR));
//對字符串進行hash
calcSign = DigestUtils.sha1Hex(calcSign);
return calcSign;
}
public static void main(String[] args) {
System.out.println(ParameterUtil.postParameter("{n" +
" "status": "0000",n" +
" "message": "success",n" +
" "data": {n" +
" "title": {n" +
" "id": "001",n" +
" "name" : "白菜"n" +
" },n" +
" "content": [n" +
" {n" +
" "id": "001",n" +
" "value":"你好 白菜"n" +
" },n" +
" {n" +
" "id": "002",n" +
" "value":"你好 蘿卜" n" +
" }n" +
" ]n" +
" }n" +
"}",""));
}
}
其中 postParameter是處理post參數,getParameter處理get參數轉成Map<String,Object>形式。
generateSign才是真正的大頭
public static final String SEPERATE_CHAR = "#%$";
public static String generateSign(Map<String, Object> param) {
Set<String> keys = param.keySet();
//過濾掉sign,對key進行排序
List<String> sortedKeys = keys.stream().filter(key -> !Objects.equals(key, "sign")).sorted().collect(Collectors.toList());
//加上自定義字符串混淆算法
String calcSign = sortedKeys.stream().map(param::get).map(String::valueOf).collect(Collectors.joining(SEPERATE_CHAR));
//對字符串進行hash
calcSign = DigestUtils.sha1Hex(calcSign);
return calcSign;
}
將key進行排序,排序完獲取value,然后它們之間使用特定分隔符分開來,讓別人猜不到我們的加密算法。
最后的sign
sb = ParameterUtil.generateSign(ParameterUtil.getParameter(map, time));
System.out.println("加密混淆之后的字符串:" + sb);
//Todo 去數據庫查詢appId對應的Sercert,比如這里固定查出來是dajitui
sb = sb + "dajitui";
System.out.println("sign:" + SecureUtil.md5(sb));
特有的根據appid獲取sercert過程,然后加入再進行md5加密。
post處理方法
使用aop+自定義注解
定義一個注解
@Target({METHOD, FIELD, TYPE})
@Retention(RUNTIME)
public @interface NoParameter {
}
一把梭哈
@NoParameter
@PostMapping("/rpc/test1")
public String a1(@RequestBody Student1 student1){
System.out.println("list:"+student1);
return "123";
}
切面邏輯
import cn.hutool.crypto.SecureUtil;
import com.alibaba.fastjson.JSON;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class TestAspect {
@Pointcut("@annotation(com.example.demo.NoParameter)")
public void annotationPoinCut() {
}
@Before(value = "annotationPoinCut()")
public void beforeTest(JoinPoint point) throws IllegalAccessException {
String args = JSON.toJSONString(point.getArgs());
String sb = ParameterUtil.generateSign(ParameterUtil.postParameter(args, MyBetterInterceptor.local.get()));
sb = sb + "dajitui";
System.out.println("sign:" + SecureUtil.md5(sb));
if (!SecureUtil.md5(sb).equals(MyBetterInterceptor.localSign.get())) {
throw new IllegalAccessException("驗簽失敗");
}
MyBetterInterceptor.local.remove();
MyBetterInterceptor.localSign.remove();
}
}






