如何實現簡單的請求鑑權

如何利用對稱加密實現簡單的請求鑑權。

前期溝通

服務端與客戶端需要在前期敲定以下內容:

  1. 祕鑰對(apiKey和secretKey),由服務端通過安全的途徑交給客戶端,如郵件、IM等內部渠道。
  2. 頭部名稱,包括APIKey、時間戳、簽名及業務相關的頭部。
  3. 加簽算法,即根據業務參數及secretKey如何生成加密簽名,客戶端與服務端需保持一致。由客戶端加密後的內容,在服務端用同樣的祕鑰加密應該是一模一樣的。

服務端

驗籤流程

大致流程如下圖所示。
img

代碼

通過Interceptor來做攔截,並根據驗簽結果來決定對請求是否放行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
public class UserInterceptor implements HandlerInterceptor {

	private final static String AUTH_HEADER_APIKEY = "X-Header-APIKey";
	private final static String AUTH_HEADER_TIMESTAMP = "X-Header-Timestamp";
	private final static String AUTH_HEADER_SIGNATURE= "X-Header-Signature";
	private final static String AUTH_HEADER_USERID = "X-Header-UserID";
	
	private static final Logger logger = LoggerFactory.getLogger(UserInterceptor.class);
	@Override
	public boolean preHandler(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
		return checkSystemAuth(request, response);
	}
	
    private boolean checkSystemAuth(HttpServletRequest request, HttpServletResponse response) {
        // 1. 檢查頭部完整
        String reqApiKey = request.getHeader(AUTH_HEADER_APIKEY);
        String reqTimestamp = request.getHeader(AUTH_HEADER_TIMESTAMP);
        String reqSign = request.getHeader(AUTH_HEADER_SIGNATURE);
        String userId = request.getHeader(AUTH_HEADER_USERID);
        if(StringUtils.isEmpty(reqApiKey) || StringUtils.isEmpty(reqTimestamp) || StringUtils.isEmpty(reqSign) || StringUtils.isEmpty(userId)) {
            logger.error("missing apikey or timestamp or signature or userid header");
            return false;
        }
        // 2. 檢查timestamp超時
        if(!isInTime(reqTimestamp)) {
            logger.error("timestamp header timedout");
            return false;
        }
        // 3. 根據apikey,從DB中找到對應的secretkey,keypairMapper爲DAO對象
        KeyPair keyStore = keypairMapper.getOneByApiKey(reqApiKey);
        if(null == keyStore) {
            logger.error("cannot find secretkey from apikey");
            return false;
        }
        String secretKey = keyStore.getSecretKey();
        // 4. 將除簽名外的頭部生成有序map
        SortedMap<String, String> reqForm = new TreeMap<>();
        reqForm.put(AUTH_HEADER_APIKEY, reqApiKey);
        reqForm.put(AUTH_HEADER_TIMESTAMP, reqTimestamp);
        reqForm.put(AUTH_HEADER_USERID, userId);
        // 5. 計算出簽名並與傳來的簽名比對
        String calculatedSign = sign(reqForm, secretKey);
        if(!reqSign.equals(calculatedSign)) {
            logger.error("mismatched signatures");
            return false;
        }
        logger.debug("system auth passed");
        return true;
    }

    private boolean isInTime(String timeStr) {
        try {
            long time = Long.parseLong(timeStr);
            if (System.currentTimeMillis() - time <= interceptorProperties.getDefaultTimestampTimeout()) {
                return true;
            } else {
                logger.error("Timestamp in request timed out.");
                return false;
            }
        } catch (NumberFormatException e) {
            logger.error("Invalid timestamp: {}", e.getMessage());
            return false;
        }
    }

    private String sign(SortedMap<String, String> reqForm, String secretKey) {
        try {
            // 1. 將有序map組合成url串
            List<String> kvList = new ArrayList<>();
            for (Map.Entry<String, String> paramEntry : reqForm.entrySet()) {
                kvList.add(paramEntry.getKey() + "=" + URLEncoder.encode(
                        StringUtils.isEmpty(
                                paramEntry.getValue()) ? "" : paramEntry.getValue(), Charsets.UTF_8.name()
                        )
                );
            }
            // 2. 計算簽名
            String queryString = StringUtils.join(kvList, '&').toLowerCase();
            String signature = Base64.encodeBase64String(new HmacUtils(HmacAlgorithms.HMAC_SHA_1, secretKey).hmac(queryString));
            // 3. 二次encode
            String encodedSign = URLEncoder.encode(signature, Charsets.UTF_8.name());
            return encodedSign;
        } catch (Exception e) {
	        logger.error("Signature error: {}", e.getMessage());
            return null;
        }
    }
}

 

客戶端

流程

客戶端的加簽過程如下圖所示。

代碼

Java版的客戶端代碼如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
public class AuthTest {
	private final static String AUTH_HEADER_APIKEY = "X-Header-APIKey";
	private final static String AUTH_HEADER_TIMESTAMP = "X-Header-Timestamp";
	private final static String AUTH_HEADER_SIGNATURE= "X-Header-Signature";
	private final static String AUTH_HEADER_USERID = "X-Header-UserID";

