一、问题说明:接口如果是被同一个项目的前端项目调用,一般都是加了各种鉴权的,比如springsercurity+token安全机制,shiro 等框架都可以控制接口访问权限。但是如果接口是提供给外部调用,肯定是不需要登录的,所以需要在自身的权限控制中放开 该接口的token校验,这样就会造成安全问题,我们一般采取拦截器的方式,和第三方做个鉴权;
二、鉴权方法:
鉴权采用固定参数同样存在安全问题,容易被抓包获取到。所以一般带入动态的时间戳来鉴权,常用的鉴权逻辑是:两边各存一 份appId和appecret,采用post请求,
1、调用处:传入appId参数,header中再传入两个参数,一个是时间戳,一个是appId、appSecret、时间戳三个参数排序后拼接的字符串的MD码;如:
String url = thirdUrl += "?appId="+appId;
Map<String, String> header = new HashMap<>();
long timestamp = new Date().getTime();
List<String> paramList = new ArrayList<>();
paramList.add(appKey);
paramList.add(timestamp);
paramList.add(appSecret);
Collections.sort(paramList, new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
return o1.compareTo(o2);
}
});
StringBuilder builder = new StringBuilder();
for (String str : paramList) {
builder.append(str);
}
String md5 = EncryptionUtil.getMD5(builder );
header.put("sign",md5 );
header.put("timestamp",String.valueOf(timestamp));
String res = HttpUtil.postWithHeader(url,header,null);
附上请求util和md5生成util:
/**
* 发送post请求
*
* @param url
* @param header
* @param body
* @return
*/
public static String postWithHeader(String url, Map<String, String> header, String body) {
String result = "";
BufferedReader in = null;
PrintWriter out = null;
try {
// 设置 url
URL realUrl = new URL(url);
URLConnection connection = realUrl.openConnection();
// 设置 header
for (String key : header.keySet()) {
connection.setRequestProperty(key, header.get(key));
}
// 设置请求 body
connection.setDoOutput(true);
connection.setDoInput(true);
out = new PrintWriter(connection.getOutputStream());
// 保存body
out.print(body);
// 发送body
out.flush();
// 获取响应body
in = new BufferedReader(new InputStreamReader(connection.getInputStream()));
String line;
while ((line = in.readLine()) != null) {
result += line;
}
} catch (Exception e) {
log.error(e.getMessage(), e);
return null;
}
return result;
}
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
public class EncryptionUtil {
/***
* MD5加码 生成32位md5码
*/
public static String string2MD5(String inStr) {
MessageDigest md5 = null;
try {
md5 = MessageDigest.getInstance("MD5");
} catch (Exception e) {
System.out.println(e.toString());
e.printStackTrace();
return "";
}
char[] charArray = inStr.toCharArray();
byte[] byteArray = new byte[charArray.length];
for (int i = 0; i < charArray.length; i++)
byteArray[i] = (byte) charArray[i];
byte[] md5Bytes = md5.digest(byteArray);
StringBuffer hexValue = new StringBuffer();
for (int i = 0; i < md5Bytes.length; i++) {
int val = ((int) md5Bytes[i]) & 0xff;
if (val < 16){
hexValue.append("0");
}
hexValue.append(Integer.toHexString(val));
}
return hexValue.toString();
}
/**
* 加密解密算法 执行一次加密,两次解密
*/
public static String convertMD5(String inStr) {
char[] a = inStr.toCharArray();
for (int i = 0; i < a.length; i++) {
a[i] = (char) (a[i] ^ 't');
}
String s = new String(a);
return s;
}
public static String getMD5(String sha){
try {
MessageDigest md = MessageDigest.getInstance("MD5");
md.reset();
md.update(sha.getBytes());
byte[] result = md.digest();
StringBuffer sb = new StringBuffer();
int len = result.length;
for (int i = 0; i < len; i++) {
int v = result[i] & 0XFF;
if(v < 16)
sb.append("0");
sb.append(Integer.toString(v, 16).toLowerCase());
}
return sb.toString();
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
return null;
}
}
2、被调用处:判断时间戳是否超过多长时间;再根据传过来的appId查询appSecret,然后这三个参数根据同样的算法生成md5,比较和第三方传过来的sign是否相等即可:
package com.demo.interceptor;
import com.alibaba.druid.util.StringUtils;
import com.demo.service.UserService;
import com.demo.util.EncryptionUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.DigestUtils;
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.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
public class ApiInterCeptor implements HandlerInterceptor {
@Autowired
private UserService userService;
/**
* 原接口post请求:1、header传入timestamp时间戳;
* 2、header传入sign,值为appId、appSecret、timestamp排序后生成的md5码
* @param request
* @param response
* @param o
* @return
* @throws Exception
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object o) throws Exception {
//1、校验header中的时间戳,时差不超过5分钟
String timestamp = "";
String sign = "";
try {
timestamp = request.getHeader("timestamp");
sign = request.getHeader("sign");
if ((Math.abs(System.currentTimeMillis() - Long.valueOf(timestamp)) / 1000 / 60) > 5) {
response.getWriter().print("访问失效");
return false;
}
} catch (Exception e) {
response.getWriter().print("参数错误");
return false;
}
//2、校验参数
// (1). 将参数进行字典序排序
List<String> paramList = new ArrayList<>();
String appId = request.getParameter("appId");
paramList.add(appId);
paramList.add(timestamp);
String appSercret = userService.getAppSecretById(appId);
paramList.add(appSecret);
Collections.sort(paramList, new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
return o1.compareTo(o2);
}
});
// (2). 将参数字符串拼接成一个字符串进行MD5
StringBuilder builder = new StringBuilder();
for (String str : paramList) {
builder.append(str);
}
String md5 = EncryptionUtil.getMD5(builder.toString());
//(3).与带入的sign比较是否相等
if (!StringUtils.equals(sign, md5)) {
response.getWriter().print("sign不一致");
return false;
}
return false;
}
@Override
public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception {
}
}