開放API端口SIGN算法詳細設計
前言
在app開放接口api的設計中,避免不了的就是安全性問題,因為大多數接口涉及到用戶的個人信息以及一些敏感的數據,所以對這些接口需要進行身份的認證,那么這就需要用戶提供一些信息,比如用戶名密碼等,但是為了安全起見讓用戶暴露的明文密碼次數越少越好,我們一般在web項目中,大多數采用保存的session中,然后在存一份到cookie中,來保持用戶的回話有效性。但是在app提供的開放接口中,后端服務器在用戶登錄后如何去驗證和維護用戶的登陸有效性呢,以下是參考項目中設計的解決方案,其原理和大多數開放接口安全驗證一樣,如淘寶的開放接口token驗證,微信開發平台token驗證都是同理。
簽名設計
原理:用戶登錄后向服務器提供用戶認證信息(如賬戶和密碼),服務器認證完后給客戶端返回一個Token令牌,用戶再次獲取信息時,帶上此令牌,如果令牌正取,則返回數據。對於獲取Token信息后,訪問用戶相關接口,客戶端請求的url需要帶上如下參數:
時間戳:timestamp
Token令牌:token
然后將所有用戶請求的參數按照字母排序(包括timestamp,token),然后全部大寫,進行MD5加密,生成sign簽名,這就是所說的URL簽名算法。然后登陸后每次調用用戶信息時,帶上sign,timestamp,token參數。
例如:
原請求: http://dsrrt.xmhcedu.gov.cn/dsideal_yy/html/ypt/index_hc.html?area_id=301053&ticket=ST-1600-Xf2j V9vIDXMqB6bxyL6h-dsssoserver(post和get都一樣,對所有參數排序加密)
加上時間戳和token
http://dsrrt.xmhcedu.gov.cn/dsideal_yy/html/ypt/index_hc.html?area_id=301053&ticket=ST-1600-Xf2j V9vIDXMqB6bxyL6h-dsssoserver×tamp =12445323134&token=wefkfjdskfjewfjkjfdfnc
然后更具url參數生成sign
最終的請求如
http://dsrrt.xmhcedu.gov.cn/dsideal_yy/html/ypt/index_hc.html?area_id=301053&ticket=ST-1600-Xf2j V9vIDXMqB6bxyL6h-dsssoserver×tamp =12445323134&token=wefkfjdskfjewfjkjfdfnc&sign=FDK2434JKJFD334FDF2
最終目的:減小明文的暴露次數;保證數據安全的訪問。
Sign算法及原理:
1. api請求客戶端想服務器端一次發送用用戶認證信息(用戶名和密碼),服務器端請求到改請求后,驗證用戶信息是否正確。
如果正確:則返回一個唯一不重復的字符串(一般為UUID),然后在Redis(任意緩存服務器)中維護Token----Uid的用戶信息關系,以便其他api對token的校驗。
如果錯誤:則返回錯誤碼。
2. 服務器設計一個url請求攔截規則
(1)判斷是否包含timestamp,token,sign參數,如果不含有返回錯誤碼。
(2)判斷服務器接到請求的時間和參數中的時間戳是否相差很長一段時間(時間自定義如半個小時),如果 超過則說明該 url已經過期(如果url被盜,他改變了時間戳,但是會導致sign簽名不相等)。
(3)判斷token是否有效,根據請求過來的token,查詢redis緩存中的uid,如果獲取不到這說明該token 已過期。
(4)根據用戶請求的url參數,服務器端按照同樣的規則生成sign簽名,對比簽名看是否相等,相等則放行。
(5)此url攔截只需對獲取身份認證的url放行使用API,剩余所有的url都需攔截。
3. Token和Uid關系維護
對於用戶登錄我們需要創建token--uid的關系,用戶退出時需要需刪除token--uid的關系。
簽名實現
1.獲取全部請求參數
Public Map<String,String> getParamsMap(HttpServletRequest request) throws ServletException,IOException{
//得到請求的參數Map,注意map的value是String數組類型
Map<String ,String> params = new HashMap<String, String>();
Map<String, String[]> map = request.getParameterMap();
Set<String> keySet = map.keySet();
for (String key : keySet) {
String[] values = (String[]) map.get(key);
for (String value : values) {
//System.out.println(key+"="+value);
}
}
//System.out.println("--------request.getParameter()--------");
//得到請求頭的name集合
Enumeration<String> em = request.getParameterNames();
System.out.println(em);
while (em.hasMoreElements()) {
String name = (String) em.nextElement();
String value = request.getParameter(name);
System.out.println(name+"="+value);
params.put(name,value);
}
Return params;
}
2. 生成簽名
代碼如下:
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.Arrays;
import java.util.Map;
import java.util.Set;
public class Sign {
public static String createSign(Map<String, String> params, boolean encode)
throws UnsupportedEncodingException {
Set<String> keysSet = params.keySet();
Object[] keys = keysSet.toArray();
Arrays.sort(keys);
StringBuffer temp = new StringBuffer();
boolean first = true;
for (Object key : keys) {
if (first) {
first = false;
} else {
temp.append("&");
}
temp.append(key).append("=");
Object value = params.get(key);
String valueString = "";
if (null != value) {
valueString = String.valueOf(value);
}
if (encode) {
temp.append(URLEncoder.encode(valueString, "UTF-8"));
} else {
temp.append(valueString);
}
}
return MD5Util.md5(temp.toString()).toUpperCase();
}
}
3.時間戳
//獲取系統時間作為 時間戳
long timestamp = System.currentTimeMillis();
4.Token令牌:token
對於用戶登錄我們需要創建token--uid的關系,用戶退出時需要需刪除token--uid的關系。
生成token--uid關系:
1) 登陸成功
//登陸成功,隨機生成uid
UUID uid = UUID.randomUUID();
//在redis中新增token--uid
String token = MD5Util.md5(userID+System.currentTimeMillis());
Jedis jedis = new Jedis("localhost",port);
jedis .set(token ,uid);
//設置過期時間為60*60*60秒-
jedis .expire(token ,60*60*60);
2) 退出時刪除
//用戶退出時需要需刪除token--uid的關系
jedis .del(token );
5.URL攔截請求類
public class InterceptorUID implements HandlerInterceptor {
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
Map<String ,String> params = new HashMap<String, String>();
String encryptionURL = "";
//得到請求頭的name集合
Enumeration<String> em = request.getParameterNames();
while (em.hasMoreElements()) {
String name = (String) em.nextElement();
String value = request.getParameter(name);
System.out.println(name+"="+value);
params.put(name,value);
}
if(params !=null){
encryptionURL = Sign.createSign(params, true);
System.out.println("加密后的URL:"+encryptionURL);
}
for(Map.Entry<String, String> entry:params.entrySet()){
String name = entry.getKey();
String value = entry.getValue();
//System.out.println(name+"="+value);
if("timestamp".equals(name) ){
request.setAttribute("timestamp", value);
Long howLong =System.currentTimeMillis()-Long.parseLong(value);
//(2)判斷服務器接到請求的時間和參數中的時間戳是否相差超過一個小時,如果超過則說明該url已經過期.
if (howLong>60*60*60){
throw new Exception("URL過期");
}
return true;
}
//判斷token是否有效,根據請求過來的token,查詢redis緩存中的uid,如果獲取不到這說明該token已過期。
if("token".equals(name)){
Jedis jedis = new Jedis("localhost",port);
if(!jedis.exists(value)){
throw new Exception("Token已過期");
}
}
//根據用戶請求的url參數,服務器端按照同樣的規則生成sign簽名,對比簽名看是否相等,相等則放行。
if("sign".equals(name)){
if(value.equals(encryptionURL)){
return true;
}else{
return false;
}
}
}
//返回錯誤
throw new Exception("URL無效");
}
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
ModelAndView modelAndView) throws Exception {
// TODO Auto-generated method stub
}
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception e)throws Exception {
// TODO Auto-generated method stub
}
}
使用到的工具類
1.自定義MD5
/**
* 加密算法類 MD5
* 64位加密
*
*/
public class MD5Util {
public static String md5(String src){
try {
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] output=md.digest(src.getBytes());
String ret = Base64.encodeBase64String(output);
return ret;
} catch (Exception e) {
throw new Md5Exception("加密失敗!",e);
}
}
}
2.查詢Redis
連接Redis使用Jedis2.2穩定版,Maven Dependency如下:
<dependency>
<groupId>net.heartsavior</groupId>
<artifactId>jedis</artifactId>
<version>2.2.1.1</version>
</dependency>
廈門理想雲:林漢欽
2017-7-4
