Java Httpインタフェース署名チェックの例---東科教育


一、業務背景
サードパーティインタフェースまたは前後端分離開発を呼び出し、業務インタフェースを呼び出す際にブラシ/違反呼び出しなどの現象を防止するために、支払い類などのインタフェースをチェックするメカニズムが必要である.インタフェース双方はデータパラメータが伝送中に改ざんされていないことを確保するために、いずれもインタフェースデータに対してチェックを行い、その後、インタフェースサーバ側でインタフェースパラメータに対してチェックを行う必要がある.2つの署名が同じであることを確認し、チェックが通過した後、ビジネスロジック処理を行います.
 
二、処理の考え方
署名チェックの大きな方向は、クライアントとサービス側が約束し、統一アルゴリズム、統一パラメータ、統一順序、統一鍵に従って暗号化し、比較することにほかならない.ここでは、比較的簡単で実用的でよく使われる署名/チェック方式の考え方を紹介します.クライアント:    署名はヘッダーを介してサービス側に渡され、key名はtokenです.      署名:APIバージョン番号+AK+署名有効期限+鍵(SK)+URL+requestMethod+クライアント順序付けパラメータ 暗号化する
        暗号化アルゴリズム:MD 5;        鍵(SK):前後が統一されており、署名暗号化の際に使用するのは塩を加えることに相当する.        AK:マルチクライアントは異なるSKを使用しており、サービス側はAKを通じて対応するSKを見つけて暗号化チェックを行う必要がある.        署名の有効期限:クライアント定義の署名の有効期限が切れ、その時間を超えて署名要求が通過できない.
    要求パラメータ:並べ替えられたパラメータはrequest-lineまたはrequest-bodyを介してサービス側に渡されます.
サービス:
     同じ方法で署名、検証を行います.呼び出し者が合法かどうかを確認することがインタフェース署名検証の考え方である.
 
サーバ・エンド・コードは次のとおりです(参照).
    springブロッキングHandlerInterceptor(内部でrequest要求bodyパラメータをlineパラメータ、So、直接getParameter)
/**
   * @Description:    
   * @Author pengju
   * @date 2018/4/11 13:44 
*/
public class ApiAkSkInterceptor implements HandlerInterceptor {

    private final Logger logger = LoggerFactory.getLogger(ApiAkSkInterceptor.class);

    @Autowired
    private BaseOpenApiConfig baseOpenApiConfig;

