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;
}
}