diff --git a/powerjob-server/pom.xml b/powerjob-server/pom.xml
index 5eaec38d..8bb0ed3b 100644
--- a/powerjob-server/pom.xml
+++ b/powerjob-server/pom.xml
@@ -22,6 +22,7 @@
powerjob-server-migrate
powerjob-server-core
powerjob-server-monitor
+ powerjob-server-auth
diff --git a/powerjob-server/powerjob-server-auth/pom.xml b/powerjob-server/powerjob-server-auth/pom.xml
new file mode 100644
index 00000000..79746431
--- /dev/null
+++ b/powerjob-server/powerjob-server-auth/pom.xml
@@ -0,0 +1,58 @@
+
+
+
+
+ tech.powerjob
+ powerjob-server
+ 4.3.7
+
+
+ 4.0.0
+
+ powerjob-server-auth
+ ${project.parent.version}
+
+
+ 8
+ 8
+ UTF-8
+ 0.11.5
+ 1.1.86
+
+
+
+
+ tech.powerjob
+ powerjob-server-persistence
+ provided
+
+
+ io.jsonwebtoken
+ jjwt-api
+ ${jjwt.version}
+
+
+ io.jsonwebtoken
+ jjwt-impl
+ ${jjwt.version}
+ runtime
+
+
+ io.jsonwebtoken
+ jjwt-jackson
+ ${jjwt.version}
+ runtime
+
+
+
+ com.aliyun
+ dingtalk
+ ${dingtalk.version}
+
+
+
+
+
+
\ No newline at end of file
diff --git a/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/Permission.java b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/Permission.java
new file mode 100644
index 00000000..e6fabb4a
--- /dev/null
+++ b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/Permission.java
@@ -0,0 +1,40 @@
+package tech.powerjob.server.auth;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * 权限
+ *
+ * @author tjq
+ * @since 2023/3/20
+ */
+@Getter
+@AllArgsConstructor
+public enum Permission {
+
+ /**
+ * 不需要权限
+ */
+ NONE(1),
+ /**
+ * 读权限,查看控制台数据
+ */
+ READ(10),
+ /**
+ * 写权限,新增/修改任务等
+ */
+ WRITE(20),
+ /**
+ * 运维权限,比如任务的执行
+ */
+ OPS(30),
+ /**
+ * 超级权限
+ */
+ SU(100)
+ ;
+
+
+ private int v;
+}
diff --git a/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/Role.java b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/Role.java
new file mode 100644
index 00000000..9b0280ab
--- /dev/null
+++ b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/Role.java
@@ -0,0 +1,53 @@
+package tech.powerjob.server.auth;
+
+import com.google.common.collect.Sets;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+import java.util.Set;
+
+import static tech.powerjob.server.auth.Permission.*;
+
+/**
+ * 角色
+ * PowerJob 采用 RBAC 实现权限,出于实际需求的考虑,不决定采用动态权限模型。因此 RBAC 中的角色和权限均在此处定义。
+ * 如果有自定义诉求,可以修改 Role 的定义
+ *
+ * @author tjq
+ * @since 2023/3/20
+ */
+@Getter
+@AllArgsConstructor
+public enum Role {
+
+ /**
+ * 观察者,默认只读权限
+ */
+ OBSERVER(10, Sets.newHashSet(READ)),
+ /**
+ * 技术质量,读 + 操作权限
+ */
+ QA(20, Sets.newHashSet(READ, OPS)),
+ /**
+ * 开发者,读 + 编辑 + 操作权限
+ */
+ DEVELOPER(30, Sets.newHashSet(READ, WRITE, OPS)),
+ /**
+ * 管理员
+ */
+ ADMIN(40, Sets.newHashSet(READ, WRITE, OPS, SU))
+ ;
+
+ private final int v;
+
+ private final Set permissions;
+
+ public static Role of(int vv) {
+ for (Role role : values()) {
+ if (vv == role.v) {
+ return role;
+ }
+ }
+ throw new IllegalArgumentException("unknown role: " + vv);
+ }
+}
diff --git a/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/RoleScope.java b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/RoleScope.java
new file mode 100644
index 00000000..328d9acf
--- /dev/null
+++ b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/RoleScope.java
@@ -0,0 +1,31 @@
+package tech.powerjob.server.auth;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * 权限范围
+ *
+ * @author tjq
+ * @since 2023/9/3
+ */
+@Getter
+@AllArgsConstructor
+public enum RoleScope {
+
+ /**
+ * NAMESPACE 权限
+ */
+ NAMESPACE(1),
+ /**
+ * APP 级别权限
+ */
+ APP(10),
+ /**
+ * 全局权限
+ */
+ GLOBAL(666)
+ ;
+
+ private final int v;
+}
diff --git a/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/login/LoginContext.java b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/login/LoginContext.java
new file mode 100644
index 00000000..16839dd5
--- /dev/null
+++ b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/login/LoginContext.java
@@ -0,0 +1,25 @@
+package tech.powerjob.server.auth.login;
+
+import lombok.Data;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * 登录上下文
+ *
+ * @author tjq
+ * @since 2024/2/10
+ */
+@Data
+public class LoginContext {
+
+ /**
+ * 原始参数,给第三方登录方式一个服务端和前端交互的数据通道。PowerJob 本身不感知其中的内容
+ */
+ private String originParams;
+
+ private transient HttpServletRequest httpServletRequest;
+
+ private transient HttpServletResponse httpServletResponse;
+}
diff --git a/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/login/LoginTypeInfo.java b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/login/LoginTypeInfo.java
new file mode 100644
index 00000000..0ecf38a1
--- /dev/null
+++ b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/login/LoginTypeInfo.java
@@ -0,0 +1,30 @@
+package tech.powerjob.server.auth.login;
+
+import lombok.Data;
+import lombok.experimental.Accessors;
+
+import java.io.Serializable;
+
+/**
+ * 登录类型描述
+ *
+ * @author tjq
+ * @since 2024/2/10
+ */
+@Data
+@Accessors(chain = true)
+public class LoginTypeInfo implements Serializable {
+
+ /**
+ * 登录类型,唯一标识
+ */
+ private String type;
+ /**
+ * 描述名称,前端展示用
+ */
+ private String name;
+ /**
+ * 展示用的 ICON
+ */
+ private String iconUrl;
+}
diff --git a/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/login/ThirdPartyLoginService.java b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/login/ThirdPartyLoginService.java
new file mode 100644
index 00000000..893eb851
--- /dev/null
+++ b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/login/ThirdPartyLoginService.java
@@ -0,0 +1,31 @@
+package tech.powerjob.server.auth.login;
+
+/**
+ * 第三方登录服务
+ *
+ * @author tjq
+ * @since 2024/2/10
+ */
+public interface ThirdPartyLoginService {
+
+ /**
+ * 登陆服务的类型
+ * @return 登陆服务类型,比如 PowerJob / DingTalk
+ */
+ LoginTypeInfo loginType();
+
+ /**
+ * 生成登陆的重定向 URL
+ * @param loginContext 上下文
+ * @return 重定向地址
+ */
+ String generateLoginUrl(LoginContext loginContext);
+
+ /**
+ * 执行第三方登录
+ * @param loginContext 上下文
+ * @return 登录地址
+ */
+ ThirdPartyUser login(LoginContext loginContext);
+
+}
diff --git a/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/login/ThirdPartyUser.java b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/login/ThirdPartyUser.java
new file mode 100644
index 00000000..e5d2e739
--- /dev/null
+++ b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/login/ThirdPartyUser.java
@@ -0,0 +1,38 @@
+package tech.powerjob.server.auth.login;
+
+import lombok.Data;
+import lombok.experimental.Accessors;
+
+/**
+ * 第三方用户
+ *
+ * @author tjq
+ * @since 2024/2/10
+ */
+@Data
+@Accessors(chain = true)
+public class ThirdPartyUser {
+
+ /**
+ * 用户的唯一标识,用于关联到 PowerJob 的 username
+ */
+ private String username;
+
+ /* ******** 以下全部选填即可,只是方便数据同步,后续都可以去 PowerJob 控制台更改 ******** */
+ /**
+ * 用户昵称
+ */
+ private String nick;
+ /**
+ * 手机号
+ */
+ private String phone;
+ /**
+ * 邮箱地址
+ */
+ private String email;
+ /**
+ * 扩展字段
+ */
+ private String extra;
+}
diff --git a/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/login/impl/DingTalkLoginService.java b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/login/impl/DingTalkLoginService.java
new file mode 100644
index 00000000..d3638917
--- /dev/null
+++ b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/login/impl/DingTalkLoginService.java
@@ -0,0 +1,143 @@
+package tech.powerjob.server.auth.login.impl;
+
+import com.aliyun.dingtalkcontact_1_0.models.GetUserHeaders;
+import com.aliyun.dingtalkcontact_1_0.models.GetUserResponseBody;
+import com.aliyun.dingtalkoauth2_1_0.models.GetUserTokenRequest;
+import com.aliyun.dingtalkoauth2_1_0.models.GetUserTokenResponse;
+import com.aliyun.teaopenapi.models.Config;
+import com.aliyun.teautil.models.RuntimeOptions;
+import lombok.SneakyThrows;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.factory.annotation.Value;
+import tech.powerjob.common.exception.PowerJobException;
+import tech.powerjob.server.auth.login.LoginContext;
+import tech.powerjob.server.auth.login.LoginTypeInfo;
+import tech.powerjob.server.auth.login.ThirdPartyLoginService;
+import tech.powerjob.server.auth.login.ThirdPartyUser;
+import tech.powerjob.server.common.Loggers;
+
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+
+/**
+ * 钉钉账号体系登录第三方网站
+ * PowerJob 官方支持钉钉账号体系登录原因:
+ * 1. 钉钉作为当下用户体量最大的企业级办公软件,覆盖率足够高,提供钉钉支持能让更多开发者开箱即用
+ * 2. 钉钉的 API 设计和 PowerJob 设想一致,算是个最佳实践,其他企业内部的账号体系可参考这套流程进行接入
+ * - PowerJob 重定向到第三方账号体系登陆页 -> 第三方完成登陆 -> 跳转回调 PowerJob auth 接口 -> PowerJob 解析回调登陆信息,完整用户关联
+ *
+ * @author tjq
+ * @since 2023/3/26
+ */
+public class DingTalkLoginService implements ThirdPartyLoginService {
+
+ /*
+ 配置示例
+ oms.auth.dingtalk.appkey=dinggzqqzqqzqqzqq
+ oms.auth.dingtalk.appSecret=iY-FS8mzqqzqq_xEizqqzqqzqqzqqzqqzqqYEbkZOal
+ oms.auth.dingtalk.callbackUrl=http://localhost:7700/auth/loginCallback
+ */
+
+ /**
+ * 钉钉应用 AppKey
+ */
+ @Value("${oms.auth.dingtalk.appkey:#{null}}")
+ private String dingTalkAppKey;
+ /**
+ * 钉钉应用 AppSecret
+ */
+ @Value("${oms.auth.dingtalk.appSecret:#{null}}")
+ private String dingTalkAppSecret;
+ /**
+ * 回调地址,powerjob-server 地址 + /user/auth
+ * 比如本地调试时为 LocalDemoCallbackUrl
+ * 部署后则为 demoCallBackUrl
+ */
+ @Value("${oms.auth.dingtalk.callbackUrl:#{null}}")
+ private String dingTalkCallbackUrl;
+
+ private static final String DING_TALK = "DingTalk";
+
+ @Override
+ public LoginTypeInfo loginType() {
+ return new LoginTypeInfo()
+ .setType(DING_TALK)
+ .setName("钉钉登录")
+ ;
+ }
+
+ @Override
+ @SneakyThrows
+ public String generateLoginUrl(LoginContext loginContext) {
+ if (StringUtils.isAnyEmpty(dingTalkAppKey, dingTalkAppSecret, dingTalkCallbackUrl)) {
+ throw new IllegalArgumentException("please config 'oms.auth.dingtalk.appkey', 'oms.auth.dingtalk.appSecret' and 'oms.auth.dingtalk.callbackUrl' in properties!");
+ }
+
+ String urlString = URLEncoder.encode(dingTalkCallbackUrl, StandardCharsets.UTF_8.name());
+ String url = "https://login.dingtalk.com/oauth2/auth?" +
+ "redirect_uri=" + urlString +
+ "&response_type=code" +
+ "&client_id=" + dingTalkAppKey +
+ "&scope=openid" +
+ "&state=" + DING_TALK +
+ "&prompt=consent";
+ Loggers.WEB.info("[DingTalkBizLoginService] login url: {}", url);
+ return url;
+ }
+
+ @Override
+ @SneakyThrows
+ public ThirdPartyUser login(LoginContext loginContext) {
+ try {
+ com.aliyun.dingtalkoauth2_1_0.Client client = authClient();
+ GetUserTokenRequest getUserTokenRequest = new GetUserTokenRequest()
+ //应用基础信息-应用信息的AppKey,请务必替换为开发的应用AppKey
+ .setClientId(dingTalkAppKey)
+ //应用基础信息-应用信息的AppSecret,,请务必替换为开发的应用AppSecret
+ .setClientSecret(dingTalkAppSecret)
+ .setCode(loginContext.getHttpServletRequest().getParameter("authCode"))
+ .setGrantType("authorization_code");
+ GetUserTokenResponse getUserTokenResponse = client.getUserToken(getUserTokenRequest);
+ //获取用户个人 token
+ String accessToken = getUserTokenResponse.getBody().getAccessToken();
+ // 查询钉钉用户
+ final GetUserResponseBody dingUser = getUserinfo(accessToken);
+ // 将钉钉用户的唯一ID 和 PowerJob 账户体系的唯一键 username 关联
+ if (dingUser != null) {
+ ThirdPartyUser bizUser = new ThirdPartyUser();
+ bizUser.setUsername(dingUser.getUnionId());
+ bizUser.setNick(dingUser.getNick());
+ bizUser.setPhone(dingUser.getMobile());
+ bizUser.setEmail(dingUser.getEmail());
+ return bizUser;
+ }
+ } catch (Exception e) {
+ Loggers.WEB.error("[DingTalkBizLoginService] login by dingTalk failed!", e);
+ throw e;
+ }
+ throw new PowerJobException("login from dingTalk failed!");
+ }
+
+ /* 以下代码均拷自钉钉官网示例 */
+
+ private static com.aliyun.dingtalkoauth2_1_0.Client authClient() throws Exception {
+ Config config = new Config();
+ config.protocol = "https";
+ config.regionId = "central";
+ return new com.aliyun.dingtalkoauth2_1_0.Client(config);
+ }
+ private static com.aliyun.dingtalkcontact_1_0.Client contactClient() throws Exception {
+ Config config = new Config();
+ config.protocol = "https";
+ config.regionId = "central";
+ return new com.aliyun.dingtalkcontact_1_0.Client(config);
+ }
+
+ private GetUserResponseBody getUserinfo(String accessToken) throws Exception {
+ com.aliyun.dingtalkcontact_1_0.Client client = contactClient();
+ GetUserHeaders getUserHeaders = new GetUserHeaders();
+ getUserHeaders.xAcsDingtalkAccessToken = accessToken;
+ //获取用户个人信息,如需获取当前授权人的信息,unionId参数必须传me
+ return client.getUserWithOptions("me", getUserHeaders, new RuntimeOptions()).getBody();
+ }
+}
diff --git a/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/login/impl/PowerJobLoginService.java b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/login/impl/PowerJobLoginService.java
new file mode 100644
index 00000000..0b37a608
--- /dev/null
+++ b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/login/impl/PowerJobLoginService.java
@@ -0,0 +1,85 @@
+package tech.powerjob.server.auth.login.impl;
+
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.stereotype.Service;
+import tech.powerjob.common.exception.PowerJobException;
+import tech.powerjob.server.auth.login.LoginContext;
+import tech.powerjob.server.auth.login.LoginTypeInfo;
+import tech.powerjob.server.auth.login.ThirdPartyLoginService;
+import tech.powerjob.server.auth.login.ThirdPartyUser;
+import tech.powerjob.server.common.Loggers;
+import tech.powerjob.server.common.SJ;
+import tech.powerjob.server.common.utils.DigestUtils;
+import tech.powerjob.server.persistence.remote.model.UserInfoDO;
+import tech.powerjob.server.persistence.remote.repository.UserInfoRepository;
+
+import javax.annotation.Resource;
+import java.util.Map;
+import java.util.Optional;
+
+/**
+ * PowerJob 自带的登陆服务
+ * 和应用主框架无关,依然属于第三方登录体系
+ *
+ * @author tjq
+ * @since 2023/3/20
+ */
+@Service
+public class PowerJobLoginService implements ThirdPartyLoginService {
+
+ @Resource
+ private UserInfoRepository userInfoRepository;
+
+ private static final String POWER_JOB_LOGIN_SERVICE = "PowerJob";
+
+ private static final String KEY_USERNAME = "username";
+ private static final String KEY_PASSWORD = "password";
+
+ @Override
+ public LoginTypeInfo loginType() {
+ return new LoginTypeInfo()
+ .setType(POWER_JOB_LOGIN_SERVICE)
+ .setName("PowerJob's built-in login system")
+ ;
+ }
+
+ @Override
+ public String generateLoginUrl(LoginContext loginContext) {
+ // 前端实现跳转,服务端返回特殊指令
+ return "FE-REDIRECT:PowerJob";
+ }
+
+ @Override
+ public ThirdPartyUser login(LoginContext loginContext) {
+ final String loginInfo = loginContext.getOriginParams();
+ if (StringUtils.isEmpty(loginInfo)) {
+ throw new IllegalArgumentException("can't find login Info");
+ }
+
+ final Map loginInfoMap = SJ.splitKvString(loginInfo);
+ final String username = loginInfoMap.get(KEY_USERNAME);
+ final String password = loginInfoMap.get(KEY_PASSWORD);
+
+ if (StringUtils.isAnyEmpty(username, password)) {
+ Loggers.WEB.debug("[PowerJobLoginService] username or password is empty, login failed!");
+ throw new IllegalArgumentException("username or password is empty!");
+ }
+
+ final Optional userInfoOpt = userInfoRepository.findByUsername(username);
+ if (!userInfoOpt.isPresent()) {
+ Loggers.WEB.debug("[PowerJobLoginService] can't find user by username: {}", username);
+ throw new PowerJobException("can't find user by username: " + username);
+ }
+
+ final UserInfoDO dbUser = userInfoOpt.get();
+
+ if (DigestUtils.rePassword(password, username).equals(dbUser.getPassword())) {
+ ThirdPartyUser bizUser = new ThirdPartyUser();
+ bizUser.setUsername(username);
+ return bizUser;
+ }
+
+ Loggers.WEB.debug("[PowerJobLoginService] user[{}]'s password is incorrect, login failed!", username);
+ throw new PowerJobException("password is incorrect");
+ }
+}
diff --git a/powerjob-server/powerjob-server-common/src/main/java/tech/powerjob/server/common/Loggers.java b/powerjob-server/powerjob-server-common/src/main/java/tech/powerjob/server/common/Loggers.java
new file mode 100644
index 00000000..20b0e58c
--- /dev/null
+++ b/powerjob-server/powerjob-server-common/src/main/java/tech/powerjob/server/common/Loggers.java
@@ -0,0 +1,18 @@
+package tech.powerjob.server.common;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * 统一定义日志
+ *
+ * @author tjq
+ * @since 2023/3/25
+ */
+public class Loggers {
+
+ /**
+ * Web 层统一日志
+ */
+ public static final Logger WEB = LoggerFactory.getLogger("P_SERVER_LOGGER_WEB");
+}
diff --git a/powerjob-server/powerjob-server-common/src/main/java/tech/powerjob/server/common/SJ.java b/powerjob-server/powerjob-server-common/src/main/java/tech/powerjob/server/common/SJ.java
index 89a89730..eb2051a8 100644
--- a/powerjob-server/powerjob-server-common/src/main/java/tech/powerjob/server/common/SJ.java
+++ b/powerjob-server/powerjob-server-common/src/main/java/tech/powerjob/server/common/SJ.java
@@ -3,6 +3,8 @@ package tech.powerjob.server.common;
import com.google.common.base.Joiner;
import com.google.common.base.Splitter;
+import java.util.Map;
+
/**
* Splitter & Joiner
*
@@ -16,4 +18,9 @@ public class SJ {
public static final Joiner MONITOR_JOINER = Joiner.on("|").useForNull("-");
+ private static final Splitter.MapSplitter MAP_SPLITTER = Splitter.onPattern(";").withKeyValueSeparator(":");
+
+ public static Map splitKvString(String kvString) {
+ return MAP_SPLITTER.split(kvString);
+ }
}
diff --git a/powerjob-server/powerjob-server-common/src/main/java/tech/powerjob/server/common/utils/DigestUtils.java b/powerjob-server/powerjob-server-common/src/main/java/tech/powerjob/server/common/utils/DigestUtils.java
new file mode 100644
index 00000000..9a6bbee8
--- /dev/null
+++ b/powerjob-server/powerjob-server-common/src/main/java/tech/powerjob/server/common/utils/DigestUtils.java
@@ -0,0 +1,41 @@
+package tech.powerjob.server.common.utils;
+
+import lombok.SneakyThrows;
+
+import java.math.BigInteger;
+import java.security.MessageDigest;
+
+/**
+ * 加密工具
+ *
+ * @author tjq
+ * @since 2023/3/25
+ */
+public class DigestUtils {
+
+ /**
+ * 32位小写 md5
+ * @param input 输入
+ * @return md5
+ */
+ @SneakyThrows
+ public static String md5(String input) {
+ MessageDigest md5 = MessageDigest.getInstance("MD5");
+ md5.update(input.getBytes());
+ byte[] byteArray = md5.digest();
+
+ BigInteger bigInt = new BigInteger(1, byteArray);
+ // 参数16表示16进制
+ StringBuilder result = new StringBuilder(bigInt.toString(16));
+ // 不足32位高位补零
+ while(result.length() < 32) {
+ result.insert(0, "0");
+ }
+ return result.toString();
+ }
+
+ public static String rePassword(String password, String salt) {
+ String f1 = String.format("%s_%s_z", salt, password);
+ return String.format("%s_%s_b", salt, md5(f1));
+ }
+}
diff --git a/powerjob-server/powerjob-server-persistence/src/main/java/tech/powerjob/server/persistence/remote/model/UserInfoDO.java b/powerjob-server/powerjob-server-persistence/src/main/java/tech/powerjob/server/persistence/remote/model/UserInfoDO.java
index e19bef8c..e13e5724 100644
--- a/powerjob-server/powerjob-server-persistence/src/main/java/tech/powerjob/server/persistence/remote/model/UserInfoDO.java
+++ b/powerjob-server/powerjob-server-persistence/src/main/java/tech/powerjob/server/persistence/remote/model/UserInfoDO.java
@@ -26,6 +26,11 @@ public class UserInfoDO {
private Long id;
private String username;
+ /**
+ * since 5.0.0
+ * 昵称(第三方登陆的 username 很难识别,方便后续展示引入 nick)
+ */
+ private String nick;
private String password;
/**
diff --git a/powerjob-server/powerjob-server-persistence/src/main/java/tech/powerjob/server/persistence/remote/model/UserRoleDO.java b/powerjob-server/powerjob-server-persistence/src/main/java/tech/powerjob/server/persistence/remote/model/UserRoleDO.java
new file mode 100644
index 00000000..22d7b629
--- /dev/null
+++ b/powerjob-server/powerjob-server-persistence/src/main/java/tech/powerjob/server/persistence/remote/model/UserRoleDO.java
@@ -0,0 +1,53 @@
+package tech.powerjob.server.persistence.remote.model;
+
+import lombok.Data;
+import org.hibernate.annotations.GenericGenerator;
+
+import javax.persistence.*;
+import java.util.Date;
+
+/**
+ * 用户角色表
+ *
+ * @author tjq
+ * @since 2023/3/20
+ */
+@Data
+@Entity
+@Table(indexes = {
+ @Index(name = "uidx01_user_id", columnList = "userId")
+})
+public class UserRoleDO {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.AUTO, generator = "native")
+ @GenericGenerator(name = "native", strategy = "native")
+ private Long id;
+
+ /**
+ * 授予角色的用户ID
+ */
+ private Long userId;
+
+ /**
+ * 权限范围,namespace 还是 app
+ */
+ private Integer scope;
+ /**
+ * 和 scope 一起组成授权目标,比如某个 app 或 某个 namespace
+ */
+ private Long target;
+
+ /**
+ * 角色,比如 Observer
+ */
+ private Integer role;
+ /**
+ * 扩展字段
+ */
+ private String extra;
+
+ private Date gmtCreate;
+
+ private Date gmtModified;
+}
diff --git a/powerjob-server/powerjob-server-persistence/src/main/java/tech/powerjob/server/persistence/remote/repository/UserInfoRepository.java b/powerjob-server/powerjob-server-persistence/src/main/java/tech/powerjob/server/persistence/remote/repository/UserInfoRepository.java
index 2a11d207..3e70cef4 100644
--- a/powerjob-server/powerjob-server-persistence/src/main/java/tech/powerjob/server/persistence/remote/repository/UserInfoRepository.java
+++ b/powerjob-server/powerjob-server-persistence/src/main/java/tech/powerjob/server/persistence/remote/repository/UserInfoRepository.java
@@ -4,6 +4,7 @@ import tech.powerjob.server.persistence.remote.model.UserInfoDO;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
+import java.util.Optional;
/**
* 用户信息表数据库访问层
@@ -13,6 +14,8 @@ import java.util.List;
*/
public interface UserInfoRepository extends JpaRepository {
+ Optional findByUsername(String username);
+
List findByUsernameLike(String username);
List findByIdIn(List userIds);
diff --git a/powerjob-server/powerjob-server-persistence/src/main/java/tech/powerjob/server/persistence/remote/repository/UserRoleRepository.java b/powerjob-server/powerjob-server-persistence/src/main/java/tech/powerjob/server/persistence/remote/repository/UserRoleRepository.java
new file mode 100644
index 00000000..18949f08
--- /dev/null
+++ b/powerjob-server/powerjob-server-persistence/src/main/java/tech/powerjob/server/persistence/remote/repository/UserRoleRepository.java
@@ -0,0 +1,19 @@
+package tech.powerjob.server.persistence.remote.repository;
+
+import org.springframework.data.jpa.repository.JpaRepository;
+import tech.powerjob.server.persistence.remote.model.UserRoleDO;
+
+import java.util.List;
+
+/**
+ * UserRoleRepository
+ *
+ * @author tjq
+ * @since 2023/3/20
+ */
+public interface UserRoleRepository extends JpaRepository {
+
+ List findAllByUserId(Long userId);
+
+ List findAllByScopeAndTarget(Integer scope, Long target);
+}