    /**
     * Intercept the execution of a handler. Called after HandlerMapping determined
     * an appropriate handler object, but before HandlerAdapter invokes the handler.
     * 

DispatcherServlet processes a handler in an execution chain, consisting * of any number of interceptors, with the handler itself at the end. * With this method, each interceptor can decide to abort the execution chain, * typically sending a HTTP error or writing a custom response. *

Note: special considerations apply for asynchronous * request processing. For more details see * {@link AsyncHandlerInterceptor}. * * @param request current HTTP request * @param response current HTTP response * @param handler chosen handler to execute, for type and/or instance evaluation * @return {@code true} if the execution chain should proceed with the * next interceptor or the handler itself. Else, DispatcherServlet assumes * that this interceptor has already dealt with the response itself. * @throws Exception in case of errors */ @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (handler == null || !(handler instanceof HandlerMethod)) { return true; } HandlerMethod handlerMethod = (HandlerMethod) handler; if (handlerMethod.getBeanType().isAnnotationPresent(ApiAkSkNotValidator.class) || handlerMethod.getMethod().isAnnotationPresent(ApiAkSkNotValidator.class)) { return true; } final long bt = System.currentTimeMillis(); response.addHeader(HTTP.CONTENT_TYPE, "application/json;charset=UTF-8"); try { // get header token String requestUri = request.getRequestURI(); final String requestMethod = request.getMethod(); final String token = request.getHeader(TokenHelper.X_IBEE_AUTH_TOKEN); logger.info("checkApiAkSkToken: check token,request uri={},method={},header token={}", requestUri, requestMethod, token); // token if (StringUtils.isBlank(token) || token.split("-").length != 4) { logger.error("checkApiAkSkToken: token invalid,head key={},token={}", TokenHelper.X_IBEE_AUTH_TOKEN, token); ResultBean result = new ResultBean(CommonEnum.ResponseEnum.AKSK_ERROR.getCode(), CommonEnum.ResponseEnum.AKSK_ERROR.getMsg(), null); return BaseOpenApiConfig.ajaxTimeOut(result, response); } // check ak value final String ak = token.split("-")[1]; if (StringUtils.isBlank(ak)) { logger.error("checkApiAkSkToken: ak is blank,token={}", token); ResultBean result = new ResultBean(CommonEnum.ResponseEnum.AKSK_ERROR.getCode(), CommonEnum.ResponseEnum.AKSK_ERROR.getMsg(), null); return BaseOpenApiConfig.ajaxTimeOut(result, response); } // get request body String jsonReqBody = parameter2Json(request); final String askReqBody = BaseOpenApiConfig.BLANK_JSON.equals(jsonReqBody) ? null : jsonReqBody; final String queryStr = request.getQueryString(); // check sk value final String sk = baseOpenApiConfig.getSkValue(ak); if (StringUtils.isBlank(sk)) { logger.error("checkApiAkSkToken: sk is blank,ak={}", ak); ResultBean result = new ResultBean(CommonEnum.ResponseEnum.AKSK_ERROR.getCode(), CommonEnum.ResponseEnum.AKSK_ERROR.getMsg(), null); return BaseOpenApiConfig.ajaxTimeOut(result, response); } // do check if (!new TokenHelper(ak, sk).verifyToken(token, requestUri, requestMethod, queryStr, askReqBody)) { logger.error("checkApiAkSkToken: token invalid,do not access,uri={},token={}", requestUri, token); ResultBean result = new ResultBean(CommonEnum.ResponseEnum.AKSK_ERROR.getCode(), CommonEnum.ResponseEnum.AKSK_ERROR.getMsg(), null); return BaseOpenApiConfig.ajaxTimeOut(result, response); } // set response header response.addHeader(TokenHelper.X_IBEE_AUTH_TOKEN, token); return true; } catch (Exception e) { logger.error("checkApiAkSkToken: catch e.", e); ResultBean result = new ResultBean(CommonEnum.ResponseEnum.AKSK_ERROR.getCode(), CommonEnum.ResponseEnum.AKSK_ERROR.getMsg(), null); return BaseOpenApiConfig.ajaxTimeOut(result, response); } finally { logger.debug("checkApiAkSkToken: sn={},end,use time={}ms.", bt, (System.currentTimeMillis() - bt)); } } /** * Intercept the execution of a handler. Called after HandlerAdapter actually * invoked the handler, but before the DispatcherServlet renders the view. * Can expose additional model objects to the view via the given ModelAndView. *

DispatcherServlet processes a handler in an execution chain, consisting * of any number of interceptors, with the handler itself at the end. * With this method, each interceptor can post-process an execution, * getting applied in inverse order of the execution chain. *

Note: special considerations apply for asynchronous * request processing. For more details see * {@link AsyncHandlerInterceptor}. * * @param request current HTTP request * @param response current HTTP response * @param handler handler (or {@link HandlerMethod}) that started asynchronous * execution, for type and/or instance examination * @param modelAndView the {@code ModelAndView} that the handler returned * (can also be {@code null}) * @throws Exception in case of errors */ @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { } /** * Callback after completion of request processing, that is, after rendering * the view. Will be called on any outcome of handler execution, thus allows * for proper resource cleanup. *

Note: Will only be called if this interceptor's {@code preHandle} * method has successfully completed and returned {@code true}! *

As with the {@code postHandle} method, the method will be invoked on each * interceptor in the chain in reverse order, so the first interceptor will be * the last to be invoked. *

Note: special considerations apply for asynchronous * request processing. For more details see * {@link AsyncHandlerInterceptor}. * * @param request current HTTP request * @param response current HTTP response * @param handler handler (or {@link HandlerMethod}) that started asynchronous * execution, for type and/or instance examination * @param ex exception thrown on handler execution, if any * @throws Exception in case of errors */ @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { } /** * request map */ @SuppressWarnings("unchecked") public static String parameter2Json(HttpServletRequest request) { LinkedHashMap linkMap = new LinkedHashMap<>(); Enumeration enu = request.getParameterNames(); while (enu.hasMoreElements()) { String paraName = enu.nextElement(); String value = request.getParameter(paraName); linkMap.put(paraName, value); System.out.println(paraName + ":" + value); } return JSONObject.toJSONString(linkMap); } }


 
AKSKConfig:
 
/**
   * @Description:AKSKConfig
   * @Author pengju
   * @date 2018/4/11 13:32 
*/
@Component
public class BaseOpenApiConfig {

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


