文章大綱
一、加密與驗(yàn)簽介紹
二、接口驗(yàn)簽實(shí)操
三、項(xiàng)目源碼下載

一、加密與驗(yàn)簽介紹
??大多數(shù)公共網(wǎng)絡(luò)是不安全的,一切基于HTTP協(xié)議的請求/響應(yīng)(Request or Response)都是可以被截獲的、篡改、重放(重發(fā))的。因此我們需要考慮以下幾點(diǎn)內(nèi)容:
- 防偽裝攻擊(案例:在公共網(wǎng)絡(luò)環(huán)境中,第三方 有意或惡意 的調(diào)用我們的接口)
- 防篡改攻擊(案例:在公共網(wǎng)絡(luò)環(huán)境中,請求頭/查詢字符串/內(nèi)容 在傳輸過程被修改)
- 防重放攻擊(案例:在公共網(wǎng)絡(luò)環(huán)境中,請求被截獲,稍后被重放或多次重放)
- 防數(shù)據(jù)信息泄漏(案例:截獲用戶登錄請求,截獲到賬號、密碼等)
二、接口驗(yàn)簽實(shí)操
1. 實(shí)操說明
??接口加密與驗(yàn)簽的方法有非常多,比如RSA(后期進(jìn)行講解),基于token等方式,而對于普通項(xiàng)目,我認(rèn)為最重要的是防偽裝攻擊、防篡改攻擊、防重放攻擊。因?yàn)榻酉聛淼膶?shí)操,主要圍繞以下幾點(diǎn)進(jìn)行。
2. 邏輯講解
客戶端操作
(1)用戶登錄成功后,會接收到對應(yīng)的key值和key過期時(shí)間,該key是經(jīng)過32位小寫加密,且編碼格式為UTF-8
(2)接口請求時(shí),將請求的參數(shù),通過key-value方式進(jìn)行字典升序,且編碼成UTF-8形式
(3)將key值拼接在升序且編碼后的字符串前面,進(jìn)行MD32位小寫加密,其編碼成UTF-8形式形成簽名,連同請求參數(shù)一同發(fā)送至后臺
(4)退出登錄時(shí),需要通知后臺失效該用戶的key
(5)補(bǔ)充說明1:對于登錄接口,如果檢測到用戶賬號密碼錯(cuò)誤,則判斷錯(cuò)誤次數(shù)后,在一定時(shí)間內(nèi)進(jìn)行登錄禁止,如果登錄禁止解除后,用戶再次出現(xiàn)錯(cuò)誤,則延長限制時(shí)間
(6)補(bǔ)充說明2:對于無需登錄接口,需要限制客戶端請求次數(shù),進(jìn)行接口防刷保護(hù)
服務(wù)端操作
(1)當(dāng)用戶登錄成功時(shí),生成與該用戶對應(yīng)的key值返回給用戶端,同時(shí)將id與key緩存在redis中
(2)當(dāng)接收到請求時(shí),根據(jù)請求id去redis查詢對應(yīng)key是多少,查不到則代表沒有請求權(quán)限,將該用戶系統(tǒng)信息,請求項(xiàng)目名、接口名,請求時(shí)間、錯(cuò)誤類型(用戶信息不正確/參數(shù)與簽名不一致)存進(jìn)redis(緩存進(jìn)磁盤),當(dāng)ip錯(cuò)誤次數(shù)超過一定次數(shù)后,限制ip訪問項(xiàng)目
(3)將key和請求參數(shù)按客戶端同樣方式進(jìn)行簽名,與請求的sign進(jìn)行比較
(4)如果驗(yàn)簽不一致,將該用戶系統(tǒng)信息,請求項(xiàng)目名、接口名,請求時(shí)間、錯(cuò)誤類型(用戶信息不正確/參數(shù)與簽名不一致)存進(jìn)redis,當(dāng)ip錯(cuò)誤次數(shù)超過一定次數(shù)時(shí),限制ip訪問所有項(xiàng)目,若驗(yàn)簽通過,則進(jìn)行接口放行,且將用戶系統(tǒng)信息,請求項(xiàng)目名、接口名,請求時(shí)間緩存進(jìn)日志中(存進(jìn)磁盤)
Redis參數(shù)需記錄信息
1.用戶信息:id,用戶key,客戶端請求系統(tǒng)信息
2.驗(yàn)簽錯(cuò)誤信息:用戶系統(tǒng)信息,請求項(xiàng)目名、接口名、請求時(shí)間、錯(cuò)誤類型(用戶信息不正確/參數(shù)與簽名不一致)
日志緩存信息
接口請求成功信息:用戶系統(tǒng)信息,請求項(xiàng)目名、接口名,請求時(shí)間
3.代碼講解
用戶登錄成功、生成key參數(shù)
//模擬用戶登錄成功
public String getMd5Key() {
return "de456878b58568e29773e6a53b39d6ef";
}
獲取客戶端信息
/**
* 獲取客戶端的信息
* @author 吳曉暢
*
*/
public final class SystemUtils {
/**
* 獲取訪問者IP
* 在一般情況下使用Request.getRemoteAddr()即可,但是經(jīng)過nginx等反向代理軟件后,這個(gè)方法會失效。
*
* 本方法先從Header中獲取X-Real-IP,如果不存在再從X-Forwarded-For獲得第一個(gè)IP(用,分割),
* 如果還不存在則調(diào)用Request .getRemoteAddr()。
* @param request
* @return
*/
public String getIpAddr(HttpServletRequest request) {
String ip = request.getHeader("X-Real-IP");
if (ip!= null && !"".equals(ip) && !"unknown".equalsIgnoreCase(ip)) {
return ip;
}
ip = request.getHeader("X-Forwarded-For");
if (ip!= null && !"".equals(ip) && !"unknown".equalsIgnoreCase(ip)) {
// 多次反向代理后會有多個(gè)IP值,第一個(gè)為真實(shí)IP。
int index = ip.indexOf(',');
if (index != -1) {
return ip.substring(0, index);
} else {
return ip;
}
} else {
return request.getRemoteAddr();
}
}
/**
* 獲取來訪者的瀏覽器版本
* @param request
* @return
*/
public String getRequestBrowserInfo(HttpServletRequest request){
String browserVersion = null;
String header = request.getHeader("user-agent");
if(header == null || header.equals("")){
return "";
}
if(header.indexOf("MSIE")>0){
browserVersion = "IE";
}else if(header.indexOf("Firefox")>0){
browserVersion = "Firefox";
}else if(header.indexOf("Chrome")>0){
browserVersion = "Chrome";
}else if(header.indexOf("Safari")>0){
browserVersion = "Safari";
}else if(header.indexOf("Camino")>0){
browserVersion = "Camino";
}else if(header.indexOf("Konqueror")>0){
browserVersion = "Konqueror";
}
return browserVersion;
}
/**
* 獲取系統(tǒng)版本信息
* @param request
* @return
*/
public String getRequestSystemInfo(HttpServletRequest request){
String systenInfo = null;
String header = request.getHeader("user-agent");
if(header == null || header.equals("")){
return "";
}
//得到用戶的操作系統(tǒng)
if (header.indexOf("NT 6.0") > 0){
systenInfo = "Windows Vista/Server 2008";
} else if (header.indexOf("NT 5.2") > 0){
systenInfo = "Windows Server 2003";
} else if (header.indexOf("NT 5.1") > 0){
systenInfo = "Windows XP";
} else if (header.indexOf("NT 6.0") > 0){
systenInfo = "Windows Vista";
} else if (header.indexOf("NT 6.1") > 0){
systenInfo = "Windows 7";
} else if (header.indexOf("NT 6.2") > 0){
systenInfo = "Windows Slate";
} else if (header.indexOf("NT 6.3") > 0){
systenInfo = "Windows 9";
} else if (header.indexOf("NT 5") > 0){
systenInfo = "Windows 2000";
} else if (header.indexOf("NT 4") > 0){
systenInfo = "Windows NT4";
} else if (header.indexOf("Me") > 0){
systenInfo = "Windows Me";
} else if (header.indexOf("98") > 0){
systenInfo = "Windows 98";
} else if (header.indexOf("95") > 0){
systenInfo = "Windows 95";
} else if (header.indexOf("Mac") > 0){
systenInfo = "Mac";
} else if (header.indexOf("Unix") > 0){
systenInfo = "UNIX";
} else if (header.indexOf("Linux") > 0){
systenInfo = "Linux";
} else if (header.indexOf("SunOS") > 0){
systenInfo = "SunOS";
}
return systenInfo;
}
/**
* 獲取來訪者的主機(jī)名稱
* @param ip
* @return
*/
public String getHostName(String ip){
InetAddress inet;
try {
inet = InetAddress.getByName(ip);
return inet.getHostName();
} catch (UnknownHostException e) {
e.printStackTrace();
}
return "";
}
/**
* 命令獲取mac地址
* @param cmd
* @return
*/
private String callCmd(String[] cmd) {
String result = "";
String line = "";
try {
Process proc = Runtime.getRuntime().exec(cmd);
InputStreamReader is = new InputStreamReader(proc.getInputStream());
BufferedReader br = new BufferedReader (is);
while ((line = br.readLine ()) != null) {
result += line;
}
}catch(Exception e) {
e.printStackTrace();
}
return result;
}
/**
*
*
*
* @param cmd
* 第一個(gè)命令
*
* @param another
* 第二個(gè)命令
*
* @return 第二個(gè)命令的執(zhí)行結(jié)果
*
*/
private String callCmd(String[] cmd,String[] another) {
String result = "";
String line = "";
try {
Runtime rt = Runtime.getRuntime();
Process proc = rt.exec(cmd);
proc.waitFor(); // 已經(jīng)執(zhí)行完第一個(gè)命令,準(zhǔn)備執(zhí)行第二個(gè)命令
proc = rt.exec(another);
InputStreamReader is = new InputStreamReader(proc.getInputStream());
BufferedReader br = new BufferedReader (is);
while ((line = br.readLine ()) != null) {
result += line;
}
}catch(Exception e) {
e.printStackTrace();
}
return result;
}
/**
*
*
*
* @param ip
* 目標(biāo)ip,一般在局域網(wǎng)內(nèi)
*
* @param sourceString
* 命令處理的結(jié)果字符串
*
* @param macSeparator
* mac分隔符號
*
* @return mac地址,用上面的分隔符號表示
*
*/
private String filterMacAddress(final String ip, final String sourceString,final String macSeparator) {
String result = "";
String regExp = "((([0-9,A-F,a-f]{1,2}" + macSeparator + "){1,5})[0-9,A-F,a-f]{1,2})";
Pattern pattern = Pattern.compile(regExp);
Matcher matcher = pattern.matcher(sourceString);
while(matcher.find()){
result = matcher.group(1);
if(sourceString.indexOf(ip) <= sourceString.lastIndexOf(matcher.group(1))) {
break; // 如果有多個(gè)IP,只匹配本IP對應(yīng)的Mac.
}
}
return result;
}
/**
* @param ip
* 目標(biāo)ip
* @return Mac Address
*
*/
private String getMacInWindows(final String ip){
String result = "";
String[] cmd = {"cmd","/c","ping " + ip};
String[] another = {"cmd","/c","arp -a"};
String cmdResult = callCmd(cmd,another);
result = filterMacAddress(ip,cmdResult,"-");
return result;
}
/**
*
* @param ip
* 目標(biāo)ip
* @return Mac Address
*
*/
private String getMacInLinux(final String ip){
String result = "";
String[] cmd = {"/bin/sh","-c","ping " + ip + " -c 2 && arp -a" };
String cmdResult = callCmd(cmd);
result = filterMacAddress(ip,cmdResult,":");
return result;
}
/**
* 獲取MAC地址
*
* @return 返回MAC地址
*/
public String getMacAddress(String ip){
String macAddress = "";
macAddress = getMacInWindows(ip).trim();
if(macAddress==null||"".equals(macAddress)){
macAddress = getMacInLinux(ip).trim();
}
return macAddress;
}
public String getSystemMessage(HttpServletRequest request)
{
String ip = getIpAddr(request);
String messsge = "IP地址為:" + ip + "&瀏覽器版本為:" + getRequestBrowserInfo(request) + "&系統(tǒng)版本為:" + getRequestSystemInfo(request)
+ "&主機(jī)名稱為:" + getHostName(ip) + "&MAC地址為:" + getMacAddress(ip);
return messsge;
}
}
排序算法
/**
* 對參數(shù)按key進(jìn)行字典升序排列
*/
public class SortUtils {
/**
* @param paraMap 參數(shù)
* @param encode 編碼
* @param isLower 是否小寫
* @return
*/
public static String formatUrlParam(Map<String, String> paraMap, String encode, boolean isLower) {
String params = "";
Map<String, String> map = paraMap;
try {
List<Entry<String, String>> itmes = new ArrayList<Entry<String, String>>(map.entrySet());
//對所有傳入的參數(shù)按照字段名從小到大排序
//Collections.sort(items); 默認(rèn)正序
//可通過實(shí)現(xiàn)Comparator接口的compare方法來完成自定義排序
Collections.sort(itmes, new Comparator<Entry<String, String>>() {
@Override
public int compare(Entry<String, String> o1, Entry<String, String> o2) {
// TODO Auto-generated method stub
return (o1.getKey().toString().compareTo(o2.getKey()));
}
});
//構(gòu)造URL 鍵值對的形式
StringBuffer sb = new StringBuffer();
for (Entry<String, String> item : itmes) {
if (StringUtils.isNotBlank(item.getKey())) {
String key = item.getKey();
String val = item.getValue();
val = URLEncoder.encode(val, encode);
if (isLower) {
sb.append(key.toLowerCase() + "=" + val);
} else {
sb.append(key + "=" + val);
}
sb.append("&");
}
}
params = sb.toString();
if (!params.isEmpty()) {
params = params.substring(0, params.length() - 1);
}
} catch (Exception e) {
return "";
}
return params;
}
}
MD5加密算法
public class MD5Utils {
private static final String hexDigIts[] = {"0","1","2","3","4","5","6","7","8","9","a","b","c","d","e","f"};
/**
* MD5加密
* @param origin 字符
* @param charsetname 編碼
* @return
*/
public static String MD5Encode(String origin, String charsetname){
String resultString = null;
try{
resultString = new String(origin);
MessageDigest md = MessageDigest.getInstance("MD5");
if(null == charsetname || "".equals(charsetname)){
resultString = byteArrayToHexString(md.digest(resultString.getBytes()));
}else{
resultString = byteArrayToHexString(md.digest(resultString.getBytes(charsetname)));
}
}catch (Exception e){
}
return resultString;
}
public static String byteArrayToHexString(byte b[]){
StringBuffer resultSb = new StringBuffer();
for(int i = 0; i < b.length; i++){
resultSb.append(byteToHexString(b[i]));
}
return resultSb.toString();
}
public static String byteToHexString(byte b){
int n = b;
if(n < 0){
n += 256;
}
int d1 = n / 16;
int d2 = n % 16;
return hexDigIts[d1] + hexDigIts[d2];
}
}
對客戶端請求參數(shù)進(jìn)行加簽
/**
* 進(jìn)行加簽
*
* @param key 用戶的key
*
* @param valueMap 需要簽名的集合,未處理前的
* @return 處理后,返回的簽名值
*/
public String getSign(String key, Map<String, String> valueMap)
{
String soreValueMap = SortUtils.formatUrlParam(valueMap, "utf-8", true);//對參數(shù)按key進(jìn)行字典升序排列
String signVlue = key + soreValueMap;//將key拼接在請求參數(shù)的前面
String md5SignVlues = MD5Utils.MD5Encode(signVlue, "utf8");//形成MD5加密后的簽名
return md5SignVlues;
}
進(jìn)行驗(yàn)簽
/**
* 進(jìn)行驗(yàn)簽操作
*
* @param valueMap 請求參數(shù)
*
* @param sign 接口調(diào)用方傳過來的sign
*
* @return 驗(yàn)簽成功返回true 否則返回false
*/
public boolean verifySign(Map<String, String> valueMap, String sign)
{
System.out.println("服務(wù)器接收簽名為:"+sign);
String soreValueMap = SortUtils.formatUrlParam(valueMap, "utf-8", true);//對參數(shù)按key進(jìn)行字典升序排列
String signVlue = getMd5Key() + soreValueMap;//將key拼接在請求參數(shù)的前面
String md5SignVlues = MD5Utils.MD5Encode(signVlue, "utf8");//形成MD5加密后的簽名
System.out.println("服務(wù)端處理得到簽名為:"+md5SignVlues);
if(md5SignVlues.equals(sign))
{
return true;
}
return false;
}
測試簽名算法
@Test
// public void testSigm(HttpServletRequest request)
public void testSigm()
{
// SystemUtils systemUtils = new SystemUtils();
//
// System.out.println(systemUtils.getSystemMessage(request));
Map<String, String> map = new HashMap<String, String>();
map.put("a", "200");
map.put("title", "測試標(biāo)題");
map.put("content", "測試內(nèi)容");
map.put("order_no","1807160812023");
Map<String, String> map2 = new HashMap<String, String>();
map2.put("a", "200");
map2.put("title", "測試標(biāo)題");
map2.put("content", "測試內(nèi)容");
map2.put("order_no","1807160812023");
String sign = getSign(getMd5Key(), map);
System.out.println(verifySign(map2, sign));
}
運(yùn)行結(jié)果如下所示:

三、項(xiàng)目源碼下載
鏈接:https://pan.baidu.com/s/1vgUxjtRY-V5TlqHjTcKDBw
提取碼:qy38