1、場景
由于項目需要開發(fā)第三方接口給多個供應(yīng)商,為保證Api接口的安全性,遂采用Api接口簽名驗證。
2、接口防御措施
- 請求發(fā)起時間得在限制范圍內(nèi)
- 請求的用戶是否真實存在
- 是否存在重復(fù)請求
- 請求參數(shù)是否被篡改
3、簽名認證邏輯
1、服務(wù)端生成一對 accessKey/secretKey密鑰對,將 accessKey公開給客戶端,將 secretKey 保密。
2、客戶端使用 secretKey和一些請求參數(shù)(如時間戳、請求內(nèi)容等),使用 MD5 算法生成簽名。
3、客戶端將 accessKey、簽名和請求參數(shù)一起發(fā)送給服務(wù)端。
4、服務(wù)端使用 和收到的請求參數(shù),使用 MD5 算法生成簽名。
5、服務(wù)端比較客戶端發(fā)來的簽名和自己生成的簽名是否相同,如果相同,則認為請求是可信的,否則認為請求是不可信的。
secretKey不進行網(wǎng)絡(luò)傳輸,只用于本地MD5運算
4、簽名算法規(guī)則
計算步驟
用于計算簽名的參數(shù)在不同接口之間會有差異,但算法過程固定如下4個步驟。
將<key, value>請求參數(shù)對按key進行字典升序排序,得到有序的參數(shù)對列表N
將列表N中的參數(shù)對按URL鍵值對的格式拼接成字符串,得到字符串T(如:key1=value1&key2=value2),URL鍵值拼接過程value部分需要URL編碼,URL編碼算法用大寫字母,例如%E8,而不是小寫%e8
將應(yīng)用密鑰以app_key為鍵名,組成URL鍵值拼接到字符串T末尾,得到字符串S(如:key1=value1&key2=value2&app_key=密鑰)
對字符串S進行MD5運算,將得到的MD5值所有字符轉(zhuǎn)換成大寫,得到接口請求簽名
注意事項
不同接口要求的參數(shù)對不一樣,計算簽名使用的參數(shù)對也不一樣
參數(shù)名區(qū)分大小寫,參數(shù)值為空不參與簽名
URL鍵值拼接過程value部分需要URL編碼文章來源:http://www.zghlxwxcb.cn/news/detail-815826.html
5、代碼示例
1、sign工具類
public class SignUtil {
/**
* 簽名算法
* 1. 計算步驟
* 用于計算簽名的參數(shù)在不同接口之間會有差異,但算法過程固定如下4個步驟。
* 將<key, value>請求參數(shù)對按key進行字典升序排序,得到有序的參數(shù)對列表N
* 將列表N中的參數(shù)對按URL鍵值對的格式拼接成字符串,得到字符串T(如:key1=value1&key2=value2),URL鍵值拼接過程value部分需要URL編碼,URL編碼算法用大寫字母,例如%E8,而不是小寫%e8
* 將應(yīng)用密鑰以app_key為鍵名,組成URL鍵值拼接到字符串T末尾,得到字符串S(如:key1=value1&key2=value2&app_key=密鑰)
* 對字符串S進行MD5運算,將得到的MD5值所有字符轉(zhuǎn)換成大寫,得到接口請求簽名
* 2. 注意事項
* 不同接口要求的參數(shù)對不一樣,計算簽名使用的參數(shù)對也不一樣
* 參數(shù)名區(qū)分大小寫,參數(shù)值為空不參與簽名
* URL鍵值拼接過程value部分需要URL編碼
* @return 簽名字符串
*/
private static String getSign(Map<String, Object> map, String secretKey) {
List<Map.Entry<String, Object>> infoIds = new ArrayList<>(map.entrySet());
Collections.sort(infoIds, new Comparator<Map.Entry<String, Object>>() {
public int compare(Map.Entry<String, Object> arg0, Map.Entry<String, Object> arg1) {
return (arg0.getKey()).compareTo(arg1.getKey());
}
});
StringBuffer sb = new StringBuffer();
for (Map.Entry<String, Object> m : infoIds) {
if(null == m.getValue() || StringUtils.isNotBlank(m.getValue().toString())){
sb.append(m.getKey()).append("=").append(URLUtil.encodeAll(m.getValue().toString())).append("&");
}
}
sb.append("secret-key=").append(secretKey);
return MD5.create().digestHex(sb.toString()).toUpperCase();
}
//獲取隨機值
private static String getNonceStr(int length){
//生成隨機字符串
String str="zxcvbnmlkjhgfdsaqwertyuiopQWERTYUIOPASDFGHJKLZXCVBNM1234567890";
Random random=new Random();
StringBuffer randomStr=new StringBuffer();
// 設(shè)置生成字符串的長度,用于循環(huán)
for(int i=0; i<length; ++i){
//從62個的數(shù)字或字母中選擇
int number=random.nextInt(62);
//將產(chǎn)生的數(shù)字通過length次承載到sb中
randomStr.append(str.charAt(number));
}
return randomStr.toString();
}
//簽名驗證方法
public static boolean signValidate(Map<String, Object> map,String secretKey,String sign){
String mySign = getSign(map,secretKey);
return mySign.equals(sign);
}
}
2、定義攔截器
@Configuration
public class SignInterceptorConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(signInterceptor())
.addPathPatterns("/openapi/**");//只攔截openapi前綴的接口
}
//交給spring管理 SignInterceptor bean
//不然下邊 private OpenApiApplyMapper applyMapper;注入為null
@Bean
public SignInterceptor signInterceptor(){
return new SignInterceptor();
}
}
3、生成accessKey、secretKey 工具類
public class KeyGenerator {
private static final int KEY_LENGTH = 32; // 指定生成的key長度為32字節(jié)
public static String generateAccessKey() {
SecureRandom random = new SecureRandom();
byte[] bytes = new byte[KEY_LENGTH / 2]; // 生成的字節(jié)數(shù)要除以2
random.nextBytes(bytes);
return Base64.getEncoder().encodeToString(bytes).replace("/", "").replace("+", "").substring(0, 20);
}
public static String generateSecretKey() {
SecureRandom random = new SecureRandom();
byte[] bytes = new byte[KEY_LENGTH];
random.nextBytes(bytes);
return Base64.getEncoder().encodeToString(bytes).replace("/", "").replace("+", "").substring(0, 40);
}
}
4、signInterceptor類
public class SignInterceptor implements HandlerInterceptor {
private static final String ACCESSKEY = "access-key";//調(diào)用者身份唯一標識
private static final String TIMESTAMP = "time-stamp";//時間戳
private static final String SIGN = "sign";//簽名
private static final String NONCE = "nonce";//隨機值
@Resource
private OpenApiApplyMapper applyMapper;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if(checkSign(request, response)){//簽名認證
return HandlerInterceptor.super.preHandle(request, response, handler);
}
return false;
}
/**
* 驗證簽名
* @param request
* @param response
* @return
* @throws Exception
*/
private boolean checkSign(HttpServletRequest request,HttpServletResponse response)throws Exception {
response.setContentType("application/json");
response.setCharacterEncoding("utf8");
String ip = IPUtils.getIpAddr(request);
FzyLogUtil.infoSafe("開放接口", "訪問時間:" + LocalDateTime.now() + ",IP:" + ip + ",訪問接口:" + request.getRequestURL());
String accessKey = request.getHeader(ACCESSKEY);
String timestamp = request.getHeader(TIMESTAMP);
String nonce = request.getHeader(NONCE);
String sign = request.getHeader(SIGN);
if (!StringUtils.isNotBlank(accessKey)) {
response.getWriter().write(JSON.toJSONString(ResultUtil.fail("accessKey無效")));
FzyLogUtil.errorSafe("開放接口請求失敗", "時間:" + LocalDateTime.now() + ",IP:" + ip + ",訪問接口:" + request.getRequestURL() + "錯誤信息:accessKey無效");
return false;
}
if (StringUtils.isBlank(sign)) {
response.getWriter().write(JSON.toJSONString(ResultUtil.fail("簽名無效")));
FzyLogUtil.errorSafe("開放接口請求失敗", "時間:" + LocalDateTime.now() + ",IP:" + ip + ",訪問接口:" + request.getRequestURL() + "錯誤信息:簽名無效");
return false;
}
OpenApiDetailDO openApiDetailDO = applyMapper.selectOneByAccessKey(accessKey);
if (openApiDetailDO == null) {
response.getWriter().write(JSON.toJSONString(ResultUtil.fail("accessKey不存在")));
FzyLogUtil.errorSafe("開放接口請求失敗", "時間:" + LocalDateTime.now() + ",IP:" + ip + ",訪問接口:" + request.getRequestURL() + "錯誤信息:accessKey不存在");
return false;
}
if (StringUtils.isNotBlank(openApiDetailDO.getBlackList())) {
for (String bIp : openApiDetailDO.getBlackList().split(",")) {
if (bIp.equals(ip)) {//黑名單
response.getWriter().write(JSON.toJSONString(ResultUtil.fail("拒絕請求")));
FzyLogUtil.errorSafe("開放接口請求失敗", "時間:" + LocalDateTime.now() + ",IP:" + ip + ",訪問接口:" + request.getRequestURL() + "錯誤信息:黑名單拒絕請求");
return false;
}
}
}
if (StringUtils.isNotBlank(openApiDetailDO.getWhiteList())) {
boolean flag = false;
for (String bIp : openApiDetailDO.getWhiteList().split(",")) {
if (bIp.equals(ip)) {//白名單
flag = true;
break;
}
}
if(!flag){
response.getWriter().write(JSON.toJSONString(ResultUtil.fail("拒絕請求")));
FzyLogUtil.errorSafe("開放接口請求失敗", "時間:" + LocalDateTime.now() + ",IP:" + ip + ",訪問接口:" + request.getRequestURL() + "錯誤信息:白名單未符合拒絕請求");
return false;
}
}
if ("0".equals(openApiDetailDO.getInvokeStatus() + "")) {
response.getWriter().write(JSON.toJSONString(ResultUtil.fail("訪問權(quán)限已被凍結(jié)")));
FzyLogUtil.errorSafe("開放接口請求失敗", "時間:" + LocalDateTime.now() + ",IP:" + ip + ",訪問接口:" + request.getRequestURL() + "錯誤信息:訪問權(quán)限已被凍結(jié)");
return false;
}
if (!"1".equals(openApiDetailDO.getApiStatus() + "")) {
response.getWriter().write(JSON.toJSONString(ResultUtil.fail("接口異常,暫停訪問")));
FzyLogUtil.errorSafe("開放接口請求失敗", "時間:" + LocalDateTime.now() + ",IP:" + ip + ",訪問接口:" + request.getRequestURL() + "錯誤信息:接口異常,暫停訪問");
return false;
}
if (!StringUtils.isNotBlank(timestamp)) {
response.getWriter().write(JSON.toJSONString(ResultUtil.fail("時間戳無效")));
FzyLogUtil.errorSafe("開放接口請求失敗", "時間:" + LocalDateTime.now() + ",IP:" + ip + ",訪問接口:" + request.getRequestURL() + "錯誤信息:時間戳無效");
return false;
} else if (openApiDetailDO.getTimeOut() != null) {
if (System.currentTimeMillis() - Long.valueOf(timestamp) > openApiDetailDO.getTimeOut() * 1000) {
response.getWriter().write(JSON.toJSONString(ResultUtil.fail("請求已過期")));
FzyLogUtil.errorSafe("開放接口請求失敗", "時間:" + LocalDateTime.now() + ",IP:" + ip + ",訪問接口:" + request.getRequestURL() + "錯誤信息:請求已過期");
return false;
}
;
}
Map<String, Object> hashMap = new HashMap<>();
String queryStrings = request.getQueryString();//獲取url后邊拼接的參數(shù)
if (queryStrings != null) {
for (String queryString : queryStrings.split("&")) {
String[] param = queryString.split("=");
if (param.length == 2) {
hashMap.put(param[0], param[1]);
}
}
}
hashMap.put(ACCESSKEY, accessKey);
hashMap.put(TIMESTAMP, timestamp);
if (StringUtils.isNotBlank(nonce)) {
hashMap.put(NONCE, nonce);
}
String secretKey = openApiDetailDO.getSecretKey();
String body = new RequestWrapper(request).getBody();
if (StringUtils.isNotBlank(body)) {
Map<String, Object> map = JSON.parseObject(body);
if (map != null) {
hashMap.putAll(map);
}
}
if (!SignUtil.signValidate(hashMap, secretKey, sign)) {//認證失敗
response.getWriter().write(JSON.toJSONString(ResultUtil.fail("認證失敗")));
FzyLogUtil.errorSafe("開放接口請求失敗", "時間:" + LocalDateTime.now() + ",IP:" + ip + ",訪問接口:" + request.getRequestURL() + "錯誤信息:認證失敗");
return false;
}
return true;
}
}
5、SignInterceptor 獲取body里參數(shù)后,接口的controller會獲取不到body的參數(shù)了,會報錯
通過過濾器解決文章來源地址http://www.zghlxwxcb.cn/news/detail-815826.html
@Component
@WebFilter(filterName = "HttpServletRequestFilter", urlPatterns = "/")
@Order(10000)
public class HttpServletRequestFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
String contentType = request.getContentType();
String method = "multipart/form-data";
if (contentType != null && contentType.contains(method)) {
// 將轉(zhuǎn)化后的 request 放入過濾鏈中
request = new StandardServletMultipartResolver().resolveMultipart(request);
}
request = new RequestWrapper((HttpServletRequest) servletRequest);
//獲取請求中的流如何,將取出來的字符串,再次轉(zhuǎn)換成流,然后把它放入到新request對象中
// 在chain.doFiler方法中傳遞新的request對象
if(null == request) {
filterChain.doFilter(servletRequest, servletResponse);
} else {
filterChain.doFilter(request, servletResponse);
}
}
@Override
public void destroy() {
}
}
到了這里,關(guān)于超詳細!完整版!基于spring對外開放接口的簽名認證方案(攔截器方式)的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!