Java API interface signature authorization security authentication problem - HMAC-based rest api authentication processing

How to implement simple request authentication using symmetric encryption.

Early communication

The server and the client need to finalize the following in the early stage:

  1. The secret key pair (apiKey and secretKey) is delivered by the server to the client through secure channels, such as internal channels such as email and IM.
  2. Header name, including APIKey, timestamp, signature and business-related headers.
  3. Signature algorithm, that is, how to generate an encrypted signature based on business parameters and secretKey, the client and server must be consistent. The content encrypted by the client should be exactly the same when encrypted with the same key on the server.

client

process

The signing process of the client is shown in the figure below.

the code

Create an interceptor

public class AkSkAuthInterceptor implements ClientHttpRequestInterceptor {

    private static final String HEADER_X_CONTENT_MD5 = "X-Content-MD5";
    private static final String HEADER_X_VERSION = "X-Sign-Version";
    private static final String HEADER_X_TIMESTAMP = "X-Timestamp";
    private static final String HEADER_X_NONCE = "X-Nonce";

    private final String accessKey;
    private final String secretKey;

    public AkSkAuthInterceptor(String accessKey, String secretKey) {
        this.accessKey = accessKey;
        this.secretKey = secretKey;
    }

    @Override
    public ClientHttpResponse intercept(
        HttpRequest request, byte[] body, ClientHttpRequestExecution execution
    ) throws IOException {
        request.getHeaders().set(HEADER_X_CONTENT_MD5, buildContentMD5(body));
        request.getHeaders().set(HEADER_X_VERSION, "1.0");
        request.getHeaders().set(HEADER_X_NONCE, UUID.randomUUID().toString().replace("-", ""));
        request.getHeaders().set(HEADER_X_TIMESTAMP, Long.toString(System.currentTimeMillis() / 1000));
        request.getHeaders().set("Authorization", "wayz " + accessKey + ":" + sign(request));
        return execution.execute(request, body);
    }

    private String sign(HttpRequest request) {

        byte[] sha = Hashing.hmacSha1(secretKey.getBytes())
            .hashString(buildStringToSign(request), StandardCharsets.UTF_8)
            .asBytes();
        return BaseEncoding.base64().encode(sha);
    }

   
    private String buildStringToSign(HttpRequest request) {

        return accessKey + "\r\n"
            + request.getMethodValue() + "\r\n"
            + request.getURI().getPath() + "\r\n"
            + sortedParamStr(request) + "\r\n"
            + getHeader(request, "Accept") + "\r\n"
            + getHeader(request, HEADER_X_CONTENT_MD5) + "\r\n"
            + getHeader(request, "Content-Type") + "\r\n"
            + getHeader(request, HEADER_X_TIMESTAMP) + "\r\n"
            + getHeader(request, HEADER_X_VERSION) + "\r\n"
            + getHeader(request, HEADER_X_NONCE);
    }

    private String sortedParamStr(HttpRequest request) {

        return splitQuery(request.getURI().getQuery()).entrySet().stream()
            .filter(e -> e.getValue() != null && !e.getValue().isEmpty())
            .sorted(Map.Entry.comparingByKey())
            .map(e -> e.getKey() + "=" + e.getValue().iterator().next())
            .collect(Collectors.joining("&"));
    }

    private Object getHeader(HttpRequest request, String key) {

        Collection<String> values = request.getHeaders().get(key);
        return values == null || values.isEmpty() ? "" : values.iterator().next();
    }

    // 此处的sign方法应与服务端的保持一致
    private String buildContentMD5(byte[] body) {

        if (body == null || body.length == 0) {
            return Hashing.md5().hashString("", StandardCharsets.UTF_8).toString();
        }

        return Hashing.md5().hashBytes(body).toString();
    }

    private Map<String, List<String>> splitQuery(String query) {
        if (Strings.isNullOrEmpty(query)) {
            return Collections.emptyMap();
        }
        return Arrays.stream(query.split("&"))
            .map(this::splitQueryParameter)
            .collect(Collectors.groupingBy(AbstractMap.SimpleImmutableEntry::getKey, LinkedHashMap::new, mapping(Map.Entry::getValue, toList())));
    }