    public static void main(String[] args) {
	    String url;
	    ...// 構造server url
	    // apikey及secretkey,由服務端提供並由客戶端保存
        String apiKey = "xxx";
        String secretKey = "yyy";

        RestTemplate restTemplate = new RestTemplate();
        Long timestamp = System.currentTimeMillis();
        // 1. 構造模擬請求參數列
        String sortedHeaders = new StringBuilder("?")
                .append(AUTH_HEADER_APIKEY)
                .append("=")
                .append(apiKey)
                .append("&")
                .append(AUTH_HEADER_TIMESTAMP)
                .append("=")
                .append(timestamp)
                .append("&")
                .append(AUTH_HEADER_USERID)
                .append("=luckliu").toString();
        SortedMap<String, String> paramMap = extractFromUrlParamToMap(sortedHeaders);
        // 2. 計算簽名
        String signature = sign(paramMap, secretKey);
        // 3. 放置頭部
        HttpHeaders headers = new HttpHeaders();
        headers.set(AUTH_HEADER_APIKEY, apiKey);
        headers.set(AUTH_HEADER_TIMESTAMP, Long.toString(timestamp));
        headers.set(AUTH_HEADER_SIGNATURE, signature);
        headers.set(AUTH_HEADER_USERID, "luckliu");
        String body = "!dlrow olleH";
        HttpEntity<String> request = new HttpEntity<String>(body, headers);
        // 4. 發起請求
        ResponseEntity<Void> responseEntity = restTemplate.postForEntity(url, request, Void.class);
    }

    /**
     * 截取url問號後面的參數, 並轉換成SortedMap
     * @param url
     * @return
     */
    private static SortedMap<String, String> extractFromUrlParamToMap(String url) {
        // TODO 需考慮參數爲空等異常情況
        String[] paramArr = url.substring(url.indexOf("?")+1).split("&");
        SortedMap<String, String> paramMap = Maps.newTreeMap();
        Arrays.stream(paramArr).forEach(
                p->paramMap.put(p.substring(0,p.indexOf("=")), p.substring(p.indexOf("=")+1))
        );
        return paramMap;
    }

	// 此處的sign方法應與服務端的保持一致
    private static String sign(SortedMap<String, String> reqForm, String secretKey) {
        try {
            // 組合成url串
            List<String> kvList = new ArrayList<>();
            for (Map.Entry<String, String> paramEntry : reqForm.entrySet()) {
                kvList.add(paramEntry.getKey() + "=" + URLEncoder.encode(
                        StringUtils.isEmpty(
                                paramEntry.getValue()) ? "" : paramEntry.getValue(), Charsets.UTF_8.name()
                        )
                );
            }
            String queryString = StringUtils.join(kvList, '&').toLowerCase();
            // 計算簽名
            String signature = Base64.encodeBase64String(new HmacUtils(HmacAlgorithms.HMAC_SHA_1, secretKey).hmac(queryString));
            // 二次encode
            String encodedSign = URLEncoder.encode(signature, Charsets.UTF_8.name());
            return encodedSign;
        } catch (Exception e) {
            return null;
        }
    }
}

 

再來一個golang版本的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
package main
import (
	"crypto/hmac"
	"crypto/sha1"
	"encoding/base64"
	"fmt"
	"net/http"
	"net/url"
	"strconv"
	"strings"
	"time"
)

// 加簽算法
func Hmac(key, data []byte) string {
	mac := hmac.New(sha1.New, key)
	mac.Write(data)
	return url.QueryEscape(base64.StdEncoding.EncodeToString(mac.Sum([]byte(""))))
}
func main() {
	...// 構造server url
	body := "!dlroW olleH"
	// 1. 通過設置系統校驗頭部來調用接口
	apikey := "xxx"
	secretKey := "yyy"
	timestamp := time.Now().UnixNano() / 1000000
	// 2. 省略排序等步驟,將校驗參數組織成有序的請求列
	sortedHeaders := []byte(strings.ToLower("X-Header-APIKey=" + apikey + "&X-Header-Timestamp=" + strconv.FormatInt(timestamp, 10) + "&MLSS-DI-UserID=luckliu"))
	// 3. 計算簽名
	signature := Hmac([]byte(secretKey), sortedHeaders)
	fmt.Println("final sorted headers: ", string(sortedHeaders))
	fmt.Println("calculated signature: " + signature)
	// 4. 放置請求頭部併發起請求
	client := &http.Client{}
	request, _ := http.NewRequest("POST", url, strings.NewReader(body))
	request.Header.Set("X-Header-APIKey", apikey)
	request.Header.Set("X-Header-Timestamp", strconv.FormatInt(timestamp, 10))
	request.Header.Set("X-Header-Signature", signature)
	request.Header.Set("X-Header-UserID", "luckliu")
	response, _ := client.Do(request)
	fmt.Println("response status: " + response.Status)
	defer response.Body.Close()
}
發佈了157 篇原創文章 · 獲贊 110 · 訪問量 31萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章