一.背景
1.與前端對接的API介面,如果被第三方抓包並進行惡意篡改引數,可能會導致資料洩露,甚至會被篡改資料
2.與第三方公司的介面對接,第三方如果得到你的介面文件,但是介面確沒安全校驗,是十分不安全的
我主要圍繞時間戳,token,簽名三個部分來保證API介面的安全性
二.請求過程
1.使用者成功登陸站點後,伺服器會返回一個token,使用者的任何操作都必須帶了這個引數,可以將這個引數直接放到header裡。
2.客戶端用需要傳送的引數和token生成一個簽名sign,作為引數一起傳送給服務端,服務端在用同樣的方法生成sign進行檢查是否被篡改。
3.但這依然存在問題,可能會被進行惡意無限制訪問,這時我們需要引入一個時間戳引數,如果超時即是無效的。
4.服務端需要對token,簽名,時間戳進行驗證,只有token有效,時間戳未超時,簽名有效才能被放行。
概念:
(1)開放介面
沒有進行任何限制,簡單粗暴的訪問方式,這樣的介面方式一般在開放的應用平臺,查天氣,查快遞,只要你輸入正確對應的引數呼叫,即可獲取到自己需要的資訊,我們可以任意修改引數值。
(2)Token認證獲取
使用者登入成功後,會獲取一個ticket值,接下去任何介面的訪問都需要這個引數。我們把它放置在redis內,有效期為10分鐘,在ticket即將超時,無感知續命。延長使用時間,如果使用者在一段時間內沒進行任何操作,就需要重新登入系統。
(3)Sign簽名
把所有的引數拼接一起,在加入系統祕鑰,進行MD5計算生成一個sign簽名,防止引數被人惡意篡改,後臺按同樣的方法生成祕鑰,進行簽名對比。
(4)重複訪問
引入一個時間戳引數,保證介面僅在一分鐘內有效,需要和客戶端時間保持一致。
(5)攔截器
每次請求都帶有這三個引數,我們都需要進行驗證,只有在三個引數都滿足我們的要求,才允許資料返回或被操作。
三.具體程式碼實現
1.編寫獲取tiket的介面
/** * 獲取tiket * @param receiveRequest * @return */ @ResponseBody @RequestMapping(value = "/gettiket",method = RequestMethod.POST) public String gettiket(@RequestBody String data){ String result = ""; String msg = ""; try{ log.info("gettiket,入參為==="+data); JdbcTemplate jdbcTemplate = new JdbcTemplate(); String userTocken = UUID.randomUUID().toString(); //cache.put(userTocken, userMap);//資料庫方式或者redis方式,這裡用資料庫方式 String insert_user_token_sql = "insert into user_token(pk_user_token,userid,user_token) VALUES (?,?,?)"; long pk_user_token = KeyUtils.nextId();//主鍵 jdbcTemplate.executeUpdate(insert_user_token_sql, new Object[]{ pk_user_token,"111",userTocken }); result = userTocken; msg = "{\"success\" : true,\"errorCode\" : \"200\", \"errorMsg\" : \"查詢完成\", \"tiket\" :" +result + "}"; log.info("msg===="+msg); return msg; }catch(Exception e){ msg = "{\"success\" : true,\"errorCode\" : \"500\", \"errorMsg\" : \"查詢完成\", \"data\" :" +e + "}"; return msg; } }
2.服務端驗證
主程式入口
Map<String, String> paramMap = new HashMap<>(); String time = DateUtils.formatDate("yyyy-MM-dd HH:mm:ss.SSS"); paramMap.put("time", time); String ticket = "056a3d29-eed3-4ee9-80aa-c03321d5302f"; paramMap.put("ticket", ticket);//userTock為我第一次請求你的單點url時傳給你的userTocken String serviceCode = "cs_demo";// 目標系統對應的金鑰 String sign = null; try { sign = SignUtils.sing(paramMap, serviceCode, "UTF-8"); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } log.info("sign==="+sign); CheckPerService checkPerService= new CheckPerService(); Boolean istrue = checkPerService.TicketSignAndTime( ticket, sign, time, serviceCode); log.info("istrue==="+istrue);
工具類SignUtils
package tcc.test.utill; import org.apache.log4j.Logger; import org.springframework.util.DigestUtils; import java.io.UnsupportedEncodingException; import java.util.*; public class SignUtils { private static final Logger a = Logger.getRootLogger(); public SignUtils() { } public static String getContent(Map params) { List keys = new ArrayList(params.keySet()); Collections.sort(keys); String prestr = ""; boolean first = true; for(int i = 0; i < keys.size(); ++i) { String key = (String)keys.get(i); if (!"sign".equals(key) && !"_r".equals(key) && !"_result_type".equals(key) && !"_".equals(key)) { String value = String.valueOf(params.get(key)); if (value != null && value.trim().length() != 0) { if (first) { prestr = prestr + key + "=" + value; first = false; } else { prestr = prestr + "&" + key + "=" + value; } } } } a.info("加密字串:" + prestr); return prestr; } public static String sing(Map Params, String key, String charset) throws UnsupportedEncodingException { String signStr = null; signStr = DigestUtils.md5DigestAsHex((getContent(Params) + key).getBytes(charset)); return signStr; } public static void main(String[] args) throws Exception { Map paramMap = new HashMap<String,String>(); paramMap.put("name","tcc"); paramMap.put("age","24"); String serviceCode = "siruinet"; String sing = SignUtils.sing(paramMap, serviceCode, "UTF-8"); System.out.println(sing); } }
許可權校驗工具類
package tcc.test.utill; import com.alibaba.druid.util.StringUtils; import com.util.FieldList; import jos.engine.core.jdbc.JdbcTemplate; import jos.engine.des.util.DesEncryptUtils; import org.apache.log4j.Logger; import java.util.HashMap; import java.util.Map; /** * Copyright (C) @2022 * * @author: tcc * @version: 1.0 * @date: 2022/1/31 * @time: 2:08 * @description: */ public class CheckPerService{ private static final Logger log = Logger.getRootLogger(); /* 介面許可權校驗方法1 ticket:票據 sign:簽名 time:時間戳 serviceCode:服務編碼*/ public static boolean TicketSignAndTime(String ticket, String sign, String time, String serviceCode){ time = time; ticket = ticket; sign = sign; Map<String, String> paramMap = new HashMap<>(); paramMap.put("time", time); paramMap.put("ticket", ticket);//ticket為第一次呼叫獲取ticket介面的資料 serviceCode = serviceCode;// 目標系統對應的金鑰 String qm = DesEncryptUtils.sing(paramMap, serviceCode, "UTF-8"); log.info("qm==="+qm); if (!StringUtils.equals(sign, qm)) { //金鑰校驗錯誤 log.info("簽名不正確"); return false; } log.info("簽名正確"); JdbcTemplate jdbcTemplate = new JdbcTemplate(); String qr_user_token_sql = "select count(1) as count from user_token where user_token = ?";//後期改成redis FieldList file_token = jdbcTemplate.queryField(qr_user_token_sql, new Object[]{ ticket }); int count = Integer.parseInt(file_token.get("count")); if(count<1){ return false; } return true; } /* 介面許可權校驗方法2 name:使用者名稱 pwd:密碼 */ public static boolean UnmAndPwd(String name,String pwd){ JdbcTemplate jdbcTemplate = new JdbcTemplate("mzdb"); String qr_user_token_sql = "select count(1) as count from bd_user where USERNAME = ? and USERPASS = ?";//後期改成redis FieldList file_token = jdbcTemplate.queryField(qr_user_token_sql, new Object[]{ name,pwd }); int count = Integer.parseInt(file_token.get("count")); if(count<1){ return false; } return true; } }