mirror of
https://github.com/PowerJob/PowerJob.git
synced 2025-07-17 00:00:04 +08:00
feat: open-api support auth
This commit is contained in:
parent
c04cb08390
commit
84b90a366c
@ -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<String> addressList;
|
||||
|
||||
/**
|
||||
* 客户端通讯协议
|
||||
*/
|
||||
private Protocol protocol = Protocol.HTTP;
|
||||
|
||||
/**
|
||||
* 连接超时时间
|
||||
*/
|
||||
private Integer connectionTimeout;
|
||||
/**
|
||||
* 指定了等待服务器响应数据的最长时间。更具体地说,这是从服务器开始返回响应数据(包括HTTP头和数据)后,客户端读取数据的超时时间
|
||||
*/
|
||||
private Integer readTimeout;
|
||||
/**
|
||||
* 指定了向服务器发送数据的最长时间。这是从客户端开始发送数据(如POST请求的正文)到数据完全发送出去的时间
|
||||
*/
|
||||
private Integer writeTimeout;
|
||||
|
||||
/**
|
||||
* 默认携带的请求头
|
||||
* 用于流量被基础设施识别
|
||||
*/
|
||||
private Map<String, String> defaultHeaders;
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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<String, Object> extra;
|
||||
}
|
@ -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;
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
package tech.powerjob.client.service;
|
||||
|
||||
/**
|
||||
* 请求服务
|
||||
*
|
||||
* @author tjq
|
||||
* @since 2024/2/20
|
||||
*/
|
||||
public interface RequestService {
|
||||
|
||||
|
||||
String request(String path, Object body);
|
||||
}
|
@ -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) {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
@ -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<String, String> 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<String> 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);
|
||||
}
|
||||
}
|
@ -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<String, String> 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);
|
||||
}
|
||||
|
||||
}
|
@ -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";
|
||||
}
|
||||
|
@ -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<T> extends ResultDTO<T> {
|
||||
|
||||
private String code;
|
||||
|
||||
public static <T> PowerResultDTO<T> s(T data) {
|
||||
PowerResultDTO<T> r = new PowerResultDTO<>();
|
||||
r.success = true;
|
||||
r.data = data;
|
||||
return r;
|
||||
}
|
||||
|
||||
public static <T> PowerResultDTO<T> f(String message) {
|
||||
PowerResultDTO<T> r = new PowerResultDTO<>();
|
||||
r.success = false;
|
||||
r.message = message;
|
||||
return r;
|
||||
}
|
||||
|
||||
public static <T> PowerResultDTO<T> f(Throwable t) {
|
||||
return f(ExceptionUtils.getStackTrace(t));
|
||||
}
|
||||
|
||||
}
|
@ -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) {
|
||||
|
@ -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();
|
@ -108,6 +108,12 @@
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>tech.powerjob</groupId>
|
||||
<artifactId>powerjob-client</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 存储扩展-MongoDB,未使用可移除 -->
|
||||
<dependency>
|
||||
<groupId>org.mongodb</groupId>
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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<AppInfoDO> findByIdWithCache(Long appId);
|
||||
}
|
||||
|
@ -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<Long, AppInfoDO> 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<AppInfoDO> findByIdWithCache(Long appId) {
|
||||
try {
|
||||
AppInfoDO appInfoDO = appId2AppInfoDO.get(appId, () -> {
|
||||
Optional<AppInfoDO> 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();
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -51,6 +51,11 @@
|
||||
<groupId>tech.powerjob</groupId>
|
||||
<artifactId>powerjob-server-migrate</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>tech.powerjob</groupId>
|
||||
<artifactId>powerjob-client</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<!-- SpringBoot maven plugin -->
|
||||
|
@ -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<AppAuthResult> auth(@RequestBody AppAuthRequest appAuthRequest) {
|
||||
return PowerResultDTO.s(openApiSecurityService.authAppByParam(appAuthRequest));
|
||||
}
|
||||
|
||||
/* ************* Job 区 ************* */
|
||||
|
||||
@PostMapping(OpenAPIConstant.SAVE_JOB)
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
@ -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<String, Object> 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<AppInfoDO> 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<String, Object> 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;
|
||||
}
|
||||
}
|
@ -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<Void> exceptionHandler(Exception e) {
|
||||
public PowerResultDTO<Void> exceptionHandler(Exception e) {
|
||||
|
||||
WebResultDTO<Void> ret = new WebResultDTO<>(ResultDTO.failed(ExceptionUtils.getMessage(e)));
|
||||
PowerResultDTO<Void> ret = PowerResultDTO.f(ExceptionUtils.getMessage(e));
|
||||
|
||||
// 不是所有异常都需要打印完整堆栈,后续可以定义内部的Exception,便于判断
|
||||
if (e instanceof PowerJobException) {
|
||||
|
@ -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<T> extends ResultDTO<T> {
|
||||
|
||||
private String code;
|
||||
|
||||
public WebResultDTO() {
|
||||
}
|
||||
|
||||
public WebResultDTO(ResultDTO<T> res) {
|
||||
this.success = res.isSuccess();
|
||||
this.data = res.getData();
|
||||
this.message = res.getMessage();
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -23,3 +23,4 @@ oms.table-prefix=
|
||||
|
||||
###### PowerJob User and Permission Configuration Configuration ######
|
||||
oms.auth.initiliaze.admin.password=powerjob_admin
|
||||
oms.auth.openapi.enable=false
|
Loading…
x
Reference in New Issue
Block a user