    private AbstractMap.SimpleImmutableEntry<String, String> splitQueryParameter(String it) {
        final int idx = it.indexOf("=");
        final String key = idx > 0 ? it.substring(0, idx) : it;
        final String value = idx > 0 && it.length() > idx + 1 ? it.substring(idx + 1) : null;
        return new AbstractMap.SimpleImmutableEntry<>(key, value);
    }
}

test class

@Slf4j
public class testSign {

    public static void main(String[] args) {

        // test ak & sk
        RestSdkClint client = new RestSdkClint("https://lbi-api.newayz.com",
            ImmutableList.of(new AkSkAuthInterceptor(
                "ASYUwavcj18gplEuxvnBO2QLJU", "dwsxhqW0YjnicMI3DeZqjH29emc"
            )));

        SdkResponse<POIListReply> response = client.execute(new POIListArgs());
        log.info("{}", response);
    }

    @Data
    public static class POIListArgs implements SdkRequest<POIListReply> {

        private int needOneFieldOtherwiseCannotSerializeByJackson;

        @Override
        public HttpMethod getHttpMethod() {
            return HttpMethod.POST;
        }

        @Override
        public String getPath() {
            return "/openapi/v1/poi";
        }

        @Override
        public TypeReference<POIListReply> getResponseType() {
            return new TypeReference<POIListReply>() {
            };
        }

        @Override
        public MultiValueMap<String, String> getHeaders() {
            return null;
        }
    }

    @Data
    static class POIListReply {
    }
}

Server

Signature verification process

The general process is shown in the figure below.

Create a filter class in the gateway service

@Slf4j
@Component
/**
 * API签名认证
 * 依赖request path,需在会修改path的filter前面执行
 */
public class AuthorizationOpenApiFilterFactory extends AbstractGatewayFilterFactory<Config> {

    private static final String X_VERSION = "1.0";
    private static final long X_TIMESTAMP_EXPIRED_SEC = 30;
    private static final long NONCE_CACHE_PERIOD_MILLIS = TimeUnit.SECONDS.toMillis(X_TIMESTAMP_EXPIRED_SEC);
    private static final int MAX_NONCE_LENGTH = 32;

    private static final String HEADER_X_CONTENT_MD5 = "X-Content-MD5";
    private static final String HEADER_X_VERSION = "X-Sign-Version";
    private static final String HEADER_X_TIMESTAMP = "X-Timestamp";
    private static final String HEADER_X_NONCE = "X-Nonce";
    private static final String MISSING_MSG = "[MISSING]";

    // d41d8cd98f00b204e9800998ecf8427e
    private static final String EMPTY_BODY_MD5 = Hashing.md5()
        .hashString("", StandardCharsets.UTF_8).toString();
    private static final byte[] EMPTY_BODY = "".getBytes(StandardCharsets.UTF_8);

    private final NonceChecker nonceChecker;
    private final SecretKeyFinder secretKeyFinder;

    public AuthorizationOpenApiFilterFactory(
        final NonceChecker nonceChecker, final SecretKeyFinder secretKeyFinder
    ) {
        super(Config.class);

        Objects.requireNonNull(nonceChecker);
        Objects.requireNonNull(secretKeyFinder);

        this.nonceChecker = nonceChecker;
        this.secretKeyFinder = secretKeyFinder;
    }

    @Override
    public GatewayFilter apply(final Config config) {

        return (exchange, chain) -> verify(exchange, chain, VerifySignHelper.of(
                exchange.getRequest().getHeaders(), exchange.getRequest(),
                nonceChecker, secretKeyFinder
            ));
    }

