如何实现简单的请求鉴权

如何利用对称加密实现简单的请求鉴权。

前期沟通

服务端与客户端需要在前期敲定以下内容:

  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万+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章