Java如何保护RestAPI? 如何指定client可以访问API?
开始前,先看看微信小程序的设计
如何利用对称加密实现简单的请求鉴权。
前期沟通
服务端与客户端需要在前期敲定以下内容:
秘钥对(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?相关推荐
- Java读取修改xlsm格式表格_Android Excel电子表格API – 在Android应用程序中读取编辑XLS CSV XLSX XLSM HTML格式...
Android Excel Spreadsheet API 更多高级特征 具备格式化工作表,行,列,单元格等能力 Array,ArrayList 和 Recordset / Resultset数据导入 ...
- pythonjava app切出后无网络连接_写了一个java的Server 用python的client访问却访问不通问题。...
首先给出这个java的Server代码 try{//1.创建一个服务器端Socket,即ServerSocket,指定绑定的端口,并监听此端口 ServerSocket serverSocket=ne ...
- 【Groovy】Groovy 动态语言特性 ( Groovy 中的变量自动类型推断以及动态调用 | Java 中必须为变量指定其类型 )
文章目录 前言 一.Groovy 动态语言 二.Groovy 中的变量自动类型推断及动态调用 三.Java 中必须为变量指定其类型 前言 Groovy 是动态语言 , Java 是静态语言 ; 一.G ...
- Java删除properties配置文件中指定键值的代码
将开发过程较好的一些内容片段记录起来,下面的内容段是关于Java删除properties配置文件中指定键值的内容. public static boolean deleteKeyValue4Pro(S ...
- java安装jdk错误1316 指定的账户已存在
java安装jdk错误1316 指定的账户已存在 处理步骤: 1.卸载jdk,成功后重启 2.删除注册表中文件夹 (1)\HKEY_LOCAL_MACHINE\SOFTWARE\JavaSoft文件夹 ...
- Spring Cloud Feign 启动报错 java.lang.ClassNotFoundException: org.springframework.cloud.client.loadbalan
问题描述:在Spring Cloud项目中引入了feign后启动项目,项目会报错:java.lang.ClassNotFoundException: org.springframework.cloud ...
- Java中为什么有时候通过指定编码集无法解决乱码
@TOC Java中为什么有时候通过指定编码集无法解决乱码 当我们编程的时候都遇到过出现乱码的情况,这通常是使用了不匹配的编码表导致的,这是我们可以用指定的编码表的方式解决乱码问题如下面 的代码 St ...
- Java String字符串长度不足指定位数补0
Java String字符串长度不足指定位数补0 自己项目中有个需求:需要5位随机数-范围在1-99999 随机数可以利用Random()来获取,但是想要生成的随机数都是5位数,那就需要做一点小处理啦 ...
- Java实现生成可跳转指定页面的二维码
Java实现生成可跳转指定页面的二维码 package test; import java.awt.BasicStroke; import java.awt.Graphics; import java ...
最新文章
- 【赠书】图表示学习+图神经网络:破解AI黑盒,揭示万物奥秘的钥匙!
- 关于静态联编和动态联编
- PyCharm----快捷键
- 利用nginx的301重定向到另外服务器
- oracle查看所有用户_Oracle实用命令查看共用一个表空间的所有用户
- 电商视觉:焦点图的万能构图模板
- Vue.js 第二天: 事件处理
- Skyline开发1-环境搭建
- scala学习笔记一------初步了解scala
- 无U盘安装系统(到固态硬盘)教程
- A1339. JZPLCM(顾昱洲)|树状数组|hash表|逆元|分解质因数
- 美团java笔试题_美团笔试题目(Java后端5题2小时)
- 前端基础之HTML特殊字符集和表情集
- python实现小说分割器
- 1038:苹果和虫子
- C++STL之初识容器和迭代器
- 国王的金币for循环
- PN532模块复制IC加密卡
- 罗技g502鼠标宏设置教程分享
- 用 PyQt5 制作动态钟表