Problema de autenticación de seguridad de autorización de firma de interfaz API de Java: procesamiento de autenticación de API de descanso basado en HMAC

Cómo implementar la autenticación de solicitud simple mediante el cifrado simétrico.

Comunicación temprana

El servidor y el cliente deben finalizar lo siguiente en la etapa inicial:

  1. El servidor entrega el par de claves secretas (apiKey y secretKey) al cliente a través de canales seguros, como canales internos como el correo electrónico y la mensajería instantánea.
  2. Nombre del encabezado, incluida APIKey, marca de tiempo, firma y encabezados relacionados con el negocio.
  3. Algoritmo de firma, es decir, cómo generar una firma cifrada basada en parámetros comerciales y secretKey, el cliente y el servidor deben ser consistentes. El contenido cifrado por el cliente debe ser exactamente el mismo cuando se cifra con la misma clave en el servidor.

cliente

proceso

El proceso de firma del cliente se muestra en la siguiente figura.

el código

Crear un 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);
    }
}

clase de prueba

@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 {
    }
}

Servidor

Proceso de verificación de firma

El proceso general se muestra en la siguiente figura.

Crear una clase de filtro en el servicio de puerta de enlace

@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());
        }

    }
}

¡Puede verificarlo en el nombre de dominio correspondiente!

Supongo que te gusta

Origin blog.csdn.net/songkai558919/article/details/122706684
Recomendado
Clasificación