开始前,先看看微信小程序的设计

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

前期沟通

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

秘钥对(apiKey和secretKey),由服务端通过安全的途径交给客户端,如邮件、IM等内部渠道。
头部名称,包括APIKey、时间戳、签名及业务相关的头部。
加签算法,即根据业务参数及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());
        }
 
    }
}

Java如何保护RestAPI? 如何指定client可以访问API?相关推荐

  1. Java读取修改xlsm格式表格_Android Excel电子表格API – 在Android应用程序中读取编辑XLS CSV XLSX XLSM HTML格式...

    Android Excel Spreadsheet API 更多高级特征 具备格式化工作表,行,列,单元格等能力 Array,ArrayList 和 Recordset / Resultset数据导入 ...

  2. pythonjava app切出后无网络连接_写了一个java的Server 用python的client访问却访问不通问题。...

    首先给出这个java的Server代码 try{//1.创建一个服务器端Socket,即ServerSocket,指定绑定的端口,并监听此端口 ServerSocket serverSocket=ne ...

  3. 【Groovy】Groovy 动态语言特性 ( Groovy 中的变量自动类型推断以及动态调用 | Java 中必须为变量指定其类型 )

    文章目录 前言 一.Groovy 动态语言 二.Groovy 中的变量自动类型推断及动态调用 三.Java 中必须为变量指定其类型 前言 Groovy 是动态语言 , Java 是静态语言 ; 一.G ...

  4. Java删除properties配置文件中指定键值的代码

    将开发过程较好的一些内容片段记录起来,下面的内容段是关于Java删除properties配置文件中指定键值的内容. public static boolean deleteKeyValue4Pro(S ...

  5. java安装jdk错误1316 指定的账户已存在

    java安装jdk错误1316 指定的账户已存在 处理步骤: 1.卸载jdk,成功后重启 2.删除注册表中文件夹 (1)\HKEY_LOCAL_MACHINE\SOFTWARE\JavaSoft文件夹 ...

  6. Spring Cloud Feign 启动报错 java.lang.ClassNotFoundException: org.springframework.cloud.client.loadbalan

    问题描述:在Spring Cloud项目中引入了feign后启动项目,项目会报错:java.lang.ClassNotFoundException: org.springframework.cloud ...

  7. Java中为什么有时候通过指定编码集无法解决乱码

    @TOC Java中为什么有时候通过指定编码集无法解决乱码 当我们编程的时候都遇到过出现乱码的情况,这通常是使用了不匹配的编码表导致的,这是我们可以用指定的编码表的方式解决乱码问题如下面 的代码 St ...

  8. Java String字符串长度不足指定位数补0

    Java String字符串长度不足指定位数补0 自己项目中有个需求:需要5位随机数-范围在1-99999 随机数可以利用Random()来获取,但是想要生成的随机数都是5位数,那就需要做一点小处理啦 ...

  9. Java实现生成可跳转指定页面的二维码

    Java实现生成可跳转指定页面的二维码 package test; import java.awt.BasicStroke; import java.awt.Graphics; import java ...

最新文章

  1. 【赠书】图表示学习+图神经网络:破解AI黑盒,揭示万物奥秘的钥匙!
  2. 关于静态联编和动态联编
  3. PyCharm----快捷键
  4. 利用nginx的301重定向到另外服务器
  5. oracle查看所有用户_Oracle实用命令查看共用一个表空间的所有用户
  6. 电商视觉:焦点图的万能构图模板
  7. Vue.js 第二天: 事件处理
  8. Skyline开发1-环境搭建
  9. scala学习笔记一------初步了解scala
  10. 无U盘安装系统(到固态硬盘)教程
  11. A1339. JZPLCM(顾昱洲)|树状数组|hash表|逆元|分解质因数
  12. 美团java笔试题_美团笔试题目(Java后端5题2小时)
  13. 前端基础之HTML特殊字符集和表情集
  14. python实现小说分割器
  15. 1038:苹果和虫子
  16. C++STL之初识容器和迭代器
  17. 国王的金币for循环
  18. PN532模块复制IC加密卡
  19. 罗技g502鼠标宏设置教程分享
  20. 用 PyQt5 制作动态钟表

热门文章

  1. 云主机弹性公网IP(EIP)介绍
  2. modbus4j,rtu,ascii,tcp/ip传输模式
  3. 随机种子不随机(random_state)
  4. python猴子偷桃递归_用matlab编程解决猴子吃桃问题
  5. 九州云:元宇宙时代,赋能工业制造新场景
  6. 保存图片到相册及图片变黑问题
  7. 鸿合怎么删掉linux6_鸿合电子白板使用手册(共6页)
  8. Windows程序设计作业1
  9. 易语言入门精品课程发布了啊
  10. Juniper SSG 550M NSRP配置文档