    @Value("${api.ak.sk}")
    private String KEY_API_AK_SK;

    public final static String BLANK_JSON = "{}";
    public final static Map akSkMap = new ConcurrentHashMap<>();

    @PostConstruct
    public void initAKSKMap() {
        String[] akSksArray = KEY_API_AK_SK.split("#");
        if (ArrayUtils.isEmpty(akSksArray)) {
            logger.error("initAKSKMap: akSksArray is empty");
        }
        for (String akSk : akSksArray) {
            String[] akSkArray = akSk.split(":");
            if (ArrayUtils.isEmpty(akSkArray) && akSkArray.length < 2) {
                logger.error("initAKSKMap: akSkArray is error");
                continue;
            }
            String localAk = akSkArray[0];
            String localSk = akSkArray[1];
            akSkMap.put(localAk, localSk);
        }
    }

    public static String streamToStr(final boolean closeReader, BufferedReader reader) throws IOException {
        if (reader == null) {
            return null;
        }
        String inLine;
        String str = "";
        try {
            while ((inLine = reader.readLine()) != null) {
                str += inLine;
            }
            return str;
        } finally {
            if (closeReader) {
                reader.close();
            }
        }
    }

    public String getSkValue(final String ak) throws Exception {
        if (StringUtils.isBlank(ak)) {
            logger.error("getSkValue: ak is blank");
            return null;
        }
        return akSkMap.get(ak);
    }

    public static String getSignErrorMsg(final ReloadableResourceBundleMessageSource messageSource, String errorKey) {
        if (messageSource != null) {
            Locale locale = LocaleContextHolder.getLocale();
            if (locale == null) {
                locale = Locale.SIMPLIFIED_CHINESE;
            }
            return messageSource.getMessage(errorKey, null, locale);
        }
        return "error msg";
    }

    public static boolean ajaxTimeOut(ResultBean result, HttpServletResponse response) {
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json; charset=utf-8");
        response.setHeader("Pragma", "No-cache");
        response.setHeader("Cache-Control", "no-cache");
        response.setHeader("Cache-Control", "no-store");
        response.setDateHeader("Expires", 0);
        String json = JsonUtil.objectToJson(result);
        response.setHeader("X-Response-Json", json);
        logger.info("------------ajaxTimeOutResponse: logs--------------");
        logger.info(json);
        return false;
    }

}

 
署名:
 
/**
   * @Description:    
   * @Author pengju
   * @date 2018/4/11 13:34 
*/
public class TokenHelper {
    private static final Logger logger = LoggerFactory.getLogger(TokenHelper.class);

    private String accessKey;

    private String secretKey;

    private static final String TOKEN_VERSION = "v2";

    public static final String X_IBEE_AUTH_TOKEN = "X-IbeeAuth-Token";

    private final List allowedMethods = Arrays.asList("GET", "POST", "PUT", "DELETE", "HEAD");

    public TokenHelper() {
    }

    public TokenHelper(String ak, String sk) {
        this.accessKey = ak;
        this.secretKey = sk;
    }

