From 84b90a366cb499cdd5ec770a82f48bc95e4b35e5 Mon Sep 17 00:00:00 2001 From: tjq Date: Fri, 9 Aug 2024 20:55:08 +0800 Subject: [PATCH] feat: open-api support auth --- .../tech/powerjob/client/ClientConfig.java | 63 +++++++++ .../tech/powerjob/client/common/Protocol.java | 28 ++++ .../client/module/AppAuthRequest.java | 33 +++++ .../powerjob/client/module/AppAuthResult.java | 25 ++++ .../client/service/RequestService.java | 13 ++ .../impl/AppAuthClusterRequestService.java | 33 +++++ .../service/impl/ClusterRequestService.java | 123 ++++++++++++++++++ .../ClusterRequestServiceOkHttp3Impl.java | 109 ++++++++++++++++ .../tech/powerjob/common/OpenAPIConstant.java | 8 ++ .../common/response/PowerResultDTO.java | 37 ++++++ .../powerjob/common/serialize/JsonUtils.java | 3 +- .../powerjob}/common/utils/DigestUtils.java | 8 +- powerjob-server/pom.xml | 6 + .../server/auth/common/AuthErrorCode.java | 2 + .../auth/jwt/impl/DefaultSecretProvider.java | 2 +- .../login/impl/PwjbAccountLoginService.java | 2 +- .../server/core/service/AppInfoService.java | 20 +++ .../core/service/impl/AppInfoServiceImpl.java | 40 ++++++ .../powerjob-server-starter/pom.xml | 5 + .../OpenAPIController.java | 22 +++- .../server/openapi/OpenApiInterceptor.java | 46 +++++++ .../security/OpenApiSecurityService.java | 28 ++++ .../security/OpenApiSecurityServiceImpl.java | 98 ++++++++++++++ .../web/ControllerExceptionHandler.java | 7 +- .../server/web/response/WebResultDTO.java | 27 ---- .../impl/PwjbUserWebServiceImplImpl.java | 2 +- .../src/main/resources/application.properties | 3 +- 27 files changed, 751 insertions(+), 42 deletions(-) create mode 100644 powerjob-client/src/main/java/tech/powerjob/client/ClientConfig.java create mode 100644 powerjob-client/src/main/java/tech/powerjob/client/common/Protocol.java create mode 100644 powerjob-client/src/main/java/tech/powerjob/client/module/AppAuthRequest.java create mode 100644 powerjob-client/src/main/java/tech/powerjob/client/module/AppAuthResult.java create mode 100644 powerjob-client/src/main/java/tech/powerjob/client/service/RequestService.java create mode 100644 powerjob-client/src/main/java/tech/powerjob/client/service/impl/AppAuthClusterRequestService.java create mode 100644 powerjob-client/src/main/java/tech/powerjob/client/service/impl/ClusterRequestService.java create mode 100644 powerjob-client/src/main/java/tech/powerjob/client/service/impl/ClusterRequestServiceOkHttp3Impl.java create mode 100644 powerjob-common/src/main/java/tech/powerjob/common/response/PowerResultDTO.java rename {powerjob-server/powerjob-server-common/src/main/java/tech/powerjob/server => powerjob-common/src/main/java/tech/powerjob}/common/utils/DigestUtils.java (86%) rename powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/{web/controller => openapi}/OpenAPIController.java (93%) create mode 100644 powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/openapi/OpenApiInterceptor.java create mode 100644 powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/openapi/security/OpenApiSecurityService.java create mode 100644 powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/openapi/security/OpenApiSecurityServiceImpl.java delete mode 100644 powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/response/WebResultDTO.java diff --git a/powerjob-client/src/main/java/tech/powerjob/client/ClientConfig.java b/powerjob-client/src/main/java/tech/powerjob/client/ClientConfig.java new file mode 100644 index 00000000..55f288c6 --- /dev/null +++ b/powerjob-client/src/main/java/tech/powerjob/client/ClientConfig.java @@ -0,0 +1,63 @@ +package tech.powerjob.client; + +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import tech.powerjob.client.common.Protocol; + +import java.io.Serializable; +import java.util.List; +import java.util.Map; + +/** + * 客户端配置 + * + * @author 程序帕鲁 + * @since 2024/2/20 + */ +@Getter +@Setter +@ToString +public class ClientConfig implements Serializable { + + /** + * 执行器 AppName + */ + private String appName; + + /** + * 执行器密码 + */ + private String password; + + /** + * 地址列表,支持格式: + * - IP:Port, eg: 192.168.1.1:7700 + * - 域名, eg: powerjob.apple-inc.com + */ + private List addressList; + + /** + * 客户端通讯协议 + */ + private Protocol protocol = Protocol.HTTP; + + /** + * 连接超时时间 + */ + private Integer connectionTimeout; + /** + * 指定了等待服务器响应数据的最长时间。更具体地说,这是从服务器开始返回响应数据(包括HTTP头和数据)后,客户端读取数据的超时时间 + */ + private Integer readTimeout; + /** + * 指定了向服务器发送数据的最长时间。这是从客户端开始发送数据(如POST请求的正文)到数据完全发送出去的时间 + */ + private Integer writeTimeout; + + /** + * 默认携带的请求头 + * 用于流量被基础设施识别 + */ + private Map defaultHeaders; +} diff --git a/powerjob-client/src/main/java/tech/powerjob/client/common/Protocol.java b/powerjob-client/src/main/java/tech/powerjob/client/common/Protocol.java new file mode 100644 index 00000000..5168a642 --- /dev/null +++ b/powerjob-client/src/main/java/tech/powerjob/client/common/Protocol.java @@ -0,0 +1,28 @@ +package tech.powerjob.client.common; + +import lombok.Getter; + +/** + * Protocol + * + * @author tjq + * @since 2024/2/20 + */ +@Getter +public enum Protocol { + + HTTP("http"), + + HTTPS("https"); + + private final String protocol; + + Protocol(String protocol) { + this.protocol = protocol; + } + + @Override + public String toString() { + return protocol; + } +} diff --git a/powerjob-client/src/main/java/tech/powerjob/client/module/AppAuthRequest.java b/powerjob-client/src/main/java/tech/powerjob/client/module/AppAuthRequest.java new file mode 100644 index 00000000..66c8a2b6 --- /dev/null +++ b/powerjob-client/src/main/java/tech/powerjob/client/module/AppAuthRequest.java @@ -0,0 +1,33 @@ +package tech.powerjob.client.module; + +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +import java.io.Serializable; +import java.util.Map; + +/** + * App 鉴权请求 + * + * @author tjq + * @since 2024/2/19 + */ +@Getter +@Setter +@ToString +public class AppAuthRequest implements Serializable { + + /** + * 应用名称 + */ + private String appName; + /** + * 加密后密码 + */ + private String encryptedPassword; + /** + * 额外参数,方便开发者传递其他参数 + */ + private Map extra; +} diff --git a/powerjob-client/src/main/java/tech/powerjob/client/module/AppAuthResult.java b/powerjob-client/src/main/java/tech/powerjob/client/module/AppAuthResult.java new file mode 100644 index 00000000..74c109e2 --- /dev/null +++ b/powerjob-client/src/main/java/tech/powerjob/client/module/AppAuthResult.java @@ -0,0 +1,25 @@ +package tech.powerjob.client.module; + +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +import java.io.Serializable; + +/** + * App 鉴权响应 + * + * @author tjq + * @since 2024/2/21 + */ +@Getter +@Setter +@ToString +public class AppAuthResult implements Serializable { + + private Long appId; + + private String token; + + private String extra; +} diff --git a/powerjob-client/src/main/java/tech/powerjob/client/service/RequestService.java b/powerjob-client/src/main/java/tech/powerjob/client/service/RequestService.java new file mode 100644 index 00000000..85d8883a --- /dev/null +++ b/powerjob-client/src/main/java/tech/powerjob/client/service/RequestService.java @@ -0,0 +1,13 @@ +package tech.powerjob.client.service; + +/** + * 请求服务 + * + * @author tjq + * @since 2024/2/20 + */ +public interface RequestService { + + + String request(String path, Object body); +} diff --git a/powerjob-client/src/main/java/tech/powerjob/client/service/impl/AppAuthClusterRequestService.java b/powerjob-client/src/main/java/tech/powerjob/client/service/impl/AppAuthClusterRequestService.java new file mode 100644 index 00000000..ddf8120d --- /dev/null +++ b/powerjob-client/src/main/java/tech/powerjob/client/service/impl/AppAuthClusterRequestService.java @@ -0,0 +1,33 @@ +package tech.powerjob.client.service.impl; + +import tech.powerjob.client.ClientConfig; +import tech.powerjob.client.module.AppAuthRequest; +import tech.powerjob.client.module.AppAuthResult; +import tech.powerjob.common.utils.DigestUtils; + +/** + * 封装鉴权相关逻辑 + * + * @author tjq + * @since 2024/2/21 + */ +abstract class AppAuthClusterRequestService extends ClusterRequestService { + + protected AppAuthResult appAuthResult; + + public AppAuthClusterRequestService(ClientConfig config) { + super(config); + } + + protected void refreshAuthInfo() { + AppAuthRequest appAuthRequest = new AppAuthRequest(); + appAuthRequest.setAppName(config.getAppName()); + appAuthRequest.setEncryptedPassword(DigestUtils.md5(config.getPassword())); + + try { + + } catch (Exception e) { + + } + } +} diff --git a/powerjob-client/src/main/java/tech/powerjob/client/service/impl/ClusterRequestService.java b/powerjob-client/src/main/java/tech/powerjob/client/service/impl/ClusterRequestService.java new file mode 100644 index 00000000..17520fb2 --- /dev/null +++ b/powerjob-client/src/main/java/tech/powerjob/client/service/impl/ClusterRequestService.java @@ -0,0 +1,123 @@ +package tech.powerjob.client.service.impl; + +import com.google.common.collect.Maps; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import tech.powerjob.client.ClientConfig; +import tech.powerjob.client.service.RequestService; +import tech.powerjob.common.OpenAPIConstant; +import tech.powerjob.common.exception.PowerJobException; +import tech.powerjob.common.serialize.JsonUtils; + +import javax.net.ssl.X509TrustManager; +import java.io.IOException; +import java.security.cert.X509Certificate; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +/** + * 集群请求服务 + * 封装网络相关通用逻辑 + * + * @author tjq + * @since 2024/2/21 + */ +@Slf4j +abstract class ClusterRequestService implements RequestService { + + protected final ClientConfig config; + + /** + * 当前地址(上次请求成功的地址) + */ + protected String currentAddress; + /** + * 鉴权相关 headers + */ + protected Map authHeaders = Maps.newHashMap(); + + /** + * 地址格式 + * 协议://域名/OpenAPI/子路径 + */ + protected static final String URL_PATTERN = "%s://%s%s%s"; + + /** + * 默认超时时间 + */ + protected static final Integer DEFAULT_TIMEOUT_SECONDS = 2; + + protected static final int HTTP_SUCCESS_CODE = 200; + + public ClusterRequestService(ClientConfig config) { + this.config = config; + } + + protected abstract String sendHttpRequest(String url, String body) throws IOException; + + @Override + public String request(String path, Object obj) { + + String body = obj instanceof String ? (String) obj : JsonUtils.toJSONStringUnsafe(obj); + + List addressList = config.getAddressList(); + // 先尝试默认地址 + String url = getUrl(path, currentAddress); + try { + String res = sendHttpRequest(url, body); + if (StringUtils.isNotEmpty(res)) { + return res; + } + } catch (IOException e) { + log.warn("[ClusterRequestService] request url:{} failed, reason is {}.", url, e.toString()); + } + + // 失败,开始重试 + for (String addr : addressList) { + if (Objects.equals(addr, currentAddress)) { + continue; + } + url = getUrl(path, addr); + try { + String res = sendHttpRequest(url, body); + if (StringUtils.isNotEmpty(res)) { + log.warn("[ClusterRequestService] server change: from({}) -> to({}).", currentAddress, addr); + currentAddress = addr; + return res; + } + } catch (IOException e) { + log.warn("[ClusterRequestService] request url:{} failed, reason is {}.", url, e.toString()); + } + } + + log.error("[ClusterRequestService] do post for path: {} failed because of no server available in {}.", path, addressList); + throw new PowerJobException("no server available when send post request"); + } + + /** + * 不验证证书 + * X.509 是一个国际标准,定义了公钥证书的格式。这个标准是由国际电信联盟(ITU-T)制定的,用于公钥基础设施(PKI)中数字证书的创建和分发。X.509证书主要用于在公开网络上验证实体的身份,如服务器或客户端的身份验证过程中,确保通信双方是可信的。X.509证书广泛应用于多种安全协议中,包括SSL/TLS,它是实现HTTPS的基础。 + */ + protected static class NoVerifyX509TrustManager implements X509TrustManager { + @Override + public void checkClientTrusted(X509Certificate[] arg0, String arg1) { + } + + @Override + public void checkServerTrusted(X509Certificate[] arg0, String arg1) { + // 不验证 + } + + @Override + public X509Certificate[] getAcceptedIssuers() { + return new X509Certificate[0]; + } + } + + + private String getUrl(String path, String address) { + String protocol = config.getProtocol().getProtocol(); + return String.format(URL_PATTERN, protocol, address, OpenAPIConstant.WEB_PATH, path); + } +} diff --git a/powerjob-client/src/main/java/tech/powerjob/client/service/impl/ClusterRequestServiceOkHttp3Impl.java b/powerjob-client/src/main/java/tech/powerjob/client/service/impl/ClusterRequestServiceOkHttp3Impl.java new file mode 100644 index 00000000..8778ce64 --- /dev/null +++ b/powerjob-client/src/main/java/tech/powerjob/client/service/impl/ClusterRequestServiceOkHttp3Impl.java @@ -0,0 +1,109 @@ +package tech.powerjob.client.service.impl; + +import com.google.common.collect.Maps; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import okhttp3.*; +import tech.powerjob.client.ClientConfig; +import tech.powerjob.client.common.Protocol; +import tech.powerjob.common.OmsConstant; +import tech.powerjob.common.exception.PowerJobException; + +import javax.net.ssl.*; +import java.io.IOException; +import java.security.SecureRandom; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.TimeUnit; + +/** + * desc + * + * @author tjq + * @since 2024/2/20 + */ +@Slf4j +public class ClusterRequestServiceOkHttp3Impl extends ClusterRequestService { + + private final OkHttpClient okHttpClient; + + + public ClusterRequestServiceOkHttp3Impl(ClientConfig config) { + super(config); + + // 初始化 HTTP 客户端 + if (Protocol.HTTPS.equals(config.getProtocol())) { + okHttpClient = initHttpsNoVerifyClient(); + } else { + okHttpClient = initHttpClient(); + } + } + + @Override + protected String sendHttpRequest(String url, String payload) throws IOException { + + // 公共 header + Map headers = Maps.newHashMap(); + if (config.getDefaultHeaders() != null) { + headers.putAll(config.getDefaultHeaders()); + } + + MediaType jsonType = MediaType.parse(OmsConstant.JSON_MEDIA_TYPE); + RequestBody requestBody = RequestBody.create(jsonType, payload); + Request request = new Request.Builder() + .post(requestBody) + .url(url) + .headers(Headers.of(headers)) + .build(); + + try (Response response = okHttpClient.newCall(request).execute()) { + int responseCode = response.code(); + if (responseCode == HTTP_SUCCESS_CODE) { + ResponseBody body = response.body(); + if (body == null) { + return null; + }else { + return body.string(); + } + } + throw new PowerJobException(String.format("http request failed,code=%d", responseCode)); + } + } + + @SneakyThrows + private OkHttpClient initHttpClient() { + OkHttpClient.Builder okHttpBuilder = commonOkHttpBuilder(); + return okHttpBuilder.build(); + } + + @SneakyThrows + private OkHttpClient initHttpsNoVerifyClient() { + + X509TrustManager trustManager = new NoVerifyX509TrustManager(); + + SSLContext sslContext = SSLContext.getInstance("TLS"); + sslContext.init(null, new TrustManager[]{trustManager}, new SecureRandom()); + SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory(); + + OkHttpClient.Builder okHttpBuilder = commonOkHttpBuilder(); + + // 不需要校验证书 + okHttpBuilder.sslSocketFactory(sslSocketFactory, trustManager); + // 不校验 url中的 hostname + okHttpBuilder.hostnameVerifier((String hostname, SSLSession session) -> true); + + + return okHttpBuilder.build(); + } + + private OkHttpClient.Builder commonOkHttpBuilder() { + return new OkHttpClient.Builder() + // 设置读取超时时间 + .readTimeout(Optional.ofNullable(config.getReadTimeout()).orElse(DEFAULT_TIMEOUT_SECONDS), TimeUnit.SECONDS) + // 设置写的超时时间 + .writeTimeout(Optional.ofNullable(config.getReadTimeout()).orElse(DEFAULT_TIMEOUT_SECONDS), TimeUnit.SECONDS) + // 设置连接超时时间 + .connectTimeout(Optional.ofNullable(config.getReadTimeout()).orElse(DEFAULT_TIMEOUT_SECONDS), TimeUnit.SECONDS); + } + +} diff --git a/powerjob-common/src/main/java/tech/powerjob/common/OpenAPIConstant.java b/powerjob-common/src/main/java/tech/powerjob/common/OpenAPIConstant.java index 2b9da25a..6be3f17c 100644 --- a/powerjob-common/src/main/java/tech/powerjob/common/OpenAPIConstant.java +++ b/powerjob-common/src/main/java/tech/powerjob/common/OpenAPIConstant.java @@ -16,6 +16,8 @@ public class OpenAPIConstant { public static final String ASSERT = "/assert"; + public static final String AUTH_APP = "/authApp"; + /* ************* JOB 区 ************* */ public static final String SAVE_JOB = "/saveJob"; @@ -56,4 +58,10 @@ public class OpenAPIConstant { public static final String RETRY_WORKFLOW_INSTANCE = "/retryWfInstance"; public static final String FETCH_WORKFLOW_INSTANCE_INFO = "/fetchWfInstanceInfo"; public static final String MARK_WORKFLOW_NODE_AS_SUCCESS = "/markWorkflowNodeAsSuccess"; + + /* ************* 鉴权 ************* */ + + public static final String HEADER_ACCESS_TOKEN = "X-POWERJOB-ACCESS-TOKEN"; + + public static final String HEADER_APP_ID = "X-POWERJOB-APP-ID"; } diff --git a/powerjob-common/src/main/java/tech/powerjob/common/response/PowerResultDTO.java b/powerjob-common/src/main/java/tech/powerjob/common/response/PowerResultDTO.java new file mode 100644 index 00000000..e7dea0b9 --- /dev/null +++ b/powerjob-common/src/main/java/tech/powerjob/common/response/PowerResultDTO.java @@ -0,0 +1,37 @@ +package tech.powerjob.common.response; + +import lombok.Getter; +import lombok.Setter; +import org.apache.commons.lang3.exception.ExceptionUtils; + +/** + * 新的 Result,带状态码 + * + * @author 程序帕鲁 + * @since 2024/2/19 + */ +@Getter +@Setter +public class PowerResultDTO extends ResultDTO { + + private String code; + + public static PowerResultDTO s(T data) { + PowerResultDTO r = new PowerResultDTO<>(); + r.success = true; + r.data = data; + return r; + } + + public static PowerResultDTO f(String message) { + PowerResultDTO r = new PowerResultDTO<>(); + r.success = false; + r.message = message; + return r; + } + + public static PowerResultDTO f(Throwable t) { + return f(ExceptionUtils.getStackTrace(t)); + } + +} diff --git a/powerjob-common/src/main/java/tech/powerjob/common/serialize/JsonUtils.java b/powerjob-common/src/main/java/tech/powerjob/common/serialize/JsonUtils.java index 2249f873..9a4d52ac 100644 --- a/powerjob-common/src/main/java/tech/powerjob/common/serialize/JsonUtils.java +++ b/powerjob-common/src/main/java/tech/powerjob/common/serialize/JsonUtils.java @@ -72,8 +72,9 @@ public class JsonUtils { try { return JSON_MAPPER.writeValueAsString(obj); }catch (Exception e) { - throw new PowerJobException(e); + ExceptionUtils.rethrow(e); } + throw new ImpossibleException(); } public static byte[] toBytes(Object obj) { diff --git a/powerjob-server/powerjob-server-common/src/main/java/tech/powerjob/server/common/utils/DigestUtils.java b/powerjob-common/src/main/java/tech/powerjob/common/utils/DigestUtils.java similarity index 86% rename from powerjob-server/powerjob-server-common/src/main/java/tech/powerjob/server/common/utils/DigestUtils.java rename to powerjob-common/src/main/java/tech/powerjob/common/utils/DigestUtils.java index 9a6bbee8..4feeb90c 100644 --- a/powerjob-server/powerjob-server-common/src/main/java/tech/powerjob/server/common/utils/DigestUtils.java +++ b/powerjob-common/src/main/java/tech/powerjob/common/utils/DigestUtils.java @@ -1,6 +1,7 @@ -package tech.powerjob.server.common.utils; +package tech.powerjob.common.utils; import lombok.SneakyThrows; +import org.apache.commons.lang3.StringUtils; import java.math.BigInteger; import java.security.MessageDigest; @@ -20,6 +21,11 @@ public class DigestUtils { */ @SneakyThrows public static String md5(String input) { + + if (StringUtils.isEmpty(input)) { + return null; + } + MessageDigest md5 = MessageDigest.getInstance("MD5"); md5.update(input.getBytes()); byte[] byteArray = md5.digest(); diff --git a/powerjob-server/pom.xml b/powerjob-server/pom.xml index 8613a597..f273a728 100644 --- a/powerjob-server/pom.xml +++ b/powerjob-server/pom.xml @@ -108,6 +108,12 @@ ${project.version} + + tech.powerjob + powerjob-client + ${project.version} + + org.mongodb diff --git a/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/common/AuthErrorCode.java b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/common/AuthErrorCode.java index ae2c90b3..29958cc4 100644 --- a/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/common/AuthErrorCode.java +++ b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/common/AuthErrorCode.java @@ -33,6 +33,8 @@ public enum AuthErrorCode { INVALID_TOKEN("-401", "INVALID_TOKEN"), + OPEN_API_AUTH_FAILED("-1001", "OPEN_API_AUTH_FAILED"), + ; private final String code; diff --git a/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/jwt/impl/DefaultSecretProvider.java b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/jwt/impl/DefaultSecretProvider.java index e5ae481f..6444f309 100644 --- a/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/jwt/impl/DefaultSecretProvider.java +++ b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/jwt/impl/DefaultSecretProvider.java @@ -5,7 +5,7 @@ import org.apache.commons.lang3.StringUtils; import org.springframework.core.env.Environment; import org.springframework.stereotype.Component; import tech.powerjob.server.auth.jwt.SecretProvider; -import tech.powerjob.server.common.utils.DigestUtils; +import tech.powerjob.common.utils.DigestUtils; import javax.annotation.Resource; diff --git a/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/login/impl/PwjbAccountLoginService.java b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/login/impl/PwjbAccountLoginService.java index 5116e38d..024d273d 100644 --- a/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/login/impl/PwjbAccountLoginService.java +++ b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/login/impl/PwjbAccountLoginService.java @@ -10,7 +10,7 @@ import tech.powerjob.server.auth.common.AuthErrorCode; import tech.powerjob.server.auth.common.PowerJobAuthException; import tech.powerjob.server.auth.login.*; import tech.powerjob.server.common.Loggers; -import tech.powerjob.server.common.utils.DigestUtils; +import tech.powerjob.common.utils.DigestUtils; import tech.powerjob.server.persistence.remote.model.PwjbUserInfoDO; import tech.powerjob.server.persistence.remote.repository.PwjbUserInfoRepository; diff --git a/powerjob-server/powerjob-server-core/src/main/java/tech/powerjob/server/core/service/AppInfoService.java b/powerjob-server/powerjob-server-core/src/main/java/tech/powerjob/server/core/service/AppInfoService.java index 4e81b048..ff2fffaf 100644 --- a/powerjob-server/powerjob-server-core/src/main/java/tech/powerjob/server/core/service/AppInfoService.java +++ b/powerjob-server/powerjob-server-core/src/main/java/tech/powerjob/server/core/service/AppInfoService.java @@ -1,5 +1,9 @@ package tech.powerjob.server.core.service; +import tech.powerjob.server.persistence.remote.model.AppInfoDO; + +import java.util.Optional; + /** * AppInfoService * @@ -7,5 +11,21 @@ package tech.powerjob.server.core.service; * @since 2023/3/4 */ public interface AppInfoService { + + /** + * 验证 APP 账号密码 + * @param appName 账号 + * @param password 原文密码 + * @return AppId + */ Long assertApp(String appName, String password); + + Long assertAppWithEncryptedPassword(String appName, String encryptedPassword); + + /** + * 获取 AppInfo(带缓存) + * @param appId appId + * @return App 信息 + */ + Optional findByIdWithCache(Long appId); } diff --git a/powerjob-server/powerjob-server-core/src/main/java/tech/powerjob/server/core/service/impl/AppInfoServiceImpl.java b/powerjob-server/powerjob-server-core/src/main/java/tech/powerjob/server/core/service/impl/AppInfoServiceImpl.java index b5e96ed0..df715075 100644 --- a/powerjob-server/powerjob-server-core/src/main/java/tech/powerjob/server/core/service/impl/AppInfoServiceImpl.java +++ b/powerjob-server/powerjob-server-core/src/main/java/tech/powerjob/server/core/service/impl/AppInfoServiceImpl.java @@ -1,13 +1,19 @@ package tech.powerjob.server.core.service.impl; +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import tech.powerjob.common.exception.PowerJobException; +import tech.powerjob.common.utils.DigestUtils; import tech.powerjob.server.core.service.AppInfoService; import tech.powerjob.server.persistence.remote.model.AppInfoDO; import tech.powerjob.server.persistence.remote.repository.AppInfoRepository; import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.TimeUnit; /** * AppInfoServiceImpl @@ -15,10 +21,17 @@ import java.util.Objects; * @author tjq * @since 2023/3/4 */ +@Slf4j @Service @RequiredArgsConstructor public class AppInfoServiceImpl implements AppInfoService { + private final Cache appId2AppInfoDO = CacheBuilder.newBuilder() + .softValues() + .expireAfterWrite(3, TimeUnit.MINUTES) + .maximumSize(1024) + .build(); + private final AppInfoRepository appInfoRepository; /** @@ -36,4 +49,31 @@ public class AppInfoServiceImpl implements AppInfoService { } throw new PowerJobException("password error!"); } + + @Override + public Long assertAppWithEncryptedPassword(String appName, String encryptedPassword) { + AppInfoDO appInfo = appInfoRepository.findByAppName(appName).orElseThrow(() -> new PowerJobException("can't find appInfo by appName: " + appName)); + if (Objects.equals(DigestUtils.md5(appInfo.getPassword()), encryptedPassword)) { + return appInfo.getId(); + } + throw new PowerJobException("password error!"); + } + + @Override + public Optional findByIdWithCache(Long appId) { + try { + AppInfoDO appInfoDO = appId2AppInfoDO.get(appId, () -> { + Optional appInfoOpt = appInfoRepository.findById(appId); + if (appInfoOpt.isPresent()) { + return appInfoOpt.get(); + } + throw new IllegalArgumentException("can't find appInfo by appId:" + appId); + }); + return Optional.of(appInfoDO); + } catch (Exception e) { + log.warn("[AppInfoService] findByIdWithCache failed,appId={}", appId, e); + } + return Optional.empty(); + } + } diff --git a/powerjob-server/powerjob-server-starter/pom.xml b/powerjob-server/powerjob-server-starter/pom.xml index 41d3dcfb..4e1ed063 100644 --- a/powerjob-server/powerjob-server-starter/pom.xml +++ b/powerjob-server/powerjob-server-starter/pom.xml @@ -51,6 +51,11 @@ tech.powerjob powerjob-server-migrate + + + tech.powerjob + powerjob-client + diff --git a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/controller/OpenAPIController.java b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/openapi/OpenAPIController.java similarity index 93% rename from powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/controller/OpenAPIController.java rename to powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/openapi/OpenAPIController.java index 33e76f7c..d3e94324 100644 --- a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/controller/OpenAPIController.java +++ b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/openapi/OpenAPIController.java @@ -1,7 +1,9 @@ -package tech.powerjob.server.web.controller; +package tech.powerjob.server.openapi; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*; +import tech.powerjob.client.module.AppAuthRequest; +import tech.powerjob.client.module.AppAuthResult; import tech.powerjob.common.OpenAPIConstant; import tech.powerjob.common.PowerQuery; import tech.powerjob.common.enums.InstanceStatus; @@ -9,16 +11,14 @@ import tech.powerjob.common.request.http.SaveJobInfoRequest; import tech.powerjob.common.request.http.SaveWorkflowNodeRequest; import tech.powerjob.common.request.http.SaveWorkflowRequest; import tech.powerjob.common.request.query.JobInfoQuery; -import tech.powerjob.common.response.InstanceInfoDTO; -import tech.powerjob.common.response.JobInfoDTO; -import tech.powerjob.common.response.ResultDTO; -import tech.powerjob.common.response.WorkflowInstanceInfoDTO; +import tech.powerjob.common.response.*; import tech.powerjob.server.core.instance.InstanceService; import tech.powerjob.server.core.service.AppInfoService; import tech.powerjob.server.core.service.CacheService; import tech.powerjob.server.core.service.JobService; import tech.powerjob.server.core.workflow.WorkflowInstanceService; import tech.powerjob.server.core.workflow.WorkflowService; +import tech.powerjob.server.openapi.security.OpenApiSecurityService; import tech.powerjob.server.persistence.remote.model.WorkflowInfoDO; import tech.powerjob.server.persistence.remote.model.WorkflowNodeInfoDO; import tech.powerjob.server.web.response.WorkflowInfoVO; @@ -46,6 +46,8 @@ public class OpenAPIController { private final WorkflowInstanceService workflowInstanceService; + private final OpenApiSecurityService openApiSecurityService; + private final CacheService cacheService; @@ -54,6 +56,16 @@ public class OpenAPIController { return ResultDTO.success(appInfoService.assertApp(appName, password)); } + /** + * APP 鉴权 + * @param appAuthRequest 鉴权请求 + * @return 鉴权响应 + */ + @PostMapping(OpenAPIConstant.AUTH_APP) + public PowerResultDTO auth(@RequestBody AppAuthRequest appAuthRequest) { + return PowerResultDTO.s(openApiSecurityService.authAppByParam(appAuthRequest)); + } + /* ************* Job 区 ************* */ @PostMapping(OpenAPIConstant.SAVE_JOB) diff --git a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/openapi/OpenApiInterceptor.java b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/openapi/OpenApiInterceptor.java new file mode 100644 index 00000000..18909450 --- /dev/null +++ b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/openapi/OpenApiInterceptor.java @@ -0,0 +1,46 @@ +package tech.powerjob.server.openapi; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.lang.NonNull; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.HandlerInterceptor; +import tech.powerjob.server.openapi.security.OpenApiSecurityService; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * OpenAPI 拦截器 + * + * @author 程序帕鲁 + * @since 2024/2/19 + */ +@Slf4j +@Component +public class OpenApiInterceptor implements HandlerInterceptor { + + @Resource + private OpenApiSecurityService openApiSecurityService; + + /** + * 4.x 及前序版本的 OpenAPI 均为携带 auth 的必要参数,直接开启鉴权功能会导致之前的服务全部报错 + * 因此提供功能开关给到使用者,若无安全影响,可展示关闭鉴权功能,等 client 升级完毕后再打开鉴权 + */ + @Value("${oms.auth.openapi.enable:false}") + private boolean enableOpenApiAuth; + + @Override + public boolean preHandle(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull Object handler) throws Exception { + + if (!enableOpenApiAuth) { + return true; + } + + openApiSecurityService.authAppByToken(request); + + return true; + } + +} diff --git a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/openapi/security/OpenApiSecurityService.java b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/openapi/security/OpenApiSecurityService.java new file mode 100644 index 00000000..ab9ee65f --- /dev/null +++ b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/openapi/security/OpenApiSecurityService.java @@ -0,0 +1,28 @@ +package tech.powerjob.server.openapi.security; + +import tech.powerjob.client.module.AppAuthRequest; +import tech.powerjob.client.module.AppAuthResult; + +import javax.servlet.http.HttpServletRequest; + +/** + * OPENAPI 安全服务 + * + * @author tjq + * @since 2024/2/19 + */ +public interface OpenApiSecurityService { + + /** + * APP 纬度请求的鉴权 & 验证 + * @param appAuthRequest 请求参数 + * @return token + */ + AppAuthResult authAppByParam(AppAuthRequest appAuthRequest); + + /** + * APP 纬度请求的鉴权 & 验证 + * @param httpServletRequest http 原始请求 + */ + void authAppByToken(HttpServletRequest httpServletRequest); +} diff --git a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/openapi/security/OpenApiSecurityServiceImpl.java b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/openapi/security/OpenApiSecurityServiceImpl.java new file mode 100644 index 00000000..bfb31e98 --- /dev/null +++ b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/openapi/security/OpenApiSecurityServiceImpl.java @@ -0,0 +1,98 @@ +package tech.powerjob.server.openapi.security; + +import com.google.common.collect.Maps; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.MapUtils; +import org.apache.commons.lang3.StringUtils; +import org.springframework.stereotype.Service; +import tech.powerjob.client.module.AppAuthRequest; +import tech.powerjob.client.module.AppAuthResult; +import tech.powerjob.common.OpenAPIConstant; +import tech.powerjob.server.auth.common.AuthErrorCode; +import tech.powerjob.server.auth.common.PowerJobAuthException; +import tech.powerjob.server.auth.common.utils.HttpServletUtils; +import tech.powerjob.server.auth.jwt.JwtService; +import tech.powerjob.common.utils.DigestUtils; +import tech.powerjob.server.core.service.AppInfoService; +import tech.powerjob.server.persistence.remote.model.AppInfoDO; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletRequest; +import java.util.Map; +import java.util.Optional; + +/** + * OpenApiSecurityService + * + * @author tjq + * @since 2024/2/19 + */ +@Slf4j +@Service +public class OpenApiSecurityServiceImpl implements OpenApiSecurityService { + + @Resource + private JwtService jwtService; + @Resource + private AppInfoService appInfoService; + + private static final String JWT_KEY_APP_ID = "appId"; + private static final String JWT_KEY_APP_PASSWORD = "password"; + + @Override + public void authAppByToken(HttpServletRequest httpServletRequest) { + + String token = HttpServletUtils.fetchFromHeader(OpenAPIConstant.HEADER_ACCESS_TOKEN, httpServletRequest); + String appIdFromHeader = HttpServletUtils.fetchFromHeader(OpenAPIConstant.HEADER_APP_ID, httpServletRequest); + + if (StringUtils.isEmpty(appIdFromHeader)) { + throw new IllegalArgumentException("can't find appId in HTTP header"); + } + + if (StringUtils.isEmpty(token)) { + throw new PowerJobAuthException(AuthErrorCode.OPEN_API_AUTH_FAILED); + } + + Map jwtResult = jwtService.parse(token, null); + + Long appIdFromJwt = MapUtils.getLong(jwtResult, JWT_KEY_APP_ID); + String passwordFromJwt = MapUtils.getString(jwtResult, JWT_KEY_APP_PASSWORD); + + // 校验 appId 一致性 + if (!StringUtils.equals(appIdFromHeader, String.valueOf(appIdFromJwt))) { + throw new IllegalArgumentException("Inconsistent appId from header and token"); + } + + // 此处不考虑改密码后的缓存时间,毕竟只要改了密码,一定会报错。换言之 OpenAPI 模式下,密码不可更改 + Optional appInfoOpt = appInfoService.findByIdWithCache(appIdFromJwt); + if (!appInfoOpt.isPresent()) { + throw new IllegalArgumentException("can't find app by appId: " + appIdFromJwt); + } + + String dbOriginPassword = appInfoOpt.get().getPassword(); + if (!StringUtils.equals(passwordFromJwt, DigestUtils.md5(dbOriginPassword))) { + throw new PowerJobAuthException(AuthErrorCode.OPEN_API_AUTH_FAILED); + } + } + + + @Override + public AppAuthResult authAppByParam(AppAuthRequest appAuthRequest) { + + String appName = appAuthRequest.getAppName(); + String encryptedPassword = appAuthRequest.getEncryptedPassword(); + + Long appId = appInfoService.assertAppWithEncryptedPassword(appName, encryptedPassword); + + Map jwtBody = Maps.newHashMap(); + jwtBody.put(JWT_KEY_APP_ID, appId); + jwtBody.put(JWT_KEY_APP_PASSWORD, encryptedPassword); + + AppAuthResult appAuthResult = new AppAuthResult(); + + appAuthResult.setAppId(appId); + appAuthResult.setToken(jwtService.build(jwtBody, null)); + + return appAuthResult; + } +} diff --git a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/ControllerExceptionHandler.java b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/ControllerExceptionHandler.java index 4903e3cd..45fc77bc 100644 --- a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/ControllerExceptionHandler.java +++ b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/ControllerExceptionHandler.java @@ -9,8 +9,7 @@ import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseBody; import tech.powerjob.common.exception.PowerJobException; -import tech.powerjob.common.response.ResultDTO; -import tech.powerjob.server.web.response.WebResultDTO; +import tech.powerjob.common.response.PowerResultDTO; /** * 统一处理 web 层异常信息 @@ -24,9 +23,9 @@ public class ControllerExceptionHandler { @ResponseBody @ExceptionHandler(Exception.class) - public WebResultDTO exceptionHandler(Exception e) { + public PowerResultDTO exceptionHandler(Exception e) { - WebResultDTO ret = new WebResultDTO<>(ResultDTO.failed(ExceptionUtils.getMessage(e))); + PowerResultDTO ret = PowerResultDTO.f(ExceptionUtils.getMessage(e)); // 不是所有异常都需要打印完整堆栈,后续可以定义内部的Exception,便于判断 if (e instanceof PowerJobException) { diff --git a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/response/WebResultDTO.java b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/response/WebResultDTO.java deleted file mode 100644 index 272c4d39..00000000 --- a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/response/WebResultDTO.java +++ /dev/null @@ -1,27 +0,0 @@ -package tech.powerjob.server.web.response; - -import lombok.Getter; -import lombok.Setter; -import tech.powerjob.common.response.ResultDTO; - -/** - * WEB 请求结果 - * - * @author tjq - * @since 2024/2/18 - */ -@Getter -@Setter -public class WebResultDTO extends ResultDTO { - - private String code; - - public WebResultDTO() { - } - - public WebResultDTO(ResultDTO res) { - this.success = res.isSuccess(); - this.data = res.getData(); - this.message = res.getMessage(); - } -} diff --git a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/service/impl/PwjbUserWebServiceImplImpl.java b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/service/impl/PwjbUserWebServiceImplImpl.java index 104783c6..0aa193b1 100644 --- a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/service/impl/PwjbUserWebServiceImplImpl.java +++ b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/service/impl/PwjbUserWebServiceImplImpl.java @@ -8,7 +8,7 @@ import tech.powerjob.common.serialize.JsonUtils; import tech.powerjob.common.utils.CommonUtils; import tech.powerjob.server.auth.common.AuthErrorCode; import tech.powerjob.server.auth.common.PowerJobAuthException; -import tech.powerjob.server.common.utils.DigestUtils; +import tech.powerjob.common.utils.DigestUtils; import tech.powerjob.server.persistence.remote.model.PwjbUserInfoDO; import tech.powerjob.server.persistence.remote.repository.PwjbUserInfoRepository; import tech.powerjob.server.web.request.ChangePasswordRequest; diff --git a/powerjob-server/powerjob-server-starter/src/main/resources/application.properties b/powerjob-server/powerjob-server-starter/src/main/resources/application.properties index 911527e7..1bb4ea0d 100644 --- a/powerjob-server/powerjob-server-starter/src/main/resources/application.properties +++ b/powerjob-server/powerjob-server-starter/src/main/resources/application.properties @@ -22,4 +22,5 @@ oms.http.port=10010 oms.table-prefix= ###### PowerJob User and Permission Configuration Configuration ###### -oms.auth.initiliaze.admin.password=powerjob_admin \ No newline at end of file +oms.auth.initiliaze.admin.password=powerjob_admin +oms.auth.openapi.enable=false \ No newline at end of file