    private Mono<Void> verify(
        ServerWebExchange exchange,
        GatewayFilterChain chain,
        VerifySignHelper verifySignHelper
    ) {

        return ServerRequest.create(exchange, HandlerStrategies.withDefaults().messageReaders())
            .bodyToMono(byte[].class)
            .defaultIfEmpty(EMPTY_BODY).flatMap(content -> {

                UserWithAkSk userWithAkSk = verifySignHelper.verify(content);

                String jwtToken = createUserToken(new User().setUserName(userWithAkSk.getName()).setUserType(userWithAkSk.getUserType()));

                ServerHttpRequest decorator =
                    new ServerHttpRequestDecorator(exchange.getRequest()) {
                        @Override
                        public HttpHeaders getHeaders() {
                            HttpHeaders httpHeaders = new HttpHeaders();
                            httpHeaders.putAll(super.getHeaders());
                            httpHeaders.set("jtoken", jwtToken);

                            return httpHeaders;
                        }

                        @Override
                        public Flux<DataBuffer> getBody() {
                            return Flux.defer(
                                () -> Mono.just(
                                    exchange.getResponse().bufferFactory().wrap(content)
                                ));
                        }
                    };

                return chain.filter(exchange.mutate().request(decorator).build());
            });
    }

    private String createUserToken(final User user) {
        try {
            return JwtUtils.createToken(user);
        } catch (final IllegalAccessException e) {
            log.error("", e);
            throw new IllegalStateException("BUG: cannot create user token");
        }
    }

    @Override
    public String name() {
        return "AuthorizationSignature";
    }

    static class Config {
        // empty
    }

    static class VerifySignHelper {

        private String method;
        private String accept;
        private String version;
        private String timestamp;
        private String nonce;
        private String contentMD5;
        private String accessKey;
        private String sign;
        private String path;
        private MultiValueMap<String, String> queries;
        private String contentType;

        private NonceChecker nonceChecker;
        private SecretKeyFinder secretKeyFinder;

        static VerifySignHelper of(
            final HttpHeaders headers, final ServerHttpRequest request,
            final NonceChecker nonceChecker, final SecretKeyFinder secretKeyFinder
        ) {

            Objects.requireNonNull(headers);
            Objects.requireNonNull(request);
            Objects.requireNonNull(nonceChecker);
            Objects.requireNonNull(secretKeyFinder);

            final String authorization = headers.getFirst(HttpHeaders.AUTHORIZATION);

            if (Strings.isNullOrEmpty(authorization)) {
                throw new ResultException(GatewayErrorCode.BAD_SIGN,
                    ImmutableMap.of(
                        HttpHeaders.AUTHORIZATION,
                        Objects.toString(authorization, MISSING_MSG)
                    ));
            }

            // wayz accessKey:sign
            final String prefix = "wayz ";
            if (!authorization.startsWith(prefix)) {
                throw new ResultException(GatewayErrorCode.BAD_SIGN,
                    ImmutableMap.of(HttpHeaders.AUTHORIZATION, authorization));
            }

            final String[] elts = authorization.substring(prefix.length()).split(":");
            if (elts.length != 2) {
                throw new ResultException(GatewayErrorCode.BAD_SIGN,
                    ImmutableMap.of(HttpHeaders.AUTHORIZATION, authorization));
            }

            String accessKey = elts[0];
            String sign = elts[1];

            String accept = "";
            final List<MediaType> mediaTypes = headers.getAccept();
            if (!mediaTypes.isEmpty()) {
                accept = mediaTypes.get(0).toString();
            }

            VerifySignHelper helper =  new VerifySignHelper();

            helper.method = request.getMethodValue();
            helper.accept = accept;
            helper.version = headers.getFirst(HEADER_X_VERSION);
            helper.timestamp = headers.getFirst(HEADER_X_TIMESTAMP);
            helper.nonce = headers.getFirst(HEADER_X_NONCE);
            helper.contentMD5 = headers.getFirst(HEADER_X_CONTENT_MD5);
            helper.accessKey = accessKey;
            helper.sign = sign;
            helper.path = request.getPath().value();
            helper.queries = request.getQueryParams();
            helper.contentType = Objects.toString(headers.getFirst(HttpHeaders.CONTENT_TYPE), "");

            helper.nonceChecker = nonceChecker;
            helper.secretKeyFinder = secretKeyFinder;

            return helper;
        }

