场景:
一,本公司一个项目支付中心,需要和另外一个系统oracle进行交互
流程是:支付中心推送数据到oracle系统,oracle系统再推送返回结果到支付中心
前者是:调用oracle接口,webservice实现
后者是:支付中心对外暴露一个restful接口,供oracle调用
因为项目发布到公网,别人如果拿到url可参数就可以直接调用接口,存在风险
怎么解决:就是调用者需要签名,校验权限
方式1,
/** * 校验 Authorization * 前后端都生成 * * @param request * @return */
private static final String BASICAUTH = "AN_JI:AJ_1113"; public static Boolean checkAuthorization(HttpServletRequest request) { String header = request.getHeader("Authorization"); byte[] encodeBase = Base64.encodeBase64(BASICAUTH.getBytes()); String authorization = new String(encodeBase); authorization = "Basic " + authorization; return authorization.equals(header); }
具体实现为:和调用方(前端)约定一个值,BASICAUTH,第三方和我方约定对该值进行base64编码处理,第三方调用时把编码后的值传过来,放到请求头中,我方取到该值进行校验,有且一致,则调用,否则没有权限。
缺点:
试想如果黑客拿到这个编码后的值,直接传到后台,我方可以调用通过,就可以随便调用我们的接口了。
场景:
二,开放平台,三个角色,平台,服务提供方,第三方调用者
服务方把接口放到平台,第三方调用者接入平台,然后通过平台调用服务提供方接口。还是类似上面的问题,需要签名。
先考虑一下,存在什么风险
1,服务提供方接口放到平台,参数是固定的,如果别人改了参数,调用接口,这样就不行了。
2,第三方不改接口参数,如果这个调用者没有接入平台,他是不能调的,即需要身份验证
再看怎么实现:流程如下
1,第三方调用方和平台约定签名规则,如下
2,第三方调用方选择平台中已上线的接口,生成签名sign和请求url
3,第三方通过url(这个url中包含签名sign,就是校验位),调用平台接口
4,平台验签,是由网关做的,就是同样的签名规则,再生成一个签名,比较url中第三方传过来的签名,一直则验签通过
5,身份验证,是校验url中,应用appId存在
参数说明:
开放平台API请求参数分为两种:系统参数和业务参数
系统参数:访问开放平台必须要传的参数,包括应用appId,接口名称method、应用签名sign和时间戳timestamp
业务参数:是由接口提供商指定,调用方访问接口时必须传接口指定的参数,详情参考接口列表
签名规则:
签名sign是通过应用在平台的私钥和请求参数根据一定算法生成的签名值,主要是防止参数在传输过程中被篡改, 同时对调用方的身份进行校验
签名生成步骤:
第1步:
将除签名sign外的所有请求参数按参数名首字母进行升序排序,其中参数包括系统参数和业务参数
第2步:
将第1步得到的排序结果,依次按照"keyvalue"的形式拼接成待加密的串
第3步
对第2步得到的新串进行HMAC加密并转化为大写的十六进制签名串
签名示例如下:
假设请求为:/anji-open/open-api/request?appId=testApp&method=testApp&sign=8D45C66B6E1E773614E5866541EAF78D×tamp=111&name=hello&password=123456&age=12
1、关键信息: 应用私钥:29bca37bf0174ea287a770cd0d4ff83c 请求参数:appId=testApp&method=testApp&sign=8D45C66B6E1E773614E5866541EAF78D×tamp=111&name=hello&password=123456&age=12 2、将除"sign"外所有的参数进行排序:age、appId、method、name、password、timestamp; 依次按照"keyvalue"形式拼接的结果:age12appIdtestAppmethodtestAppnamehellopassword123456timestamp111 3、将得到的新串进行HMAC加密并转化为大写的十六进制签名串:8D45C66B6E1E773614E5866541EAF78D
实现:
** * 验证签名,防止数据被篡改 * @author lr * @since 2019-05-29 17:02 */ public class SignUtils { private final static String SIGN_PARAM="sign"; /** * 生产签名 * @param params * @param secret * @return * @throws IOException */ public static String generateSign(Mapparams, String secret) throws IOException { // 第一步:检查参数是否已经排序 String[] keys = params.keySet().toArray(new String[0]); Arrays.sort(keys); // 第二步:把所有参数名和参数值串在一起 StringBuilder query = new StringBuilder(); for (String key : keys) { //跳过参数"sign",sign不参与签名 if(StringUtils.equals(SIGN_PARAM,key)) { continue; } String value = params.get(key); if (StringUtils.isNotBlank(value)) { query.append(key).append(value); } } // 第三步:使用HMAC加密 byte[] bytes = encryptHMAC(query.toString(), secret); // 第四步:把二进制转化为大写的十六进制(正确签名应该为32大写字符串,此方法需要时使用) return byte2hex(bytes); } /** * 加密 * @param data * @param secret * @return * @throws IOException */ public static byte[] encryptHMAC(String data, String secret) throws IOException { byte[] bytes = null; try { SecretKey secretKey = new SecretKeySpec(secret.getBytes(ApiConstants.CHARSET_UTF8), "HmacMD5"); Mac mac = Mac.getInstance(secretKey.getAlgorithm()); mac.init(secretKey); bytes = mac.doFinal(data.getBytes(ApiConstants.CHARSET_UTF8)); } catch (GeneralSecurityException gse) { throw new IOException(gse.toString()); } return bytes; } /** * 转16进制 * @param bytes * @return */ public static String byte2hex(byte[] bytes) { StringBuilder sign = new StringBuilder(); for (int i = 0; i < bytes.length; i++) { String hex = Integer.toHexString(bytes[i] & 0xFF); if (hex.length() == 1) { sign.append("0"); } sign.append(hex.toUpperCase()); } return sign.toString(); } public static void main(String[] args) { Map map = new HashMap<>(16); map.put("appId","testApp"); map.put("method","testPost"); map.put("timestamp","111"); try { String s = generateSign(map, "e113e43bb76a437d94848a2bbbb45b0b"); System.out.println(s); } catch (IOException e) { e.printStackTrace(); } } }
三,公积金项目
流程:
调公积金接口,需要验证签名,签名规则MD5 apiKey
系统入参:apiKey timestamp sign
盐:appSecret