RestFul介面的安全驗證事例
阿新 • • 發佈:2019-01-27
這次要寫一些restful的介面,在訪問安全這一塊最開始我想到用redis儲存模擬session,客戶端訪問會帶token過來模擬jsessionid,然後比對,但是這樣會讓token暴露在網路中,很不安全而且沒有意義。
- 其實可以用簽名的方法來解決這個問題:
首先:client開通服務的時候,server會給它建立authKey和一個token,authKey相當於是公鑰可以暴露在網路中,token是私鑰不會暴露在網路中
然後:client請求服務端的時候在header中新增請求的時間戳,並且對傳送的資訊以及時間戳和token拼接起來的字串進行簽名(可以採用MD5或者SHA-0、SHA-1等)
最後:把簽名串以及資訊和header的資訊傳送到服務端,然後服務端會取出header資訊以及簽名串和資訊,先對header裡面的時間和伺服器接收到的時間校驗然後求差值如果大於多少秒就返回時間錯誤(防止重複攻擊),然後在伺服器端也對資料拼接簽名,最後對比簽名是否相等,如果不等就返回錯誤資訊,如果相等下面就可以開始處理業務資訊。
- 剛寫了份demo程式碼 結構如下:
- 工程中使用springmvc釋出服務,其中controller中的程式碼如下:
package com.lijie.api;
import java.util.Calendar;
import javax.servlet.http.HttpServletRequest;
import org.springframework.stereotype.Controller;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import com.lijie.utils.Signature;
/**
*
* @author Lijie
*
*/
@Controller
public class ApiAuth {
/**
* 測試auth
* @param request
* @param authKey
* @param sign
* @param info
* @return
* @throws Exception
*/
@RequestMapping(value = "/api/auth/test", method = RequestMethod.POST)
@ResponseBody
public String testAuth( HttpServletRequest request, String authKey, String sign,
String info) throws Exception {
String reqTime = request.getHeader("ReqTime-Time");
//這裡的token應該是根據客戶端那邊傳來的authKey從redis裡面將token取出來,我這裡直接寫死了
String token = "lijieauthtest01";
String check = check(reqTime, info, token, sign);
System.out.println("服務端:" + check);
//do service
return check;
}
/**
* 校驗引數和簽名
*
* @param reqTime
* @param info
* @param token
* @param sign
* @return
* @throws Exception
*/
private String check(String reqTime, String info, String token, String sign) throws Exception {
if (StringUtils.isEmpty(reqTime)) {
return "頭部時間為空";
}
if (StringUtils.isEmpty(info)) {
return "資訊為空";
}
if (StringUtils.isEmpty(token)) {
return "沒有授權";
}
if (StringUtils.isEmpty(sign)) {
return "簽名為空";
}
long serverTime = Calendar.getInstance().getTimeInMillis();
long clientTime = Long.parseLong(reqTime);
long flag = serverTime - clientTime;
if (flag < 0 || flag > 5000) {
return "時間錯誤";
}
String allStr = info + reqTime + token;
String md5 = Signature.md5(allStr);
if (sign.equals(md5)) {
System.out.println("服務端未簽名時為:" + allStr);
System.out.println("服務端簽名之後為:" + md5);
return "簽名成功";
}
return "簽名錯誤";
}
}
- 測試httpclient請求的程式碼如下:
package com.lijie.test;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;
import org.apache.http.NameValuePair;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.message.BasicHeader;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;
import org.junit.Test;
import com.lijie.utils.Signature;
public class MyTest {
private static final String token = "lijieauthtest01";
@Test
public void authTest() throws Exception {
//建立一個httpclient物件
CloseableHttpClient client = HttpClients.createDefault();
//建立一個post物件
HttpPost post = new HttpPost("http://localhost:8080/api/auth/test");
//建立一個Entity,模擬表單資料
List<NameValuePair> formList = new ArrayList<>();
//新增表單資料
long clientTime = Calendar.getInstance().getTimeInMillis();
String info = "這是一個測試 restful api的 info";
String reqStr = info + clientTime + token;
formList.add(new BasicNameValuePair("authKey", "1000001"));
formList.add(new BasicNameValuePair("info", info));
String md5 = Signature.md5(reqStr);
formList.add(new BasicNameValuePair("sign", md5));
//包裝成一個Entity物件
StringEntity entity = new UrlEncodedFormEntity(formList, "utf-8");
//設定請求的內容
post.setEntity(entity);
//設定請求的報文頭部的編碼
post.setHeader(
new BasicHeader("Content-Type", "application/x-www-form-urlencoded; charset=utf-8"));
//設定期望服務端返回的編碼
post.setHeader(new BasicHeader("Accept", "text/plain;charset=utf-8"));
//設定時間
post.setHeader(new BasicHeader("ReqTime-Time", clientTime + ""));
System.out.println("客戶端未簽名時為:" + reqStr);
System.out.println("客戶端簽名之後為:" + md5);
//執行post請求
CloseableHttpResponse response = client.execute(post);
//獲取響應碼
int statusCode = response.getStatusLine().getStatusCode();
if (statusCode == 200) {
//獲取資料
String resStr = EntityUtils.toString(response.getEntity());
//輸出
System.out.println("請求成功,請求返回內容為: " + resStr);
} else {
//輸出
System.out.println("請求失敗,錯誤碼為: " + statusCode);
}
//關閉response和client
response.close();
client.close();
}
}
- Signature類其實就是MD5加密的一個工具類,程式碼如下:
package com.lijie.utils;
import java.security.MessageDigest;
/**
*
* @author Lijie
*
*/
public class Signature {
public static String md5(String data) throws Exception {
// 將字串轉為位元組陣列
byte[] strBytes = (data).getBytes();
// 加密器
MessageDigest md = MessageDigest.getInstance("MD5");
// 執行加密
md.update(strBytes);
// 加密結果
byte[] digest = md.digest();
// 返回
return byteArrayToHex(digest);
}
private static String byteArrayToHex(byte[] byteArray) {
// 首先初始化一個字元陣列,用來存放每個16進位制字元
char[] hexDigits = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D',
'E', 'F' };
// new一個字元陣列,這個就是用來組成結果字串的(解釋一下:一個byte是八位二進位制,也就是2位十六進位制字元(2的8次方等於16的2次方))
char[] resultCharArray = new char[byteArray.length * 2];
// 遍歷位元組陣列,通過位運算(位運算效率高),轉換成字元放到字元陣列中去
int index = 0;
for (byte b : byteArray) {
resultCharArray[index++] = hexDigits[b >>> 4 & 0xf];
resultCharArray[index++] = hexDigits[b & 0xf];
}
// 字元陣列組合成字串返回
return new String(resultCharArray);
}
}
- 開啟服務,測試訪問,其中服務端輸出:
- 客戶端輸出: