如何实现简单的请求鉴权
如何利用对称加密实现简单的请求鉴权。
前期沟通
服务端与客户端需要在前期敲定以下内容:
- 秘钥对(apiKey和secretKey),由服务端通过安全的途径交给客户端,如邮件、IM等内部渠道。
- 头部名称,包括APIKey、时间戳、签名及业务相关的头部。
- 加签算法,即根据业务参数及secretKey如何生成加密签名,客户端与服务端需保持一致。由客户端加密后的内容,在服务端用同样的秘钥加密应该是一模一样的。
服务端
验签流程
代码
通过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() } |