概述
與前端對接的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即將超時,無感知續命。延長使用時間,如果用戶在一段時間內沒進行任何操作,就需要重新登錄系統。擴展:記一次token安全認證的實踐
@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
作者:一劍天門
cnblogs.com/dslx/p/14116294.html