java API接口签名授权安全认证问题—基于HMAC的rest api鉴权处理

如何利用对称加密实现简单的请求鉴权。

前期沟通

服务端与客户端需要在前期敲定以下内容:

  1. 秘钥对(apiKey和secretKey),由服务端通过安全的途径交给客户端,如邮件、IM等内部渠道。
  2. 头部名称,包括APIKey、时间戳、签名及业务相关的头部。
  3. 加签算法,即根据业务参数及secretKey如何生成加密签名,客户端与服务端需保持一致。由客户端加密后的内容,在服务端用同样的秘钥加密应该是一模一样的。

客户端

流程

客户端的加签过程如下图所示。

代码

创建一个拦截器

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

测试类

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

服务端

验签流程

大致流程如下图所示。

在网关服务创建过滤器类

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

    }
}

就可以在相应的域名进行校验了!

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值