1. 程式人生 > 程式設計 >常用系統間介面呼叫認證設計/剖析/程式碼實現

常用系統間介面呼叫認證設計/剖析/程式碼實現

簡介

本文實現介面加密簽名及校驗。 可適用於絕大多數系統間介面呼叫。

簽名實現

請求籤名實現過程

  1. 將當前請求路徑(不含域名。如:http:// x x x.com/sys/user/list,其中請求路徑即為/sys/user/list)作為URL引數的值(例如:URL=/sys/user/list),加上當前請求引數,對這些引數名進行升序排序,排序之後生成請求引數字串queryStr(拼接時要對引數值進行URLEncoder.encode編碼,防止中文等問題),形如:引數a=xx&引數b=22。
  2. 系統間約定加密字串key【houdask2019】 ,生成當前時間戳time(毫秒數),獲取當前使用者編號 uid(注意uid key time 這三個引數均不參與排序)
  3. 將步驟1中生成的queryStr 和 time 、key 、uid 進行拼接,形成待加密字串str。形如 queryStr &time=xx&key=xxx&uid=xxx
  4. 對待加密字串str進行MD5加密,並轉化成大寫,即生成簽名字串hash。
  5. 客戶端將uid放到請求header中,將原始請求引數和time、hash一起作引數傳遞(注意:其中URL引數不必傳遞)。

校驗請求籤名過程

  1. 從請求header中獲取使用者編號uid
  2. 從請求引數裡獲取簽名字串hash,以及請求時間time
  3. 獲取當前請求路徑,作為引數名為URL的引數值。【URL屬於隱藏引數】
  4. 再將請求引數進行一遍簽名加密,生成出來正確的簽名字串hash2
  5. 比較hash和hash2即可

優點:

  1. 引數防篡改(篡改引數之後簽名不一致)
  2. 簽名防串用(防止多個介面引數相同)
  3. 防過期呼叫(需校驗time在三分鐘或者更短時間內)
  4. 防暴力破解(含隱形引數,隱性引數名可以不用URL換成其他變數,增加安全性)
  5. 不可逆加密
  6. 簡單易用,安全性高

缺點:

嚴重依賴key的保密性,如果key洩露和演演算法暴露,安全性就有問題。 建議不定時更改key。

程式碼實現

public class FkSignUtil {
    public static final String UID = "uid";
    /**
     * 加密祕鑰
     */
    private static final String KEY = "key可以自定義"
; /** * 日誌物件 */ private static Logger logger = LoggerFactory.getLogger(FkSignUtil.class); /** * 生成簽名【注意傳送請求時一定要帶上time 和hash 這2個引數】 * * 功能:將一個Map按照Key字母升序構成一個QueryString. 並且加入時間混淆的hash串 * @param queryMap query內容 * @param time 加密時候,為當前時間;解密時,為從querystring得到的時間; * @param uid 表示當前使用者id * @return */ public static String createSign(Map<String,Object> queryMap,long time,String uid) { if(null == uid || "".equals(uid)){ return null ; } String qs = sortQueryParamString(queryMap); if (qs == null) { return null; } String hash = MD5Util.MD5(String.format("%s&time=%d&key=%s&uid=%s",qs,time,KEY,uid)); hash = hash.toUpperCase(); return hash; // String params = String.format("%s&time=%d&hash=%s",hash); // return params; } /** * 對請求引數進行排序 * @param params 請求引數 。注意請求引數中不能包含uid、time【這2引數是排序之後拼接的】 * @return */ private static String sortQueryParamString(Map<String,Object> params) { List<String> listKeys = Lists.newArrayList( params.keySet()); Collections.sort(listKeys); StringBuilder content = new StringBuilder(); for(String param : listKeys){ try { content.append(param).append("=").append(URLEncoder.encode(params.get(param).toString(),"UTF-8")).append("&"); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } } if(content.length()> 0){ return content.substring(0,content.length() -1 ); } return content.toString(); } /** * 解密判斷是否簽名正確 * @param params 請求引數map【從request的引數中獲取】 * @param uid 表示當前使用者id【從header中獲取】 * @return */ public static boolean checkHashSign(Map<String,Object> params,String uid) { if(null == uid || "".equals(uid)){ if (logger.isInfoEnabled()) { logger.info("checkHashSign ERROR: uid is null."); } return false ; } if (!params.containsKey("hash") || !params.containsKey("time") ) { if (logger.isInfoEnabled()) { logger.info("checkHashSign ERROR: hash or time is null."); } return false; } String hash = (String) params.remove("hash"); Long time =Long.parseLong((String) params.remove("time")); String signHash = createSign(params,uid); return hash.equals(signHash); } public static void main(String[] args) { Map<String,Object> params = new HashMap<String,Object>(); params.put("zhangId",12321); params.put("guanId",true); params.put("test","這是11AVC"); params.put("URL","XXXXXX"); params.put("name","是的商家"); String uid = "asdsa"; long time = System.currentTimeMillis(); String hash = createSign(params,uid); logger.info("hash={},time={}",hash,time); params.put("hash",hash); params.put("time",time); boolean flag = checkHashSign(params,uid); logger.info("校驗flag={} ",flag); } } 複製程式碼

伺服器端介面校驗

通過Filter實現hash簽名的校驗。

校驗請求籤名過程

  1. 從請求header中獲取使用者編號uid
  2. 從請求引數裡獲取簽名字串hash,以及請求時間time
  3. 獲取當前請求路徑,作為引數名為URL的引數值。【URL屬於隱藏引數】
  4. 再將請求引數進行一遍簽名加密,生成出來正確的簽名字串hash2
  5. 比較hash和hash2即可
public class AppFilter implements Filter {
    /**
     * 日誌物件
     */
    private static Logger logger = LoggerFactory.getLogger(AppFilter.class);

    private   static String CHECK_ERROR = null,PARAM_ERROR = null;
	
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        CHECK_ERROR =  new FkJsonResult( "校驗失敗",FkJsonResult.DICT_COMMON_ERROR,"簽名認證失敗。" ).toJSONString();
        PARAM_ERROR =  new  FkJsonResult( "引數錯誤","認證失敗。" ).toJSONString();
    }

    public static Map<String,Object> reqParamterToMap(HttpServletRequest req){
        Map<String,String[]> m=req.getParameterMap();
        Map<String,Object> rm=new HashMap<String,Object>(m.size());
        Iterator<String> itor=m.keySet().iterator();
        while(itor.hasNext()){
            String key=itor.next();
            String[] strs=m.get(key);
            String val=null;
            if(strs.length>0){
                val=strs[0];
            }
            rm.put(key,val);
        }
//        新增當前請求地址作為引數 防止不同介面間互用祕鑰,該引數屬於隱含引數。
        rm.put("URL",req.getRequestURI());
        return rm;
    }

    @Override
    public void doFilter(ServletRequest request,ServletResponse response,FilterChain chain) throws IOException,ServletException {

        if (request != null){
            HttpServletRequest request1 = (HttpServletRequest) request;
            String uid = request1.getHeader(FkSignUtil.UID);
            if(StringUtils.isEmpty(uid)){
                logger.info("header uid is null.");
                response.setCharacterEncoding("UTF-8");
                response.setContentType("application/json; charset=utf-8");
                PrintWriter out = response.getWriter();
                out.append(PARAM_ERROR);
            }else{
                Map map = reqParamterToMap(request1);
                boolean flag = FkSignUtil.checkHashSign(map,uid);
                logger.info(" check flag = {}",flag);
                if(flag){
                    chain.doFilter(request,response);
                }else{
                    response.setCharacterEncoding("UTF-8");
                    response.setContentType("application/json; charset=utf-8");
                    PrintWriter out = response.getWriter();
                    out.append(CHECK_ERROR);
                }
            }
        }else{
            chain.doFilter(request,response);
        }
    }


    @Override
    public void destroy() {

    }
}
複製程式碼

其中定義json返回規範

  • code:狀態編碼,可以自定義 。1表示成功 0表示通用失敗
  • msg:提示資訊
  • data: 資料

/**
 * 定義 json返回
 */
public class FkJsonResult extends JSONObject implements  Serializable {

    /**
     * 通用成功
     */
    public static final String DICT_COMMON_SUCCESS = "1";
    /**
     * 通用失敗
     */
    public static final String DICT_COMMON_ERROR = "0";
 
 
    private String code;
 
    private String message;
 
    private Object data;

    public FkJsonResult() {
    }

    /**
     *
     * @param data
     * @param code
     * @param message
     */
    public FkJsonResult(Object data,String code,String message ) {
        this.put("code",code);
        this.put("message",message);
        this.put("data",data);

    }

    public static FkJsonResult success(Object data){
        return new FkJsonResult(data,DICT_COMMON_SUCCESS,"ok");
    }

    public static FkJsonResult success(Object data,String message){
        return new FkJsonResult(data,message);
    }

    public static FkJsonResult success( ){
        return new FkJsonResult(null,"ok");
    }

    public static FkJsonResult error(String code,String message){
        return new FkJsonResult(null,code,message);
    }
    public static FkJsonResult error(String code,String message,Object data){
        return new FkJsonResult(data,message);
    }
    /**
     * 系統錯誤
     * @return
     */
    public static FkJsonResult error(  ){
        return new FkJsonResult("系統異常","系統維護中...");
    }
    public String getCode() {
        return this.getString("code");
    }

    public void setCode(String code) {
        this.put("code",code);
    }

    public String getMessage() {
        return this.getString("message");
    }

    public void setMessage(String message) {
        this.put("message",message);
    }

    public Object getData() {
        return this.get ("data");
    }

    public void setData(Object data) {
        this.put("data",data);
    }

    @Override
    public String toString() {
        return this.toJSONString();
    }
}
複製程式碼

將Filter配置到web.xml中

具體攔截規則 需要根據自己的情況定義。最好使用該Filter的介面統一請求路徑字首。

	<filter>
		<filter-name>appFilter</filter-name>
		<filter-class>com.xxxx.common.filter. AppFilter</filter-class>
	</filter>
	<filter-mapping>
		<filter-name>appFilter</filter-name>
		<url-pattern>/aa/*</url-pattern>
		此處要定義攔截規則
複製程式碼

由此伺服器接收端已做好自動校驗。

客戶端呼叫

利用Spring RestTemplate,實現2個post呼叫方法。

  • 同步呼叫:建議使用非同步呼叫

  • 非同步呼叫:新增自定義執行緒池 並設計如下請求引數:

    • @param domain 域名
    • @param path 請求路徑 (domain + path才是請求連結)
    • @param params 引數map
    • @param uid 使用者標識

其中JsonResult 同伺服器端的FkJsonResult.java。 TkSignUtil同伺服器端的FkSignUtil.java.。

請求籤名實現過程

  1. 將當前請求路徑(不含域名。如:xx.com/sys/user/li…,其中請求路徑即為/sys/user/list)作為URL引數的值(例如:URL=/sys/user/list),加上當前請求引數,對這些引數名進行升序排序,排序之後生成請求引數字串queryStr(拼接時要對引數值進行URLEncoder.encode編碼,防止中文等問題),形如:引數a=xx&引數b=22。
  2. 系統間約定加密字串key【houdask2019】 ,生成當前時間戳time(毫秒數),獲取當前使用者編號 uid(注意uid key time 這三個引數均不參與排序)
  3. 將步驟1中生成的queryStr 和 time 、key 、uid 進行拼接,形成待加密字串str。形如 queryStr &time=xx&key=xxx&uid=xxx
  4. 對待加密字串str進行MD5加密,並轉化成大寫,即生成簽名字串hash。
  5. 客戶端將uid放到請求header中,將原始請求引數和time、hash一起作引數傳遞(注意:其中URL引數不必傳遞)。
public class RestTemplateUtils {


    private static Logger logger = LoggerFactory.getLogger(RestTemplateUtils.class);

    private static final RestTemplate restTemplate = new RestTemplate();


    private static ThreadFactory namedThreadFactory = new ThreadFactoryBuilder().setNameFormat("RestTemplateUtils-pool-%d").build();

    private static ExecutorService pool = new ThreadPoolExecutor(5,200,0L,TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>(1024),namedThreadFactory,new ThreadPoolExecutor.AbortPolicy());

    /**
     * POST請求
     *
     * @param domain 域名
     * @param path   請求路徑 (domain + path才是請求連結)
     * @param params 引數map
     * @param uid    使用者標識
     * @return 返回結果  只有code == 1 才是成功返回
     */
    public static JsonResult post(String domain,String path,Map<String,String uid) {
        if (StringUtils.isEmpty(uid)) {
            return JsonResult.error(JsonResult.DICT_COMMON_ERROR,"引數錯誤。","uid is null.");
        }
//        簽名
        params = getSignMap(params,uid,path);
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
        headers.set("uid",uid);

        MultiValueMap<String,Object> reqParams = new LinkedMultiValueMap();
        for (String s : params.keySet()) {
            reqParams.add(s,params.get(s).toString());
        }
        HttpEntity<MultiValueMap<String,Object>> entity = new HttpEntity<MultiValueMap<String,Object>>(reqParams,headers);
        ResponseEntity<JsonResult> resp = restTemplate.exchange
                (domain + path,HttpMethod.POST,entity,JsonResult.class);
        if (resp.getStatusCode().equals(HttpStatus.OK)) {
            return resp.getBody();
        } else {
            logger.error("{}{}請求錯誤{}:{}",domain,path,resp.getStatusCodeValue(),resp.getBody());
            return JsonResult.error(resp.getStatusCodeValue() + "","請求失敗",resp.getBody());
        }
    }

    /**
     * 非同步呼叫POST請求
     *
     * @param domain 域名
     * @param path   請求路徑 (domain + path才是請求連結)
     * @param params 引數map
     * @param uid    使用者標識
     * @return 返回結果 FutureTask  通過FutureTask.get()即返回JsonResult。  只有code == 1 才是成功返回
     */
    public static FutureTask<JsonResult> asynPost(String domain,String uid) {
        FutureTask<JsonResult> task = new FutureTask((Callable<JSONObject>) () -> post(domain,params,uid));
        pool.submit(task);
        return task;
    }


    /**
     * 獲取簽名引數並返回引數集合
     */
    private static Map getSignMap(Map<String,Object> chapterMap,String userId,String path) {
        long time = System.currentTimeMillis();
        chapterMap.put("URL",path);
        String hash = TkSignUtil.createSign(chapterMap,userId);
        logger.info("hash={},time);
        chapterMap.put("hash",hash);
        chapterMap.put("time",time);
        chapterMap.remove("URL");
        return chapterMap;
    }
}
複製程式碼

優化方案

可以考慮使用token快取,免加密解密校驗。 同時也可以校驗time是否不是在有效期內,比如判斷time是不是在當前時間的前2分鐘之內,防止介面擴散重複呼叫。

伺服器端安全升級--https安全升級

利用阿里雲申請 Symantec 免費版 SSL 證書。 在nginx上新增ssl證書到/etc/nginx/ssl.conf資料夾下面。 配置示例:


# 以下屬性中以ssl開頭的屬性代表與證書配置有關,其他屬性請根據自己的需要進行配置。
server {
    listen 443;
    server_name localhost;  # localhost修改為您證書繫結的域名。
    ssl on;   #設定為on啟用SSL功能。
    root html;
    index index.html index.htm;
    ssl_certificate cert/domain name.pem;   #將domain name.pem替換成您證書的檔名。
    ssl_certificate_key cert/domain name.key;   #將domain name.key替換成您證書的金鑰檔名。
    ssl_session_timeout 5m;
    ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4;  #使用此加密套件。
    ssl_protocols TLSv1 TLSv1.1 TLSv1.2;   #使用該協議進行配置。
    ssl_prefer_server_ciphers on;   
    location / {
        root html;   #站點目錄。
        index index.html index.htm;   
    }
}
複製程式碼

如無法訪問請檢查nginx所在機器的443埠是否開啟