    /**
     * generate the token according the request or response contents
     *
     * @param urlPath    the url of request
     * @param method     request method, must be one of 'GET', 'POST', 'DELETE', 'HEAD',
     *                   'PUT'
     * @param queryParam the query string of request
     * @param body       the post body for request, or response body
     * @param expireTime the token expired time
     * @return the token
     */
    public String generateToken(String urlPath, String method, String queryParam, String body, int expireTime) {
        if (accessKey == null || accessKey.isEmpty() || secretKey == null || secretKey.isEmpty()) {
            throw new IllegalArgumentException("Invalid AK or SK");
        }
        if (urlPath == null || urlPath.isEmpty()) {
            throw new IllegalArgumentException("Empty url path");
        }
        if (!allowedMethods.contains(method)) {
            throw new IllegalArgumentException("invalid request method");
        }
        String token;
        try {
            // |v2-{AK}-{ExpireTime}|{SK}|
            StringBuffer sbSign = new StringBuffer(String.format("|%s-%s-%d|%s|", TOKEN_VERSION, accessKey, expireTime, secretKey));

            // {UrlPath}|
            sbSign.append(decodeUtf8(urlPath)).append("|");

            // {Method}|
            sbSign.append(method).append("|");

            // {QueryParam}|
            if (StringUtils.isNoneBlank(queryParam)) {
                List qsArray = new ArrayList();
                for (String kv : queryParam.split("&")) {
                    String[] t = kv.split("=");
                    if (t.length > 1) {
                        qsArray.add(String.format("%s=%s", decodeUtf8(t[0]), decodeUtf8(t[1])));
                    } else {
                        qsArray.add(String.format("%s=", decodeUtf8(t[0])));
                    }
                }
                Collections.sort(qsArray);
                boolean first = true;
                for (String s : qsArray) {
                    if (first) {
                        first = false;
                    } else {
                        sbSign.append("&");
                    }
                    sbSign.append(s);
                }
            }
            sbSign.append("|");

            // {body}|
            if (StringUtils.isNoneBlank(body)) {
                sbSign.append(body);
            }
            sbSign.append("|");

            MessageDigest digest = MessageDigest.getInstance("MD5");
            digest.reset();
            digest.update(sbSign.toString().getBytes("UTF-8"));

            // v2-{AK}-{ExpireTime}-{Signature}
            token = String.format("%s-%s-%s-%s", TOKEN_VERSION, accessKey, expireTime, new String(Hex.encodeHex(digest.digest())));
        } catch (Exception e) {
            logger.error("failed to decode url or query path,e.msg={}", e.getMessage());
            throw new IllegalStateException("Bad encoded url path or query string");
        }
        return token;
    }

    private static String decodeUtf8(String url) {
        try {
            return URLDecoder.decode(url, "UTF-8");
        } catch (UnsupportedEncodingException var2) {
            return url;
        }
    }

    /**
     * verify the token
     *
     * @param token      the token for verification
     * @param urlPath    the url of request
     * @param method     request method, must be one of 'GET', 'POST', 'DELETE', 'HEAD',
     *                   'PUT'
     * @param queryParam the query string of request
     * @param body       the post body for request, or response body
     */
    public boolean verifyToken(String token, String urlPath, String method, String queryParam, String body) {
        if (StringUtils.isBlank(token)) {
            logger.warn("token is null");
            return false;
        }
        try {
            String[] tokenParts = token.split("-");
            if (tokenParts.length != 4) {
                logger.warn("invalid token format");
                return false;
            }
            if (!TOKEN_VERSION.equals(tokenParts[0])) {
                logger.warn("invalid token protocol version");
                return false;
            }
            int expireTime = Integer.parseInt(tokenParts[2]);
            if (expireTime < System.currentTimeMillis() / 1000) {
                logger.warn("expired token");
                return false;
            }
            String tokenVerify = generateToken(urlPath, method, queryParam, body, expireTime);
            if (token.equals(tokenVerify)) {
                return true;
            }
        } catch (Exception e) {
            logger.error("failed to parse token '{}',e.msg={}", token, e.getMessage());
        }
        return false;
    }

    public String getAccessKey() {
        return accessKey;
    }

    public void setAccessKey(String accessKey) {
        this.accessKey = accessKey;
    }

    public String getSecretKey() {
        return secretKey;
    }

    public void setSecretKey(String secretKey) {
        this.secretKey = secretKey;
    }
}