接口签名的目的:
- 防篡改, 保证数据的完整性
- 认证, 验证调用者的身份,校验请求是否合法
此外还需要防止请求的重放
流程
- 客户端到服务端登记 appId(客户端标识), appSecret
- 客户端将请求参数拼接成字符串,计算签名值,附加在请求参数中
- 服务端验证签名的正确性
签名的算法主要有两种,基于对称密钥的HAMC, 基于非对称加密的 SHA256withRSA
一、消息认证码 mac(message authentication code)
- hmac : Hash-based message authentication code 基于hash算法的消息认证码
hmac由hash函数和私钥组成;用于校验数据完整性和消息的认证
定义
- Mac = H(key || 消息)
- H(ke || H(key || message))
- H 哈希函数
- K 私钥
- m 消息
- K' padding key, hashed key
- ipad inner padding
- opad outer padding
不同的哈希函数选择导致不同的安全性;
Hmac-sha256, hmac-md5
public static String HMACSHA256(String strToSign, String key) throws Exception {
Mac sha256_HMAC = Mac.getInstance("HmacSHA256");
SecretKeySpec secret_key = new SecretKeySpec(key.getBytes("UTF-8"), "HmacSHA256");
sha256_HMAC.init(secret_key);
byte[] array = sha256_HMAC.doFinal(data.getBytes("UTF-8"));
System.out.println(array.length);
return Hex.encodeHexString(array).toUpperCase();
}
复制代码
array的长度是32, 32 * 8 = 256 bit
二、 HMACwithRsa256 (基于非对称加密)
signed = Rsa(sha256(text))
public static final String SIGNATURE_ALGORITHM = "SHA256withRSA";
public static final String ENCODE_ALGORITHM = "SHA-256";
public static byte[] sign(PrivateKey privateKey, String plainText){
MessageDigest messageDigest;
byte[] signed;
try{
messageDigest = MessageDigest.getInstance(ENCODE_ALGORITHM);
messageDigest.update(plainText.getBytes());
byte[] outputDigest = messageDigest.digest();
Signature signature = Signature.getInstance(SIGNATURE_ALGORITHM);
signature.initSign(privateKey);
signature.update(outputDigest);
signed = signature.sign();
}catch (NoSuchAlgorithmException | InvalidKeyException | SignatureException e) {
e.printStackTrace();
return null;
}
return signed;
}
public static boolean verifySign(PublicKey publicKey, String plainText, byte[] signed){
MessageDigest messageDigest;
boolean result = false;
try{
messageDigest = MessageDigest.getInstance(ENCODE_ALGORITHM);
messageDigest.update(plainText.getBytes());
byte[] outputToVerify = messageDigest.digest();
Signature verifySign = Signature.getInstance(SIGNATURE_ALGORITHM);
verifySign.initVerify(publicKey);
verifySign.update(outputToVerify);
result = verifySign.verify(signed);
} catch (NoSuchAlgorithmException | InvalidKeyException | SignatureException e) {
e.printStackTrace();
}
return result;
}
复制代码
防止重放
- timestamp : 请求中附带一个时间戳,服务端拦截超过一定时间的请求
- nonce : 每一个请求附带一个随机数,可以是UUID,一段时间内不可以重复;服务端将nonce记录在缓存中
接口签名算法
strToSign = "param1=val1¶m2=val2%..." + "&body=‘...’" + "&key=prvKey";
- 请求的get参数按照字典顺序排列;
- 请求body中的json数据也按照字典顺序序列化 (body中的json一定要限定序列化的顺序,不然服务端读取到的json字符串与客户端签名时的不一致)
一、 hmac签名的服务端实现
基于spring security框架,在拦截器链中添加拦截器,校验签名
public class AppAuthenticationFilter extends GenericFilter {
static final String FILTER_APPLIED = "_app_filter_applied";
private AuthenticationManager authenticationManager;
public AppAuthenticationFilter(AuthenticationManager authenticationManager){
this.authenticationManager = authenticationManager;
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws IOException, ServletException {
HttpServletRequestWrapper request = (HttpServletRequestWrapper) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
if (request.getAttribute(FILTER_APPLIED) != null) {
// 该filter只会执行一次
chain.doFilter(request, response);
return;
}
request.setAttribute(FILTER_APPLIED, Boolean.TRUE);
//get参数中存在appid时,拦截器工作
String appId = request.getParameter("appId");
if (StringUtils.isEmpty(appId)){
chain.doFilter(request, response);
return;
}
//1 拼接签名字符串
String paramStr = sortParamForApp(request);
String body = inputStream2String(request.getInputStream());
StringBuilder sbToSign = new StringBuilder();
if (StringUtils.isNotEmpty(paramStr)){
sbToSign.append(paramStr);
}
if (StringUtils.isNotEmpty(body)){
sbToSign.append("&body=").append(body);
}
//2 认证
AppAuthenticationToken token = new AppAuthenticationToken();
token.setAppId(request.getParameter("appId"));
token.setNonce(request.getParameter("nonce"));
token.setTimeStamp(Long.parseLong(request.getParameter("timestamp")));
token.setSignature(request.getParameter("signature"));
token.setStringToSign(sbToSign.toString());
try {
Authentication authentication = authenticationManager.authenticate(token);
if (authentication != null) {
//3 认证成功,设置安全上下文
SecurityContextHolder.getContext().setAuthentication(authentication);
chain.doFilter(request, response);
}
}catch (AuthenticationException e){
e.printStackTrace();
throw e;
}
}
}
复制代码
添加拦截器
http.addFilterBefore(new AppAuthenticationFilter(providerManager()), UsernamePasswordAuthenticationFilter.class);
复制代码
提供认证
public class AppAuthenticationProvider implements AuthenticationProvider {
private static final int DEFAULT_TIMEOUT = 30;// 30S
private static final String PREFIX_NONCE="printer-nonce"; // key = PREFIX_NONCE:${appId}:${nonce}, value = timeStamp
private AppDetailsService appDetailsService;
private int timeout; //请求有效的时长 (now() - timestamp < timeout)
@Autowired
private Jedis jedis;
public AppAuthenticationProvider(AppDetailsService appDetailsService){
this.appDetailsService = appDetailsService;
this.timeout = DEFAULT_TIMEOUT;
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
AppAuthenticationToken token = (AppAuthenticationToken) authentication;
String appId = token.getAppId();
if (StringUtils.isEmpty(appId)){
throw new AppIdNotFoundException("appId不能为空");
}
AppDetails appDetails = appDetailsService.loadAppDetails(appId);
if (appDetails == null) {
throw new AppIdNotFoundException("appId不存在");
}
if (!appDetails.isEnabled()){
throw new AppIdNotFoundException("app被禁用");
}
//1. 校验签名
boolean verifySuccess = HMACUtil.verifySign(token.getStringToSign(), token.getSignature(), appDetails.appKey());
if (!verifySuccess) {
throw new AppInvalidSignException("app调用签名不正确");
}
//2. 校验nonce
String nonceKey = PREFIX_NONCE + ":" + appDetails.appId() + ":" + token.getNonce();
boolean nonceExist = jedis.exists(nonceKey);
if (nonceExist){
throw new NonceExistException("nonce已存在" + token.getNonce());
}else{
jedis.setex(nonceKey, DEFAULT_TIMEOUT, String.valueOf(System.currentTimeMillis()));
}
//3. 校验timestamp
if (System.currentTimeMillis() - token.getTimeStamp() > DEFAULT_TIMEOUT*1000){
throw new InvalidTimeStampException("时间戳非法");
}
return new AppAuthenticationToken(
appDetails, token, Arrays.asList(new SimpleGrantedAuthority("APP"))
);
}
@Override
public boolean supports(Class<?> authentication) {
return AppAuthenticationToken.class.isAssignableFrom(authentication);
}
}
复制代码
查询app相关信息
public interface AppDetailsService {
AppDetails loadAppDetails(String appId) throws AppIdNotFoundException;
}
复制代码
hmacUtil 对称签名工具类
package cn.raiyee.printer.util;
import org.apache.commons.codec.binary.Hex;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
public class HMACUtil {
public static String generateHash(String content, String prvKey){
String encodeStr = "";
try {
MessageDigest digest = MessageDigest.getInstance("sha-256");
String strToSign = content + "&key=" + prvKey;
digest.update(strToSign.getBytes());
byte[] output = digest.digest();
encodeStr = Base64.getUrlEncoder().encodeToString(output);
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
return encodeStr;
}
public static boolean verifySign(String content, String hash, String prvKey) {
boolean success = false;
try {
MessageDigest digest = MessageDigest.getInstance("sha-256");
String strToSign = content + "&key=" + prvKey;
digest.update(strToSign.getBytes());
byte[] output = digest.digest();
String encodeStr = Base64.getUrlEncoder().encodeToString(output);
success = encodeStr.equals(hash);
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
return success;
}
public static String HMACSHA256(String data, String key) throws Exception {
Mac sha256_HMAC = Mac.getInstance("HmacSHA256");
SecretKeySpec secret_key = new SecretKeySpec(key.getBytes("UTF-8"), "HmacSHA256");
sha256_HMAC.init(secret_key);
byte[] array = sha256_HMAC.doFinal(data.getBytes("UTF-8"));
return Hex.encodeHexString(array).toUpperCase();
}
}
复制代码
客户端调用接口:
void testAppSign() {
HttpHeaders header = new HttpHeaders();
header.add("Accept", "application/json");
AgentProxyReq agentProxyReq = AgentProxyReq.builder()
.agentId("2")
.agentPhoneName("小米手机")
.agentPhoneType("小米")
.merchantId("123")
.agentPubKey("222")
.build();
String timestamp = String.valueOf(System.currentTimeMillis());
String nonce = UUID.randomUUID().toString();
String appId = "473632086310273024";
Map<String, String> params = new TreeMap<>();
params.put("appId", appId);
params.put("timestamp", timestamp);
params.put("nonce", nonce);
StringBuilder sb = new StringBuilder();
for (Map.Entry<String,String> entry : params.entrySet()){
sb.append(entry.getKey()).append("=").append(entry.getValue()).append("&");
}
if (sb.length() > 0){
sb.deleteCharAt(sb.length()-1);
}
sb.append("&body=").append(JSON.toJSONString(agentProxyReq));
System.out.println("stringToSign = " + sb.toString());
HttpEntity<AgentProxyReq> requestEntity = new HttpEntity(agentProxyReq, header);
String signature = HMACUtil.generateHash(sb.toString(), "acb196c11e904ebeae93ff86ed9bcfbe059c837b02784e45814d9fc33fb58256");
System.out.println("signature = " + signature);
RestTemplate restTemplate = new RestTemplate();
ResponseEntity<Result> resultResponseEntity = restTemplate.postForEntity("http://127.0.0.1:8006/printer/app/test?appId="+appId+"&signature="
+signature+"×tamp="+timestamp+"&nonce=" + nonce
, requestEntity, Result.class, params);
System.out.println(resultResponseEntity);
Result result = resultResponseEntity.getBody();
System.out.println(result);
}
复制代码
SHA256withRSA 的服务端实现
拦截器
public class AgentAuthenticationFilter extends GenericFilter {
static final String FILTER_APPLIED = "_agent_filter_applied";
private AuthenticationManager authenticationManager;
public AgentAuthenticationFilter(AuthenticationManager authenticationManager){
this.authenticationManager = authenticationManager;
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws IOException, ServletException {
HttpServletRequestWrapper request = (HttpServletRequestWrapper) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
if (request.getAttribute(FILTER_APPLIED) != null) {
// 该filter只会执行一次
chain.doFilter(request, response);
return;
}
request.setAttribute(FILTER_APPLIED, Boolean.TRUE);
String agentId = request.getParameter("agentId");
if (StringUtils.isEmpty(agentId)){
chain.doFilter(request, response);
return;
}
//1 拼接签名字符串
String paramStr = sortParamForApp(request);
String body = inputStream2String(request.getInputStream());
StringBuilder sbToSign = new StringBuilder();
if (StringUtils.isNotEmpty(paramStr)){
sbToSign.append(paramStr);
}
if (StringUtils.isNotEmpty(body)){
sbToSign.append("&body=").append(body);
}
//2 认证
AgentAuthenticationToken token = new AgentAuthenticationToken();
token.setAgentId(agentId);
token.setSignature(request.getParameter("signature"));
token.setTimestamp(Long.parseLong(request.getParameter("timestamp")));
token.setStringToSign(sbToSign.toString());
try {
Authentication authentication = authenticationManager.authenticate(token);
if (authentication != null){
//3 认证成功,设置安全上下文
SecurityContextHolder.getContext().setAuthentication(authentication);
chain.doFilter(request, response);
}
}catch (AuthenticationException e){
e.printStackTrace();
throw e;
}
}
}
复制代码
提供认证的Provider
public class AgentAuthenticationProvider implements AuthenticationProvider {
private static final int DEFAULT_TIMEOUT = 30;// 30S
private AgentDetailService agentDetailService;
public AgentAuthenticationProvider(AgentDetailService agentDetailService){
this.agentDetailService = agentDetailService;
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
AgentAuthenticationToken token = (AgentAuthenticationToken) authentication;
String agentId = token.getAgentId();
if (StringUtils.isEmpty(agentId)){
throw new AgentIdNotFoundException("agentId不能为空");
}
PrinterAgent printerAgent = agentDetailService.loadPrinterAgent(agentId);
if (printerAgent == null) {
throw new AgentIdNotFoundException("agentId不存在");
}
// 校验签名
try {
PublicKey publicKey = loadPublicKey(printerAgent.getAgentPubKey());
byte[] signatureBytes = Base64.decodeBase64(token.getSignature());
boolean verifySuccess = SignUtil.verifySign(publicKey, token.getStringToSign(), signatureBytes);
if (!verifySuccess) {
throw new AgentInvalidSignException("agent签名不正确");
}
return new AgentAuthenticationToken(
printerAgent, token, Collections.singletonList(new SimpleGrantedAuthority("AGENT"))
);
} catch (Exception e) {
e.printStackTrace();
}
long timestamp = token.getTimestamp();
if (timestamp + DEFAULT_TIMEOUT*1000 > System.currentTimeMillis()) {
throw new InvalidTimeStampException("时间戳非法");
}
return null;
}
@Override
public boolean supports(Class<?> authentication) {
return AgentAuthenticationToken.class.isAssignableFrom(authentication);
}
}
复制代码
签名工具栏
public class SignUtil {
/**
* 根据公钥字符串加载公钥
*
* @param publicKeyStr 公钥字符串
* @return
* @throws Exception
*/
public static RSAPublicKey loadPublicKey(String publicKeyStr) throws Exception {
try {
byte[] buffer = javax.xml.bind.DatatypeConverter.parseBase64Binary(publicKeyStr);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(buffer);
return (RSAPublicKey) keyFactory.generatePublic(keySpec);
} catch (NoSuchAlgorithmException e) {
throw new Exception("无此算法", e);
} catch (InvalidKeySpecException e) {
throw new Exception("公钥非法", e);
} catch (NullPointerException e) {
throw new Exception("公钥数据为空", e);
}
}
/**
* 根据私钥字符串加载私钥
*
* @param privateKeyStr 私钥字符串
* @return
* @throws Exception
*/
public static RSAPrivateKey loadPrivateKey(String privateKeyStr) throws Exception {
try {
byte[] buffer = Base64.getDecoder().decode(privateKeyStr);
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(buffer);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
return (RSAPrivateKey) keyFactory.generatePrivate(keySpec);
} catch (NoSuchAlgorithmException e) {
throw new Exception("无此算法", e);
} catch (InvalidKeySpecException e) {
throw new Exception("私钥非法", e);
} catch (NullPointerException e) {
throw new Exception("私钥数据为空", e);
}
}
/**
* 生成没有加密的密钥对
* @param algorithm
* @param keySize
* @return
* @throws NoSuchAlgorithmException
*/
public static Map<String, Object> createKey(String algorithm, int keySize) throws NoSuchAlgorithmException {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(algorithm);
keyPairGenerator.initialize(keySize);
KeyPair keyPair = keyPairGenerator.generateKeyPair();
RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
RSAPrivateKey privateCrtKey = (RSAPrivateKey) keyPair.getPrivate();
System.out.println(Base64.getEncoder().encodeToString(publicKey.getEncoded()).length());
System.out.println(Base64.getEncoder().encodeToString(privateCrtKey.getEncoded()).length());
Map<String, Object> keyMap = new HashMap<>();
keyMap.put("publicKey", publicKey);
keyMap.put("privateKey", privateCrtKey);
return keyMap;
}
public static final String SIGNATURE_ALGORITHM = "SHA256withRSA";
public static final String ENCODE_ALGORITHM = "SHA-256";
public static byte[] sign(PrivateKey privateKey, String plainText){
MessageDigest messageDigest;
byte[] signed;
try{
messageDigest = MessageDigest.getInstance(ENCODE_ALGORITHM);
messageDigest.update(plainText.getBytes());
byte[] outputDigest = messageDigest.digest();
Signature signature = Signature.getInstance(SIGNATURE_ALGORITHM);
signature.initSign(privateKey);
signature.update(outputDigest);
signed = signature.sign();
}catch (NoSuchAlgorithmException | InvalidKeyException | SignatureException e) {
e.printStackTrace();
return null;
}
return signed;
}
public static boolean verifySign(PublicKey publicKey, String plainText, byte[] signed){
MessageDigest messageDigest;
boolean result = false;
try{
messageDigest = MessageDigest.getInstance(ENCODE_ALGORITHM);
messageDigest.update(plainText.getBytes());
byte[] outputToVerify = messageDigest.digest();
Signature verifySign = Signature.getInstance(SIGNATURE_ALGORITHM);
verifySign.initVerify(publicKey);
verifySign.update(outputToVerify);
result = verifySign.verify(signed);
} catch (NoSuchAlgorithmException | InvalidKeyException | SignatureException e) {
e.printStackTrace();
}
return result;
}
}
复制代码
调用客户端
void testAgentSign() throws Exception {
String agentPrvKey = "MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDIp0gcsntXwSs9drdU4Q69ir+9EzjN8sxR9X2RDhRIFe/OLsEBheCbZ5REQsFNwqhwBNcRnx/3hMsV+jspYc6nneBzExnW/dHxvXnKKOEf2jo75xzntBFKjLitXPqGZei2kX4CrTZTUG5jLGV/nG47s4maTFaMce9Edb2Ndi2W7u3Iua4N5RRFvTPZcV38ceRDwbhzkv0iEW+FJgulOGTE4WSodInlnjkgfdn3spIXCN/ztchlU9hGtL1KSg+jsb5jsLwg1Bo2oIAG/mTOKS8vFj69r49MO61SgQRduWRy8z2koCa4NjVgaREUKODqDFFaxFd7lqOE4z1lNvuXqknpAgMBAAECggEAZPMkaKuzOndJFedAXUfNbrb7uFiPX64j9agYwH3g3lOLDqSfHfEJC6aVBbLAJislKxaETa1NG+6HbksysKMwoUvgvISDn5KbqY+2Ums2uBvG6JRiCoChomwiDbaVpEBpUFDqoNYcWtZAc0zG0+kT3J17QVHeyVIZGsxRzXYDgcdWHANKCukESqdCXpDX3wGzHkRq2Xz2U5ZuxWbCfpFd85HNdEZVl6jzDiYDGEVwOLjFPi/rcSkGNX5BOHOFwcg8c/0cfu+IPyhQWfAGPLz2UWuXcPVEk6Xt0eCM+k5/AW9h1IDWc9xu6XWCWU7RSikZ0iz+sT4iOnMSZIeVXDOdMQKBgQD8o/O0mI0CDmurI6pwVINY3nYjDSGcAjTM0Epa9jbrGIZj/vPLPevuMMje/c7uQJ7HPOBuxWGwYDGsndwEodFW+i6JKCbpO+Z83E1CmL9SBg/zafG37qu/37BaD4gNEszdO7JV0JP6bzQM8fnzXFKiFh+8AqW/BS7NkO0vuCgDiwKBgQDLUlqIlNfAUn8eBCuQZN5JDxXotdsw26/FfcB0TJaa9W8Gs7jhdD807JfO+3Aa/DPjLjy5nM46a/V07zoZYF3TDYGyzNr5pYiqywjWFTbIdUD8EPbZcGzaaYq0GN9fY4Q8SrifxIxgOgAowi2aeC879dyImomyBvo+4xZCO04G2wKBgQDzPP8Ur5ODmVK8cRhWEmhrlbP0R15GkDE5yIjuTwPNEc3CVONwmOugZsPfPkqPRRQaC1iiDdPiNptc8Je2tf2RWkqXr1rXT9639HtGVT5OwJt25lfdmSMvFzT5YN7Ch4lKr4Eh8jGm+o4IsKjQT+EXQWnIYFwoL9tB+/kA6rNLxQKBgQCeA6nSngKzOCoMtOb6eDn9A5leWv83kHShgqKwf9lIItifl8tmhEafJgSxWt38Suc0dvnAsynfY4nG0CkSEb+5R7T1tZm1DT4Spmp+nswNrHrNq4183Y/riry+TNpEsv3RMa0clc8W9dyr0IVKmH71FZXIIHpE/oE7oJbq8FYqowKBgGbFqMY+hMJ6xUrc61cteMOk08g4p42hWiqLp5eKmGi8ZauGFZDrwtZElJ0Xvvw/1O6d2gBU/Udd5NbF+KaawoQkObXE1lut9PHHnWSlnPl4ZCnaFB5vbiqONzUh/C15MNZ+2p8LvNZ01n39KpzMjZ7xa5fOaRoN3he+JawkYnV4";
String agentId = "111";
HttpHeaders header = new HttpHeaders();
header.add("Accept", "application/json");
String timestamp = String.valueOf(System.currentTimeMillis());
Map<String, String> params = new TreeMap<>();
params.put("agentId", agentId);
params.put("timestamp", timestamp);
params.put("merchantId", "222");
StringBuilder sb = new StringBuilder();
for (Map.Entry<String,String> entry : params.entrySet()){
sb.append(entry.getKey()).append("=").append(entry.getValue()).append("&");
}
if (sb.length() > 0){
sb.deleteCharAt(sb.length()-1);
}
System.out.println("stringToSign = " + sb.toString());
byte[] signatureBytes = SignUtil.sign(SignUtil.loadPrivateKey(agentPrvKey), sb.toString());
String signature = Base64.getUrlEncoder().encodeToString(signatureBytes);
System.out.println("signature = " + signature);
RestTemplate restTemplate = new RestTemplate();
ResponseEntity<Result> resultResponseEntity = restTemplate
.getForEntity("http://127.0.0.1:8006/printer-platform/agent/test?agentId="+agentId+"&signature="
+signature+"×tamp="+timestamp+"&merchantId=222", Result.class, params);
System.out.println(resultResponseEntity);
Result result = resultResponseEntity.getBody();
System.out.println(result);
}
复制代码




近期评论