概述
與前端對接的API介面,如果被第三方抓包並進行惡意篡改引數,可能會導致資料洩露,甚至會被篡改資料,我主要圍繞時間戳,token,簽名三個部分來保證API介面的安全性
1.使用者成功登陸站點後,伺服器會返回一個token,使用者的任何操作都必須帶了這個引數,可以將這個引數直接放到header裡。
2.客戶端用需要傳送的引數和token生成一個簽名sign,作為引數一起傳送給服務端,服務端在用同樣的方法生成sign進行檢查是否被篡改。
3.但這依然存在問題,可能會被進行惡意無限制訪問,這時我們需要引入一個時間戳引數,如果超時即是無效的。
4.服務端需要對token,簽名,時間戳進行驗證,只有token有效,時間戳未超時,簽名有效才能被放行。
開放介面
沒有進行任何限制,簡單粗暴的訪問方式,這樣的介面方式一般在開放的應用平臺,查天氣,查快遞,只要你輸入正確對應的引數呼叫,即可獲取到自己需要的資訊,我們可以任意修改引數值。
* * Description: 開放的介面 * @author huangweicheng * @date 2020/12/21 */ @RestController @RequestMapping("/token") public class TokenSignController { @Autowired private TokenSignService tokenSignService; @RequestMapping(value = "openDemo",method = RequestMethod.GET) public List<PersonEntity> openDemo(int personId){ return tokenSignService.getPersonList(personId); } }
Token認證獲取
使用者登入成功後,會獲取一個ticket值,接下去任何介面的訪問都需要這個引數。我們把它放置在redis內,有效期為10分鐘,在ticket即將超時,無感知續命。延長使用時間,如果使用者在一段時間內沒進行任何操作,就需要重新登入系統。
@RequestMapping(value = "login",method = RequestMethod.POST) public JSONObject login(@NotNull String username, @NotNull String password){ return tokenSignService.login(username,password); }
登入操作,檢視是否有這個使用者,使用者名稱和密碼匹配即可成功登入。
/** * * Description:驗證登入,ticket成功後放置快取中, * @param * @author huangweicheng * @date 2020/12/31 */ public JSONObject login(String username,String password){ JSONObject result = new JSONObject(); PersonEntity personEntity = personDao.findByLoginName(username); if (personEntity == null || (personEntity != null && !personEntity.getPassword().equals(password))){ result.put("success",false); result.put("ticket",""); result.put("code","999"); result.put("message","使用者名稱和密碼不匹配"); return result; } if (personEntity.getLoginName().equals(username) && personEntity.getPassword().equals(password)){ String ticket = UUID.randomUUID().toString(); ticket = ticket.replace("-",""); redisTemplate.opsForValue().set(ticket,personEntity.getLoginName(),10L, TimeUnit.MINUTES); result.put("success",true); result.put("ticket",ticket); result.put("code",200); result.put("message","登入成功"); return result; } result.put("success",false); result.put("ticket",""); result.put("code","1000"); result.put("message","未知異常,請重試"); return result; }
Sign簽名
把所有的引數拼接一起,在加入系統祕鑰,進行MD5計算生成一個sign簽名,防止引數被人惡意篡改,後臺按同樣的方法生成祕鑰,進行簽名對比。
/** * @param request * @return */ public static Boolean checkSign(HttpServletRequest request,String sign){ Boolean flag= false; //檢查sigin是否過期 Enumeration<?> pNames = request.getParameterNames(); Map<String, String> params = new HashMap<String, String>(); while (pNames.hasMoreElements()) { String pName = (String) pNames.nextElement(); if("sign".equals(pName)) continue; String pValue = (String)request.getParameter(pName); params.put(pName, pValue); } System.out.println("現在的sign-->>" + sign); System.out.println("驗證的sign-->>" + getSign(params,secretKeyOfWxh)); if(sign.equals(getSign(params, secretKeyOfWxh))){ flag = true; } return flag; }
重複訪問
引入一個時間戳引數,保證介面僅在一分鐘內有效,需要和客戶端時間保持一致。
public static long getTimestamp(){ long timestampLong = System.currentTimeMillis(); long timestampsStr = timestampLong / 1000; return timestampsStr; }
需要跟當前伺服器時間進行對比,如果超過一分鐘,就拒絕本次請求,節省伺服器查詢資料的消耗
攔截器
每次請求都帶有這三個引數,我們都需要進行驗證,只有在三個引數都滿足我們的要求,才允許資料返回或被操作。
public class LoginInterceptor implements HandlerInterceptor { @Autowired private RedisTemplate redisTemplate; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response,Object handler) throws IOException { JSONObject jsonObject = new JSONObject(); String ticket = request.getParameter("ticket"); String sign = request.getParameter("sign"); String ts = request.getParameter("ts"); if (StringUtils.isEmpty(ticket) || StringUtils.isEmpty(sign) || StringUtils.isEmpty(ts)){ jsonObject.put("success",false); jsonObject.put("message","args is isEmpty"); jsonObject.put("code","1001"); PrintWriter printWriter = response.getWriter(); printWriter.write(jsonObject.toJSONString()); return false; } //如果redis存在ticket就認為是合法的請求 if (redisTemplate.hasKey(ticket)){ System.out.println(redisTemplate.opsForValue().getOperations().getExpire(ticket)); String values = (String) redisTemplate.opsForValue().get(ticket); //判斷ticket是否即將過期,進行續命操作 if (redisTemplate.opsForValue().getOperations().getExpire(ticket) != -2 && redisTemplate.opsForValue().getOperations().getExpire(ticket) < 20){ redisTemplate.opsForValue().set(ticket,values,10L, TimeUnit.MINUTES); } System.out.println(SignUtils.getTimestamp()); //判斷是否重複訪問,存在重放攻擊的時間視窗期 if (SignUtils.getTimestamp() - Long.valueOf(ts) > 600){ jsonObject.put("success",false); jsonObject.put("message","Overtime to connect to server"); jsonObject.put("code","1002"); PrintWriter printWriter = response.getWriter(); printWriter.write(jsonObject.toJSONString()); return false; } //驗證簽名 if (!SignUtils.checkSign(request,sign)){ jsonObject.put("success",false); jsonObject.put("message","sign is invalid"); jsonObject.put("code","1003"); PrintWriter printWriter = response.getWriter(); printWriter.write(jsonObject.toJSONString()); return false; } return true; }else { jsonObject.put("success",false); jsonObject.put("message","ticket is invalid,Relogin."); jsonObject.put("code","1004"); PrintWriter printWriter = response.getWriter(); printWriter.write(jsonObject.toJSONString()); } return false; } }
訪問
先登入系統,獲取合法的ticket
生成一個合法的sign驗證,獲取測試ts,訪問openDemo,即可正常訪問。還可以將引數加密,將http換成https,就不一 一展開了。
demo程式碼 https://github.com/hwc4110/spring-demo1221