        void checkVersion() {

            if (!X_VERSION.equals(version)) {
                throw new ResultException(GatewayErrorCode.BAD_SIGN_VERSION,
                    ImmutableMap.of(
                        HEADER_X_VERSION,
                        Objects.toString(version, MISSING_MSG)
                    ));
            }
        }

        void checkTimestamp() {

            if (Strings.isNullOrEmpty(timestamp)) {
                throw new ResultException(GatewayErrorCode.BAD_TIMESTAMP,
                    ImmutableMap.of(
                        HEADER_X_TIMESTAMP,
                        Objects.toString(timestamp, MISSING_MSG)
                    ));
            }

            try {
                final long now = System.currentTimeMillis() / 1000;
                if ((now - Long.parseLong(timestamp)) >= X_TIMESTAMP_EXPIRED_SEC) {
                    throw new ResultException(GatewayErrorCode.REQUEST_OUT_OF_DATE,
                        ImmutableMap.of(HEADER_X_TIMESTAMP, timestamp, "serverTimestamp", now));
                }
            } catch (final NumberFormatException e) {
                throw new ResultException(GatewayErrorCode.BAD_TIMESTAMP,
                    ImmutableMap.of(HEADER_X_TIMESTAMP, timestamp));
            }
        }

        void checkNonce() {

            if (Strings.isNullOrEmpty(nonce) || nonce.length() > MAX_NONCE_LENGTH) {

                throw new ResultException(GatewayErrorCode.BAD_NONCE,
                    ImmutableMap.of(
                        HEADER_X_NONCE, Objects.toString(nonce, MISSING_MSG)
                    )
                );
            }
            nonceChecker.check(nonce, NONCE_CACHE_PERIOD_MILLIS);
        }

        void checkContentMD5(final String contentMD5) {

            if (Strings.isNullOrEmpty(this.contentMD5) || !this.contentMD5
                .equalsIgnoreCase(contentMD5)) {

                throw new ResultException(GatewayErrorCode.BAD_CONTENT_MD5,
                    ImmutableMap.of(
                        HEADER_X_CONTENT_MD5,
                        Objects.toString(this.contentMD5, MISSING_MSG),
                        "calcContentMD5",
                        contentMD5
                    )
                );
            }
        }

        UserWithAkSk verify(final String contentMD5) {

            checkVersion();
            checkTimestamp();
            checkNonce();
            checkContentMD5(contentMD5);

            final UserWithAkSk userWithAkSk = secretKeyFinder.find(accessKey);

            final String sortedQueries = Objects.isNull(queries) ? "" :
                queries.toSingleValueMap().entrySet().stream().sorted(Entry.comparingByKey())
                    .map(e -> e.getKey() + "=" + Objects.toString(e.getValue(), ""))
                    .collect(Collectors.joining("&"));

            final String toSignStr = String.join("\r\n", accessKey, method, path,
                sortedQueries, accept, contentMD5, contentType, timestamp, version, nonce);

            final String secretKey = userWithAkSk.getSecretKey();
            final String calcSign = Base64.getEncoder().encodeToString(
                Hashing.hmacSha1(secretKey.getBytes(StandardCharsets.UTF_8))
                    .hashString(toSignStr, StandardCharsets.UTF_8).asBytes());
            if (!calcSign.equalsIgnoreCase(sign)) {
                log.error("verify sign failed: request:[{}], calc: [{}], secretKey:[{}], toSignStr: [{}]",
                    sign, calcSign, secretKey, toSignStr);
                throw new ResultException(GatewayErrorCode.INCORRECT_SIGN, ImmutableMap.of(
                    "requestSign", sign
                ));
            }

            return userWithAkSk;
        }

        UserWithAkSk verify(final byte[] content) {
            return verify(
                Objects.isNull(content) ?
                    EMPTY_BODY_MD5 :
                    Hashing.md5().hashBytes(content).toString());
        }

    }
}

You can verify it on the corresponding domain name!

Guess you like

Origin blog.csdn.net/songkai558919/article/details/122706684