接口的安全設(shè)計(jì)要素:ticket,簽名,時(shí)間戳
不點(diǎn)藍(lán)字,我們哪來故事?

每天 11 點(diǎn)更新文章,餓了點(diǎn)外賣,點(diǎn)擊 ??《無(wú)門檻外賣優(yōu)惠券,每天免費(fèi)領(lǐng)!》

概述
與前端對(duì)接的API接口,如果被第三方抓包并進(jìn)行惡意篡改參數(shù),可能會(huì)導(dǎo)致數(shù)據(jù)泄露,甚至?xí)淮鄹臄?shù)據(jù),我主要圍繞時(shí)間戳,token,簽名三個(gè)部分來保證API接口的安全性

1.用戶成功登陸站點(diǎn)后,服務(wù)器會(huì)返回一個(gè)token,用戶的任何操作都必須帶了這個(gè)參數(shù),可以將這個(gè)參數(shù)直接放到header里。
2.客戶端用需要發(fā)送的參數(shù)和token生成一個(gè)簽名sign,作為參數(shù)一起發(fā)送給服務(wù)端,服務(wù)端在用同樣的方法生成sign進(jìn)行檢查是否被篡改。
3.但這依然存在問題,可能會(huì)被進(jìn)行惡意無(wú)限制訪問,這時(shí)我們需要引入一個(gè)時(shí)間戳參數(shù),如果超時(shí)即是無(wú)效的。
4.服務(wù)端需要對(duì)token,簽名,時(shí)間戳進(jìn)行驗(yàn)證,只有token有效,時(shí)間戳未超時(shí),簽名有效才能被放行。
開放接口
沒有進(jìn)行任何限制,簡(jiǎn)單粗暴的訪問方式,這樣的接口方式一般在開放的應(yīng)用平臺(tái),查天氣,查快遞,只要你輸入正確對(duì)應(yīng)的參數(shù)調(diào)用,即可獲取到自己需要的信息,我們可以任意修改參數(shù)值。
/*
?*?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?openDemo(int?personId) {
????????return?tokenSignService.getPersonList(personId);
????}
}
Token認(rèn)證獲取
用戶登錄成功后,會(huì)獲取一個(gè)ticket值,接下去任何接口的訪問都需要這個(gè)參數(shù)。我們把它放置在redis內(nèi),有效期為10分鐘,在ticket即將超時(shí),無(wú)感知續(xù)命。延長(zhǎng)使用時(shí)間,如果用戶在一段時(shí)間內(nèi)沒進(jìn)行任何操作,就需要重新登錄系統(tǒng)。擴(kuò)展:記一次token安全認(rèn)證的實(shí)踐
@RequestMapping(value?=?"login",method?=?RequestMethod.POST)
????public?JSONObject?login(@NotNull?String?username,?@NotNull?String?password){
????????return?tokenSignService.login(username,password);
????}
登錄操作,查看是否有這個(gè)用戶,用戶名和密碼匹配即可成功登錄。
/**?
?????*?
?????*?Description:驗(yàn)證登錄,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","未知異常,請(qǐng)重試");
????????return?result;
????}
Sign簽名
把所有的參數(shù)拼接一起,在加入系統(tǒng)秘鑰,進(jìn)行MD5計(jì)算生成一個(gè)sign簽名,防止參數(shù)被人惡意篡改,后臺(tái)按同樣的方法生成秘鑰,進(jìn)行簽名對(duì)比。
/**
?????*?@param?request
?????*?@return
?????*/
????public?static?Boolean?checkSign(HttpServletRequest?request,String?sign){
????????Boolean?flag=?false;
????????//檢查sigin是否過期
????????Enumeration>?pNames?=??request.getParameterNames();
????????Map?params?=?new?HashMap();
????????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("現(xiàn)在的sign-->>"?+?sign);
????????System.out.println("驗(yàn)證的sign-->>"?+?getSign(params,secretKeyOfWxh));
????????if(sign.equals(getSign(params,?secretKeyOfWxh))){
????????????flag?=?true;
????????}
????????return?flag;
????}
重復(fù)訪問
引入一個(gè)時(shí)間戳參數(shù),保證接口僅在一分鐘內(nèi)有效,需要和客戶端時(shí)間保持一致。
public?static?long?getTimestamp(){
????????long?timestampLong?=?System.currentTimeMillis();
????????long?timestampsStr?=?timestampLong?/?1000;
????????return?timestampsStr;
????}
需要跟當(dāng)前服務(wù)器時(shí)間進(jìn)行對(duì)比,如果超過一分鐘,就拒絕本次請(qǐng)求,節(jié)省服務(wù)器查詢數(shù)據(jù)的消耗
攔截器
每次請(qǐng)求都帶有這三個(gè)參數(shù),我們都需要進(jìn)行驗(yàn)證,只有在三個(gè)參數(shù)都滿足我們的要求,才允許數(shù)據(jù)返回或被操作。
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就認(rèn)為是合法的請(qǐng)求
????????if?(redisTemplate.hasKey(ticket)){
????????????System.out.println(redisTemplate.opsForValue().getOperations().getExpire(ticket));
????????????String?values?=?(String)?redisTemplate.opsForValue().get(ticket);
????????????//判斷ticket是否即將過期,進(jìn)行續(xù)命操作
????????????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());
????????????//判斷是否重復(fù)訪問,存在重放攻擊的時(shí)間窗口期
????????????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;
????????????}
????????????//驗(yàn)證簽名
????????????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;
????}
}
訪問
先登錄系統(tǒng),獲取合法的ticket

生成一個(gè)合法的sign驗(yàn)證,獲取測(cè)試ts,訪問openDemo,即可正常訪問。還可以將參數(shù)加密,將http換成https,就不一 一展開了。

demo代碼
https://github.com/hwc4110/spring-demo1221
往期推薦
下方二維碼關(guān)注我

技術(shù)草根,堅(jiān)持分享?編程,算法,架構(gòu)

看完文章,餓了點(diǎn)外賣,點(diǎn)擊 ??《無(wú)門檻外賣優(yōu)惠券,每天免費(fèi)領(lǐng)!》

