今天看啥  ›  专栏  ›  Halburt

常用系统间接口调用认证设计/剖析/代码实现

Halburt  · 掘金  ·  · 2019-08-10 04:17
阅读 38

常用系统间接口调用认证设计/剖析/代码实现

简介

本文实现接口加密签名及校验。 可适用于绝大多数系统间接口调用。

签名实现

请求签名实现过程

  1. 将当前请求路径(不含域名。如:http:// x x x.com/sys/user/list ,其中请求路径即为/sys/user/list)作为URL参数的值(例如:URL=/sys/user/list),加上当前请求参数,对这些参数名进行升序排序,排序之后生成请求参数字符串queryStr(拼接时要对参数值进行URLEncoder.encode编码,防止中文等问题),形如:参数a=xx&参数b=22。
  2. 系统间约定加密字符串key【houdask2019】 ,生成当前时间戳time(毫秒数),获取当前用户编号 uid(注意uid key time 这三个参数均不参与排序)
  3. 将步骤1中生成的queryStr 和 time 、key 、uid 进行拼接,形成待加密字符串str。形如 queryStr &time=xx&key=xxx&uid=xxx
  4. 对待加密字符串str进行MD5加密,并转化成大写,即生成签名字符串hash。
  5. 客户端将uid放到请求header中,将原始请求参数和time、hash一起作参数传递(注意:其中URL参数不必传递)。

校验请求签名过程

  1. 从请求header中获取用户编号uid
  2. 从请求参数里获取签名字符串hash,以及请求时间time
  3. 获取当前请求路径,作为参数名为URL的参数值。【URL属于隐藏参数】
  4. 再将请求参数进行一遍签名加密,生成出来正确的签名字符串hash2
  5. 比较hash和hash2即可

优点:

  1. 参数防篡改(篡改参数之后签名不一致)
  2. 签名防串用(防止多个接口参数相同)
  3. 防过期调用(需校验time在三分钟或者更短时间内)
  4. 防暴力破解(含隐形参数,隐性参数名可以不用URL换成其他变量,增加安全性)
  5. 不可逆加密
  6. 简单易用,安全性高

缺点:

严重依赖key的保密性,如果key泄露和算法暴露,安全性就有问题。 建议不定时更改key。

代码实现

public class FkSignUtil {
    public static final String UID = "uid";
    /**
     * 加密秘钥
     */
    private static final String KEY = "key可以自定义";
    /**
     * 日志对象
     */
    private static  Logger logger = LoggerFactory.getLogger(FkSignUtil.class);
    /**
     * 生成签名【注意发送请求时一定要带上time 和hash 这2个参数】
     *
     * 功能:将一个Map按照Key字母升序构成一个QueryString. 并且加入时间混淆的hash串
     * @param queryMap  query内容
     * @param time  加密时候,为当前时间;解密时,为从querystring得到的时间;
     * @param uid 表示当前用户id
     * @return
     */
    public static String createSign(Map<String, Object> queryMap,long time, String uid) {
        if(null == uid || "".equals(uid)){
            return null ;
        }
        String qs = sortQueryParamString(queryMap);
        if (qs == null) {
            return null;
        }
        String hash = MD5Util.MD5(String.format("%s&time=%d&key=%s&uid=%s", qs, time , KEY ,uid));
        hash = hash.toUpperCase();
        return hash;
//        String params = String.format("%s&time=%d&hash=%s", qs, time, hash);
//        return params;
    }

    /**
     * 对请求参数进行排序 
     * @param params 请求参数 。注意请求参数中不能包含uid、time【这2参数是排序之后拼接的】
     * @return
     */
    private static String  sortQueryParamString(Map<String,Object> params)  {
        List<String> listKeys = Lists.newArrayList( params.keySet());
        Collections.sort(listKeys);
        StringBuilder content = new StringBuilder();

        for(String param : listKeys){
            try {
                content.append(param).append("=").append(URLEncoder.encode(params.get(param).toString(),"UTF-8")).append("&");
            } catch (UnsupportedEncodingException e) {
                e.printStackTrace();
            }
        }
        if(content.length()> 0){
            return content.substring(0 ,content.length() -1 );
        }
        return  content.toString();
    }

    /**
     * 解密判断是否签名正确
     * @param params 请求参数map【从request的参数中获取】
     * @param uid 表示当前用户id【从header中获取】
     * @return
     */
    public static boolean checkHashSign(Map<String, Object> params,String uid) {
        if(null == uid || "".equals(uid)){
            if (logger.isInfoEnabled()) {
                logger.info("checkHashSign ERROR: uid  is null.");
            }
            return false ;
        }
        if (!params.containsKey("hash") || !params.containsKey("time") ) {
            if (logger.isInfoEnabled()) {
                logger.info("checkHashSign ERROR: hash or  time  is null.");
            }
            return false;
        }
        String hash = (String) params.remove("hash");
        Long time =Long.parseLong((String) params.remove("time"));
        String signHash = createSign(params, time, uid);
        return hash.equals(signHash);
    }

public static void main(String[] args) {
        Map<String, Object> params = new HashMap<String, Object>();
        params.put("zhangId", 12321);
        params.put("guanId", true);
        params.put("test", "这是11AVC");
         params.put("URL","XXXXXX");
        params.put("name", "是的商家");
        String uid = "asdsa";
        long time = System.currentTimeMillis();
        String hash = createSign(params, time, uid);
        logger.info("hash={} ,time={}", hash, time);
         
        params.put("hash", hash);
        params.put("time", time);
        boolean flag = checkHashSign(params, uid);
        logger.info("校验flag={}  ", flag);
    }

}
复制代码

服务器端接口校验

通过Filter实现hash签名的校验。

校验请求签名过程

  1. 从请求header中获取用户编号uid
  2. 从请求参数里获取签名字符串hash,以及请求时间time
  3. 获取当前请求路径,作为参数名为URL的参数值。【URL属于隐藏参数】
  4. 再将请求参数进行一遍签名加密,生成出来正确的签名字符串hash2
  5. 比较hash和hash2即可
public class AppFilter implements Filter {
    /**
     * 日志对象
     */
    private static Logger logger = LoggerFactory.getLogger(AppFilter.class);

    private   static String CHECK_ERROR = null ,PARAM_ERROR = null;
	
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        CHECK_ERROR =  new FkJsonResult( "校验失败",FkJsonResult.DICT_COMMON_ERROR,"签名认证失败。" ).toJSONString();
        PARAM_ERROR =  new  FkJsonResult( "参数错误",FkJsonResult.DICT_COMMON_ERROR,"认证失败。" ).toJSONString();
    }

    public static Map<String,Object> reqParamterToMap(HttpServletRequest req){
        Map<String, String[]> m=req.getParameterMap();
        Map<String,Object> rm=new HashMap<String,Object>(m.size());
        Iterator<String> itor=m.keySet().iterator();
        while(itor.hasNext()){
            String key=itor.next();
            String[] strs=m.get(key);
            String val=null;
            if(strs.length>0){
                val=strs[0];
            }
            rm.put(key, val);
        }
//        添加当前请求地址作为参数 防止不同接口间互用秘钥,该参数属于隐含参数。
        rm.put("URL",req.getRequestURI());
        return rm;
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {

        if (request != null){
            HttpServletRequest request1 = (HttpServletRequest) request;
            String uid = request1.getHeader(FkSignUtil.UID);
            if(StringUtils.isEmpty(uid)){
                logger.info("header uid is null.");
                response.setCharacterEncoding("UTF-8");
                response.setContentType("application/json; charset=utf-8");
                PrintWriter out = response.getWriter();
                out.append(PARAM_ERROR);
            }else{
                Map map = reqParamterToMap(request1);
                boolean flag = FkSignUtil.checkHashSign(map , uid);
                logger.info(" check flag = {}" , flag);
                if(flag){
                    chain.doFilter(request,response);
                }else{
                    response.setCharacterEncoding("UTF-8");
                    response.setContentType("application/json; charset=utf-8");
                    PrintWriter out = response.getWriter();
                    out.append(CHECK_ERROR);
                }
            }
        }else{
            chain.doFilter(request,response);
        }
    }


    @Override
    public void destroy() {

    }
}
复制代码

其中定义json返回规范

  • code:状态编码,可以自定义 。1表示成功 0表示通用失败
  • msg:提示信息
  • data: 数据

/**
 * 定义 json返回
 */
public class FkJsonResult extends JSONObject implements  Serializable {

    /**
     * 通用成功
     */
    public static final String DICT_COMMON_SUCCESS = "1";
    /**
     * 通用失败
     */
    public static final String DICT_COMMON_ERROR = "0";
 
 
    private String code;
 
    private String message;
 
    private Object data;

    public FkJsonResult() {
    }

    /**
     *
     * @param data
     * @param code
     * @param message
     */
    public FkJsonResult(Object data, String code, String message ) {
        this.put("code",code);
        this.put("message",message);
        this.put("data",data);

    }

    public static FkJsonResult success(Object data){
        return new FkJsonResult(data , DICT_COMMON_SUCCESS,"ok");
    }

    public static FkJsonResult success(Object data, String message){
        return new FkJsonResult(data , DICT_COMMON_SUCCESS, message);
    }

    public static FkJsonResult success( ){
        return new FkJsonResult(null , DICT_COMMON_SUCCESS,"ok");
    }

    public static FkJsonResult error(String code, String message){
        return new FkJsonResult(null , code,message);
    }
    public static FkJsonResult error(String code, String message, Object data){
        return new FkJsonResult(data , code,message);
    }
    /**
     * 系统错误
     * @return
     */
    public static FkJsonResult error(  ){
        return new FkJsonResult("系统异常" , FkJsonResult.DICT_COMMON_ERROR, "系统维护中...");
    }
    public String getCode() {
        return this.getString("code");
    }

    public void setCode(String code) {
        this.put("code",code);
    }

    public String getMessage() {
        return this.getString("message");
    }

    public void setMessage(String message) {
        this.put("message",message);
    }

    public Object getData() {
        return this.get ("data");
    }

    public void setData(Object data) {
        this.put("data",data);
    }

    @Override
    public String toString() {
        return this.toJSONString();
    }
}
复制代码

将Filter配置到web.xml中

具体拦截规则 需要根据自己的情况定义。最好使用该Filter的接口统一请求路径前缀。

	<filter>
		<filter-name>appFilter</filter-name>
		<filter-class>com.xxxx.common.filter. AppFilter</filter-class>
	</filter>
	<filter-mapping>
		<filter-name>appFilter</filter-name>
		<url-pattern>/aa/*</url-pattern>
		此处要定义拦截规则
复制代码

由此服务器接收端已做好自动校验。

客户端调用

利用Spring RestTemplate,实现2个post调用方法。

  • 同步调用:建议使用异步调用

  • 异步调用:添加自定义线程池 并设计如下请求参数:

    • @param domain 域名
    • @param path 请求路径 (domain + path才是请求链接)
    • @param params 参数map
    • @param uid 用户标识

其中JsonResult 同服务器端的FkJsonResult.java。 TkSignUtil同服务器端的FkSignUtil.java.。

请求签名实现过程

  1. 将当前请求路径(不含域名。如:xx.com/sys/user/li… ,其中请求路径即为/sys/user/list)作为URL参数的值(例如:URL=/sys/user/list),加上当前请求参数,对这些参数名进行升序排序,排序之后生成请求参数字符串queryStr(拼接时要对参数值进行URLEncoder.encode编码,防止中文等问题),形如:参数a=xx&参数b=22。
  2. 系统间约定加密字符串key【houdask2019】 ,生成当前时间戳time(毫秒数),获取当前用户编号 uid(注意uid key time 这三个参数均不参与排序)
  3. 将步骤1中生成的queryStr 和 time 、key 、uid 进行拼接,形成待加密字符串str。形如 queryStr &time=xx&key=xxx&uid=xxx
  4. 对待加密字符串str进行MD5加密,并转化成大写,即生成签名字符串hash。
  5. 客户端将uid放到请求header中,将原始请求参数和time、hash一起作参数传递(注意:其中URL参数不必传递)。
public class RestTemplateUtils {


    private static Logger logger = LoggerFactory.getLogger(RestTemplateUtils.class);

    private static final RestTemplate restTemplate = new RestTemplate();


    private static ThreadFactory namedThreadFactory = new ThreadFactoryBuilder().setNameFormat("RestTemplateUtils-pool-%d").build();

    private static ExecutorService pool = new ThreadPoolExecutor(5, 200, 0L, TimeUnit.MILLISECONDS,
            new LinkedBlockingQueue<Runnable>(1024), namedThreadFactory, new ThreadPoolExecutor.AbortPolicy());

    /**
     * POST请求
     *
     * @param domain 域名
     * @param path   请求路径 (domain + path才是请求链接)
     * @param params 参数map
     * @param uid    用户标识
     * @return 返回结果  只有code == 1 才是成功返回
     */
    public static JsonResult post(String domain, String path, Map<String, Object> params, String uid) {
        if (StringUtils.isEmpty(uid)) {
            return JsonResult.error(JsonResult.DICT_COMMON_ERROR, "参数错误。", "uid is null.");
        }
//        签名
        params = getSignMap(params, uid, path);
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
        headers.set("uid", uid);

        MultiValueMap<String, Object> reqParams = new LinkedMultiValueMap();
        for (String s : params.keySet()) {
            reqParams.add(s, params.get(s).toString());
        }
        HttpEntity<MultiValueMap<String, Object>> entity = new HttpEntity<MultiValueMap<String, Object>>(reqParams, headers);
        ResponseEntity<JsonResult> resp = restTemplate.exchange
                (domain + path, HttpMethod.POST, entity, JsonResult.class);
        if (resp.getStatusCode().equals(HttpStatus.OK)) {
            return resp.getBody();
        } else {
            logger.error("{}{}请求错误{}:{}", domain, path, resp.getStatusCodeValue(), resp.getBody());
            return JsonResult.error(resp.getStatusCodeValue() + "", "请求失败", resp.getBody());
        }
    }

    /**
     * 异步调用POST请求
     *
     * @param domain 域名
     * @param path   请求路径 (domain + path才是请求链接)
     * @param params 参数map
     * @param uid    用户标识
     * @return 返回结果 FutureTask  通过FutureTask.get()即返回JsonResult。  只有code == 1 才是成功返回
     */
    public static FutureTask<JsonResult> asynPost(String domain, String path, Map<String, Object> params, String uid) {
        FutureTask<JsonResult> task = new FutureTask((Callable<JSONObject>) () -> post(domain, path, params, uid));
        pool.submit(task);
        return task;
    }


    /**
     * 获取签名参数并返回参数集合
     */
    private static Map getSignMap(Map<String, Object> chapterMap, String userId, String path) {
        long time = System.currentTimeMillis();
        chapterMap.put("URL", path);
        String hash = TkSignUtil.createSign(chapterMap, time, userId);
        logger.info("hash={} ,time={}", hash, time);
        chapterMap.put("hash", hash);
        chapterMap.put("time", time);
        chapterMap.remove("URL");
        return chapterMap;
    }
}
复制代码

