一、问题引入:鉴权接口的安全性,防止数据泄露和网络攻击,接口一般是需要鉴权控制访问的。如前后端分离项目中,前端带入token等方式。如果接口提供给第三方调用且无需登陆,则需要给该接口加白名单,这样就会造成安全问题,我们可以约定参数进行鉴权;
二、鉴权方法:
鉴权采用固定参数同样存在安全问题,容易被抓包获取到。可以带入动态的时间戳来鉴权。
1、调用处:参数传入appId;header头部传时间戳和sign,其中sign是appId、appSecret、时间戳三个参数排序后拼接的字符串转成的MD5加密字符串;如:
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 {
}
}
接口中直接校验
/**
提供给第三方的查询接口
**/
@RequestMapping("/third/listSchool")
public ResponseMessage listSchool(HttpServletRequest request, @RequestBody ThirdQuery query) {
String timestamp = request.getHeader("timestamp");
String sign = request.getHeader("sign");
Result<String> signCheckResult = SignUtils.signCheck(timestamp, sign, appId, appSecret);
if (!signCheckResult.isSuccess()){
return ResponseMessage.error(signCheckResult.getCode(), signCheckResult.getMessage());
}
//
//业务逻辑
}
/**
签名验证
*/
public class SignUtils {
private static Logger logger = LoggerFactory.getLogger(SignUtils.class);
public static Result<String> signCheck(String timestamp, String sign, String appId, String appSecret) {
logger.info("SignUtils.signChecktimestamp={}, sign={}, appId={},appSecret={}",
timestamp, sign, appId, appSecret);
Result<String> result = new Result<>();
if (StringUtils.isBlank(timestamp)) {
result.setErrorMessage(CommonEnums.CODE_400.getCode(), "timestamp为空");
return result;
}
if (StringUtils.isBlank(sign)) {
result.setErrorMessage(CommonEnums.CODE_400.getCode(), "sign为空");
return result;
}
if (StringUtils.isBlank(appId)) {
result.setErrorMessage(CommonEnums.CODE_400.getCode(), "appId为空");
return result;
}
if (StringUtils.isBlank(appSecret)) {
result.setErrorMessage(CommonEnums.CODE_400.getCode(), "appSecret为空");
return result;
}
try {
//时间差
long currentTimes = System.currentTimeMillis();
if ((Math.abs(currentTimes - Long.valueOf(timestamp)) / 1000 / 60) > 5) {
result.setErrorMessage(CommonEnums.CODE_406.getCode());
return result;
}
//密文
List<String> paramList = new ArrayList<>();
paramList.add(appId);
paramList.add(appSecret);
paramList.add(timestamp);
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 = MD5Util.getMD5(builder.toString());
if (!StringUtils.equals(sign, md5)) {
result.setErrorMessage(CommonEnums.CODE_401.getCode(), "签名验证失败");
return result;
}
} catch (Exception e) {
logger.error("SignUtils.signCheck error", e);
result.setErrorMessage(CommonEnums.CODE_500.getCode(), e.getMessage());
return result;
}
return result;
}
}