如何利用对称加密实现简单的请求鉴权。
前期沟通
服务端与客户端需要在前期敲定以下内容:
- 秘钥对(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());
}
}
}
就可以在相应的域名进行校验了!