优化方案

可以考虑使用token缓存,免加密解密校验。 同时也可以校验time是否不是在有效期内,比如判断time是不是在当前时间的前2分钟之内,防止接口扩散重复调用。

服务器端安全升级--https安全升级

利用阿里云申请 Symantec 免费版 SSL 证书。 在nginx上添加ssl证书到/etc/nginx/ssl.conf文件夹下面。 配置示例:


# 以下属性中以ssl开头的属性代表与证书配置有关,其他属性请根据自己的需要进行配置。
server {
    listen 443;
    server_name localhost;  # localhost修改为您证书绑定的域名。
    ssl on;   #设置为on启用SSL功能。
    root html;
    index index.html index.htm;
    ssl_certificate cert/domain name.pem;   #将domain name.pem替换成您证书的文件名。
    ssl_certificate_key cert/domain name.key;   #将domain name.key替换成您证书的密钥文件名。
    ssl_session_timeout 5m;
    ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4;  #使用此加密套件。
    ssl_protocols TLSv1 TLSv1.1 TLSv1.2;   #使用该协议进行配置。
    ssl_prefer_server_ciphers on;   
    location / {
        root html;   #站点目录。
        index index.html index.htm;   
    }
}
复制代码

如无法访问请检查nginx所在机器的443端口是否打开




原文地址:访问原文地址
快照地址: 访问文章快照