API接口的安全設計驗證:ticket,簽名,時間戳


https://mp.weixin.qq.com/s/vdeeypw5juuveY_NEtS1tA

與前端對接的API接口,如果被第三方抓包並進行惡意篡改參數,可能會導致數據泄露,甚至會被篡改數據,我主要圍繞時間戳,token,簽名三個部分來保證API接口的安全性

 

 

1.用戶成功登陸站點后,服務器會返回一個token,用戶的任何操作都必須帶了這個參數,可以將這個參數直接放到header里。

2.客戶端用需要發送的參數和token生成一個簽名sign,作為參數一起發送給服務端,服務端在用同樣的方法生成sign進行檢查是否被篡改。

3.但這依然存在問題,可能會被進行惡意無限制訪問,這時我們需要引入一個時間戳參數,如果超時即是無效的。

4.服務端需要對token,簽名,時間戳進行驗證,只有token有效,時間戳未超時,簽名有效才能被放行。

工具類

package com.example.springdemo1221.util;

import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.security.GeneralSecurityException;
import java.security.MessageDigest;
import java.util.*;

public class SignUtils {

    private static final String secretKeyOfWxh = "e10adc3949ba59abbe56e057f20f883f";

    public static void main(String[] args) {
        //參數簽名算法測試
        HashMap<String, String> signMap = new HashMap<String, String>();
        signMap.put("ticket","a2144337fdcd47daab3ed0623deb1817");
        signMap.put("personId","1");
        signMap.put("ts","1609827917");
        System.out.println("得到簽名sign1:"+getSign(signMap,secretKeyOfWxh));
    }


    /**
     * @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 String utf8Encoding(String value, String sourceCharsetName) {
        try {
            return new String(value.getBytes(sourceCharsetName), "UTF-8");
        } catch (UnsupportedEncodingException e) {
            throw new IllegalArgumentException(e);
        }
    }



    private static byte[] getMD5Digest(String data) throws IOException {
        byte[] bytes = null;
        try {
            MessageDigest md = MessageDigest.getInstance("MD5");
            bytes = md.digest(data.getBytes("UTF-8"));
        } catch (GeneralSecurityException gse) {
            throw new IOException(gse);
        }
        return bytes;
    }


    private static String byte2hex(byte[] bytes) {
        StringBuilder sign = new StringBuilder();
        for (int i = 0; i < bytes.length; i++) {
            String hex = Integer.toHexString(bytes[i] & 0xFF);
            if (hex.length() == 1) {
                sign.append("0");
            }
            sign.append(hex.toUpperCase());
        }
        return sign.toString();
    }


    /**
     * 得到簽名
     * @param params 參數集合不含secretkey
     * @param secret 驗證接口的secretkey
     * @return
     */
    public static String getSign(Map<String, String> params,String secret)
    {
        String sign="";
        StringBuilder sb = new StringBuilder();
        //step1:先對請求參數排序
        Set<String> keyset=params.keySet();
        TreeSet<String> sortSet=new TreeSet<String>();
        sortSet.addAll(keyset);
        Iterator<String> it=sortSet.iterator();
        //step2:把參數的key value鏈接起來 secretkey放在最后面,得到要加密的字符串
        while(it.hasNext())
        {
            String key=it.next();
            String value=params.get(key);
            sb.append(key).append(value);
        }
        sb.append(secret);
        byte[] md5Digest;
        try {
            //得到Md5加密得到sign
            md5Digest = getMD5Digest(sb.toString());
            sign = byte2hex(md5Digest);
        } catch (IOException e) {
            System.out.println("生成簽名錯誤");
        }
        return sign;
    }

    public static long getTimestamp(){
        long timestampLong = System.currentTimeMillis();

        long timestampsStr = timestampLong / 1000;

        return timestampsStr;
    }
}

 

開放接口

沒有進行任何限制,簡單粗暴的訪問方式,這樣的接口方式一般在開放的應用平台,查天氣,查快遞,只要你輸入正確對應的參數調用,即可獲取到自己需要的信息,我們可以任意修改參數值。

/*
 * 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,就不一 一展開了。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM