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 52f955fc..b706186a 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 @@ -1,5 +1,6 @@ package tech.powerjob.common.serialize; +import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; @@ -32,6 +33,10 @@ public class JsonUtils { .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) .build(); + static { + JSON_MAPPER.setSerializationInclusion(JsonInclude.Include.NON_NULL); + } + private static final TypeReference> MAP_TYPE_REFERENCE = new TypeReference> () {}; private JsonUtils(){ @@ -39,6 +44,9 @@ public class JsonUtils { } public static String toJSONString(Object obj) { + if (obj == null) { + return null; + } if (obj instanceof String) { return (String) obj; } diff --git a/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/common/AuthConstants.java b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/common/AuthConstants.java index 770b8fc2..e4983c9d 100644 --- a/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/common/AuthConstants.java +++ b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/common/AuthConstants.java @@ -26,6 +26,13 @@ public class AuthConstants { */ public static final String ACCOUNT_LARK = "LARK"; + public static final String PARAM_KEY_USERNAME = "username"; + public static final String PARAM_KEY_PASSWORD = "password"; + /** + * 前端参数-密码加密类型,官方版本出于成本未进行前后端传输的对称加密,接入方有需求可自行实现,此处定义加密协议字段 + */ + public static final String PARAM_KEY_ENCRYPTION = "encryption"; + /* ********** 账号体系 ********** */ /** @@ -40,4 +47,6 @@ public class AuthConstants { public static final String FE_REDIRECT_KEY = "FE-REDIRECT:"; public static final String TIPS_NO_PERMISSION_TO_SEE = "NO_PERMISSION_TO_SEE"; + + public static final Long GLOBAL_ADMIN_TARGET_ID = 1L; } 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 be64f578..8d51f25d 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 @@ -15,6 +15,7 @@ public enum AuthErrorCode { USER_NOT_LOGIN("-100", "UserNotLoggedIn"), USER_NOT_EXIST("-101", "UserNotExist"), + USER_AUTH_FAILED("-102", "UserAuthFailed"), NO_PERMISSION("-200", "NoPermission"), @@ -24,7 +25,9 @@ public enum AuthErrorCode { */ INVALID_REQUEST("-300", "INVALID_REQUEST"), - INCORRECT_PASSWORD("-400", "INCORRECT_PASSWORD") + INCORRECT_PASSWORD("-400", "INCORRECT_PASSWORD"), + + INVALID_TOKEN("-401", "INVALID_TOKEN"), ; 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 index edbed635..16dd819b 100644 --- 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 @@ -30,4 +30,13 @@ public interface ThirdPartyLoginService { */ ThirdPartyUser login(ThirdPartyLoginRequest loginRequest); + /** + * JWT 登录的回调校验 + * @param username 用户名称 + * @param tokenLoginVerifyInfo 二次校验信息 + * @return 是否通过 + */ + default boolean tokenLoginVerify(String username, TokenLoginVerifyInfo tokenLoginVerifyInfo) { + return true; + } } 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 index bf2bca28..f34604a1 100644 --- 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 @@ -17,6 +17,11 @@ public class ThirdPartyUser { * 用户的唯一标识,用于关联到 PowerJob 的 username */ private String username; + /** + * JWT 登录的二次校验配置 + * 可空,空则代表放弃二次校验(会出现第三方登录改了密码当 PowerJob JWT 登录依然可用的情况) + */ + private TokenLoginVerifyInfo tokenLoginVerifyInfo; /* ******** 以下全部选填即可,只是方便数据同步,后续都可以去 PowerJob 控制台更改 ******** */ /** diff --git a/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/login/TokenLoginVerifyInfo.java b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/login/TokenLoginVerifyInfo.java new file mode 100644 index 00000000..e60f0077 --- /dev/null +++ b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/login/TokenLoginVerifyInfo.java @@ -0,0 +1,31 @@ +package tech.powerjob.server.auth.login; + +import lombok.Data; + +import java.io.Serializable; + +/** + * JWT 登录时的校验信息 + * + * @author tjq + * @since 2024/2/16 + */ +@Data +public class TokenLoginVerifyInfo implements Serializable { + + /** + * 加密 token 部分,比如密码的 md5,会直接写入 JWT 下发给前端 + * 如果需要使用 JWT 二次校验,则该参数必须存在 + */ + private String encryptedToken; + + /** + * 补充信息,用于二次校验 + */ + private String additionalInfo; + + /** + * 依然是预留字段,第三方实现自用即可 + */ + private String extra; +} diff --git a/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/login/impl/PowerJobThirdPartyLoginService.java b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/login/impl/PowerJobThirdPartyLoginService.java index 738820ed..fbf9b3d4 100644 --- a/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/login/impl/PowerJobThirdPartyLoginService.java +++ b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/login/impl/PowerJobThirdPartyLoginService.java @@ -8,10 +8,7 @@ import tech.powerjob.common.serialize.JsonUtils; import tech.powerjob.server.auth.common.AuthConstants; import tech.powerjob.server.auth.common.AuthErrorCode; import tech.powerjob.server.auth.common.PowerJobAuthException; -import tech.powerjob.server.auth.login.LoginTypeInfo; -import tech.powerjob.server.auth.login.ThirdPartyLoginRequest; -import tech.powerjob.server.auth.login.ThirdPartyLoginService; -import tech.powerjob.server.auth.login.ThirdPartyUser; +import tech.powerjob.server.auth.login.*; import tech.powerjob.server.common.Loggers; import tech.powerjob.server.common.utils.DigestUtils; import tech.powerjob.server.persistence.remote.model.PwjbUserInfoDO; @@ -35,10 +32,6 @@ public class PowerJobThirdPartyLoginService implements ThirdPartyLoginService { @Resource private PwjbUserInfoRepository pwjbUserInfoRepository; - private static final String KEY_USERNAME = "username"; - private static final String KEY_PASSWORD = "password"; - - private static final String KEY_ENCRYPTION = "encryption"; @Override public LoginTypeInfo loginType() { @@ -63,9 +56,11 @@ public class PowerJobThirdPartyLoginService implements ThirdPartyLoginService { Map loginInfoMap = JsonUtils.parseMap(loginInfo); - final String username = MapUtils.getString(loginInfoMap, KEY_USERNAME); - final String password = MapUtils.getString(loginInfoMap, KEY_PASSWORD); - final String encryption = MapUtils.getString(loginInfoMap, KEY_ENCRYPTION); + final String username = MapUtils.getString(loginInfoMap, AuthConstants.PARAM_KEY_USERNAME); + final String password = MapUtils.getString(loginInfoMap, AuthConstants.PARAM_KEY_PASSWORD); + final String encryption = MapUtils.getString(loginInfoMap, AuthConstants.PARAM_KEY_ENCRYPTION); + + Loggers.WEB.debug("[PowerJobLoginService] username: {}, password: {}, encryption: {}", username, password, encryption); if (StringUtils.isAnyEmpty(username, password)) { Loggers.WEB.debug("[PowerJobLoginService] username or password is empty, login failed!"); @@ -96,10 +91,31 @@ public class PowerJobThirdPartyLoginService implements ThirdPartyLoginService { } } + // 下发加密的密码作为 JWT 的一部分,方便处理改密码后失效的场景 + TokenLoginVerifyInfo tokenLoginVerifyInfo = new TokenLoginVerifyInfo(); + tokenLoginVerifyInfo.setEncryptedToken(dbUser.getPassword()); + bizUser.setTokenLoginVerifyInfo(tokenLoginVerifyInfo); + return bizUser; } Loggers.WEB.debug("[PowerJobLoginService] user[{}]'s password is incorrect, login failed!", username); throw new PowerJobException("password is incorrect"); } + + @Override + public boolean tokenLoginVerify(String username, TokenLoginVerifyInfo tokenLoginVerifyInfo) { + + if (tokenLoginVerifyInfo == null) { + return false; + } + + final Optional userInfoOpt = pwjbUserInfoRepository.findByUsername(username); + if (userInfoOpt.isPresent()) { + String dbPassword = userInfoOpt.get().getPassword(); + return StringUtils.equals(dbPassword, tokenLoginVerifyInfo.getEncryptedToken()); + } + + return false; + } } diff --git a/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/service/login/LoginRequest.java b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/service/login/LoginRequest.java index 77b66bf2..a72bd06c 100644 --- a/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/service/login/LoginRequest.java +++ b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/service/login/LoginRequest.java @@ -25,5 +25,8 @@ public class LoginRequest { */ private String originParams; + /** + * http原始请求,第三方回调参数传递类型无法枚举,直接传递 HttpServletRequest 满足扩展性要求 + */ private transient HttpServletRequest httpServletRequest; } diff --git a/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/service/login/impl/PowerJobLoginServiceImpl.java b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/service/login/impl/PowerJobLoginServiceImpl.java index 2b50e704..e107b889 100644 --- a/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/service/login/impl/PowerJobLoginServiceImpl.java +++ b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/service/login/impl/PowerJobLoginServiceImpl.java @@ -2,11 +2,15 @@ package tech.powerjob.server.auth.service.login.impl; import com.google.common.collect.Lists; import com.google.common.collect.Maps; +import lombok.Data; +import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.MapUtils; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import tech.powerjob.common.serialize.JsonUtils; import tech.powerjob.server.auth.LoginUserHolder; import tech.powerjob.server.auth.PowerJobUser; import tech.powerjob.server.auth.common.AuthConstants; @@ -14,10 +18,7 @@ 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.server.auth.login.LoginTypeInfo; -import tech.powerjob.server.auth.login.ThirdPartyLoginRequest; -import tech.powerjob.server.auth.login.ThirdPartyLoginService; -import tech.powerjob.server.auth.login.ThirdPartyUser; +import tech.powerjob.server.auth.login.*; import tech.powerjob.server.auth.service.login.LoginRequest; import tech.powerjob.server.auth.service.login.PowerJobLoginService; import tech.powerjob.server.common.Loggers; @@ -26,6 +27,8 @@ import tech.powerjob.server.persistence.remote.repository.UserInfoRepository; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; +import java.io.Serializable; +import java.util.Date; import java.util.List; import java.util.Map; import java.util.Optional; @@ -41,15 +44,10 @@ import java.util.stream.Collectors; @Service public class PowerJobLoginServiceImpl implements PowerJobLoginService { - private final JwtService jwtService; private final UserInfoRepository userInfoRepository; private final Map code2ThirdPartyLoginService; - - - private static final String KEY_USERNAME = "userName"; - @Autowired public PowerJobLoginServiceImpl(JwtService jwtService, UserInfoRepository userInfoRepository, List thirdPartyLoginServices) { @@ -96,6 +94,8 @@ public class PowerJobLoginServiceImpl implements PowerJobLoginService { newUser.setAccountType(loginType); newUser.setOriginUsername(bizUser.getUsername()); + newUser.setTokenLoginVerifyInfo(JsonUtils.toJSONString(bizUser.getTokenLoginVerifyInfo())); + // 同步素材 newUser.setEmail(bizUser.getEmail()); newUser.setPhone(bizUser.getPhone()); @@ -107,6 +107,15 @@ public class PowerJobLoginServiceImpl implements PowerJobLoginService { userInfoRepository.saveAndFlush(newUser); powerJobUserOpt = userInfoRepository.findByUsername(dbUserName); + } else { + + // 更新二次校验的 TOKEN 信息 + UserInfoDO dbUserInfoDO = powerJobUserOpt.get(); + + dbUserInfoDO.setTokenLoginVerifyInfo(JsonUtils.toJSONString(bizUser.getTokenLoginVerifyInfo())); + dbUserInfoDO.setGmtModified(new Date()); + + userInfoRepository.saveAndFlush(dbUserInfoDO); } PowerJobUser ret = new PowerJobUser(); @@ -118,23 +127,52 @@ public class PowerJobLoginServiceImpl implements PowerJobLoginService { ret.setUsername(dbUserName); } - fillJwt(ret); + fillJwt(ret, Optional.ofNullable(bizUser.getTokenLoginVerifyInfo()).map(TokenLoginVerifyInfo::getEncryptedToken).orElse(null)); return ret; } @Override public Optional ifLogin(HttpServletRequest httpServletRequest) { - final Optional userNameOpt = parseUserName(httpServletRequest); - return userNameOpt.flatMap(uname -> userInfoRepository.findByUsername(uname).map(userInfoDO -> { - PowerJobUser powerJobUser = new PowerJobUser(); - BeanUtils.copyProperties(userInfoDO, powerJobUser); + final Optional jwtBodyOpt = parseJwt(httpServletRequest); + if (!jwtBodyOpt.isPresent()) { + return Optional.empty(); + } - // 兼容某些直接通过 ifLogin 判断登录的场景 - LoginUserHolder.set(powerJobUser); + JwtBody jwtBody = jwtBodyOpt.get(); - return powerJobUser; - })); + Optional dbUserInfoOpt = userInfoRepository.findByUsername(jwtBody.getUsername()); + if (!dbUserInfoOpt.isPresent()) { + throw new PowerJobAuthException(AuthErrorCode.USER_NOT_EXIST); + } + + UserInfoDO dbUser = dbUserInfoOpt.get(); + + PowerJobUser powerJobUser = new PowerJobUser(); + + String tokenLoginVerifyInfoStr = dbUser.getTokenLoginVerifyInfo(); + TokenLoginVerifyInfo tokenLoginVerifyInfo = Optional.ofNullable(tokenLoginVerifyInfoStr).map(x -> JsonUtils.parseObjectIgnoreException(x, TokenLoginVerifyInfo.class)).orElse(new TokenLoginVerifyInfo()); + + // DB 中的 encryptedToken 存在,代表需要二次校验 + if (StringUtils.isNotEmpty(tokenLoginVerifyInfo.getEncryptedToken())) { + if (!StringUtils.equals(jwtBody.getEncryptedToken(), tokenLoginVerifyInfo.getEncryptedToken())) { + throw new PowerJobAuthException(AuthErrorCode.INVALID_TOKEN); + } + + ThirdPartyLoginService thirdPartyLoginService = code2ThirdPartyLoginService.get(dbUser.getAccountType()); + boolean tokenLoginVerifyOk = thirdPartyLoginService.tokenLoginVerify(dbUser.getOriginUsername(), tokenLoginVerifyInfo); + + if (!tokenLoginVerifyOk) { + throw new PowerJobAuthException(AuthErrorCode.USER_AUTH_FAILED); + } + } + + BeanUtils.copyProperties(dbUser, powerJobUser); + + // 兼容某些直接通过 ifLogin 判断登录的场景 + LoginUserHolder.set(powerJobUser); + + return Optional.of(powerJobUser); } private ThirdPartyLoginService fetchBizLoginService(String loginType) { @@ -145,16 +183,22 @@ public class PowerJobLoginServiceImpl implements PowerJobLoginService { return loginService; } - private void fillJwt(PowerJobUser powerJobUser) { - Map jwtMap = Maps.newHashMap(); + private void fillJwt(PowerJobUser powerJobUser, String encryptedToken) { // 不能下发 userId,容易被轮询爆破 - jwtMap.put(KEY_USERNAME, powerJobUser.getUsername()); + JwtBody jwtBody = new JwtBody(); + jwtBody.setUsername(powerJobUser.getUsername()); + if (StringUtils.isNotEmpty(encryptedToken)) { + jwtBody.setEncryptedToken(encryptedToken); + } + + Map jwtMap = JsonUtils.parseMap(JsonUtils.toJSONString(jwtBody)); powerJobUser.setJwtToken(jwtService.build(jwtMap, null)); } - private Optional parseUserName(HttpServletRequest httpServletRequest) { + @SneakyThrows + private Optional parseJwt(HttpServletRequest httpServletRequest) { // header、cookie 都能获取 String jwtStr = HttpServletUtils.fetchFromHeader(AuthConstants.JWT_NAME, httpServletRequest); @@ -169,12 +213,19 @@ public class PowerJobLoginServiceImpl implements PowerJobLoginService { return Optional.empty(); } final Map jwtBodyMap = jwtService.parse(jwtStr, null); - final Object userName = jwtBodyMap.get(KEY_USERNAME); - if (userName == null) { + if (MapUtils.isEmpty(jwtBodyMap)) { return Optional.empty(); } - return Optional.of(String.valueOf(userName)); + return Optional.ofNullable(JsonUtils.parseObject(JsonUtils.toJSONString(jwtBodyMap), JwtBody.class)); + } + + @Data + static class JwtBody implements Serializable { + + private String username; + + private String encryptedToken; } } diff --git a/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/service/permission/PowerJobPermissionService.java b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/service/permission/PowerJobPermissionService.java index 382f02cc..db534abd 100644 --- a/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/service/permission/PowerJobPermissionService.java +++ b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/service/permission/PowerJobPermissionService.java @@ -27,23 +27,23 @@ public interface PowerJobPermissionService { boolean hasPermission(Long userId, RoleScope roleScope, Long target, Permission permission); /** - * 授予用户权限 + * 授予用户角色 * @param roleScope 权限范围 * @param target 权限目标 * @param userId 用户ID * @param role 角色 * @param extra 其他 */ - void grantPermission(RoleScope roleScope, Long target, Long userId, Role role, String extra); + void grantRole(RoleScope roleScope, Long target, Long userId, Role role, String extra); /** - * 回收用户权限 + * 回收用户角色 * @param roleScope 权限范围 * @param target 权限目标 * @param userId 用户ID * @param role 角色 */ - void retrievePermission(RoleScope roleScope, Long target, Long userId, Role role); + void retrieveRole(RoleScope roleScope, Long target, Long userId, Role role); /** * 获取有相关权限的用户 diff --git a/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/service/permission/PowerJobPermissionServiceImpl.java b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/service/permission/PowerJobPermissionServiceImpl.java index e62e7853..dcbad2da 100644 --- a/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/service/permission/PowerJobPermissionServiceImpl.java +++ b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/service/permission/PowerJobPermissionServiceImpl.java @@ -85,7 +85,7 @@ public class PowerJobPermissionServiceImpl implements PowerJobPermissionService } @Override - public void grantPermission(RoleScope roleScope, Long target, Long userId, Role role, String extra) { + public void grantRole(RoleScope roleScope, Long target, Long userId, Role role, String extra) { UserRoleDO userRoleDO = new UserRoleDO(); userRoleDO.setGmtCreate(new Date()); @@ -102,7 +102,7 @@ public class PowerJobPermissionServiceImpl implements PowerJobPermissionService } @Override - public void retrievePermission(RoleScope roleScope, Long target, Long userId, Role role) { + public void retrieveRole(RoleScope roleScope, Long target, Long userId, Role role) { List originUserRole = userRoleRepository.findAllByScopeAndTargetAndRoleAndUserId(roleScope.getV(), target, role.getV(), userId); log.info("[PowerJobPermissionService] [retrievePermission] origin rule: {}", originUserRole); Optional.ofNullable(originUserRole).orElse(Collections.emptyList()).forEach(r -> { diff --git a/powerjob-server/powerjob-server-persistence/src/main/java/tech/powerjob/server/persistence/remote/model/SundryDO.java b/powerjob-server/powerjob-server-persistence/src/main/java/tech/powerjob/server/persistence/remote/model/SundryDO.java new file mode 100644 index 00000000..b72cd64f --- /dev/null +++ b/powerjob-server/powerjob-server-persistence/src/main/java/tech/powerjob/server/persistence/remote/model/SundryDO.java @@ -0,0 +1,49 @@ +package tech.powerjob.server.persistence.remote.model; + +import lombok.Data; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.GenericGenerator; + +import javax.persistence.*; +import java.util.Date; + +/** + * 杂项 + * KKV 表存一些配置数据 + * + * @author tjq + * @since 2024/2/15 + */ +@Data +@Entity +@NoArgsConstructor +@Table(uniqueConstraints = {@UniqueConstraint(name = "uidx01_sundry", columnNames = {"pkey", "skey"})}) +public class SundryDO { + + @Id + @GeneratedValue(strategy = GenerationType.AUTO, generator = "native") + @GenericGenerator(name = "native", strategy = "native") + private Long id; + + /** + * PKEY + */ + private String pkey; + /** + * SKEY + */ + private String skey; + /** + * 内容 + */ + private String content; + + /** + * 其他参数 + */ + 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/model/UserInfoDO.java b/powerjob-server/powerjob-server-persistence/src/main/java/tech/powerjob/server/persistence/remote/model/UserInfoDO.java index 8a6aa581..d0c5db1d 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 @@ -54,6 +54,12 @@ public class UserInfoDO { * webHook */ private String webHook; + + /** + * JWT 登录的二次校验信息 + */ + private String tokenLoginVerifyInfo; + /** * 扩展字段 for 第三方 * PowerJob 内部不允许使用该字段 diff --git a/powerjob-server/powerjob-server-persistence/src/main/java/tech/powerjob/server/persistence/remote/repository/SundryRepository.java b/powerjob-server/powerjob-server-persistence/src/main/java/tech/powerjob/server/persistence/remote/repository/SundryRepository.java new file mode 100644 index 00000000..62e47a5a --- /dev/null +++ b/powerjob-server/powerjob-server-persistence/src/main/java/tech/powerjob/server/persistence/remote/repository/SundryRepository.java @@ -0,0 +1,20 @@ +package tech.powerjob.server.persistence.remote.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import tech.powerjob.server.persistence.remote.model.SundryDO; + +import java.util.List; +import java.util.Optional; + +/** + * SundryRepository + * + * @author tjq + * @since 2024/2/15 + */ +public interface SundryRepository extends JpaRepository { + + List findAllByPkey(String pkey); + + Optional findByPkeyAndSkey(String pkey, String skey); +} diff --git a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/auth/plugin/SaveGrantPermissionPlugin.java b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/auth/plugin/SaveGrantPermissionPlugin.java index 3bc273e0..766d6854 100644 --- a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/auth/plugin/SaveGrantPermissionPlugin.java +++ b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/auth/plugin/SaveGrantPermissionPlugin.java @@ -69,7 +69,7 @@ public abstract class SaveGrantPermissionPlugin implements GrantPermissionPlugin Map extra = Maps.newHashMap(); extra.put("source", "SaveGrantPermissionPlugin"); - powerJobPermissionService.grantPermission(fetchRuleScope(), savedId, powerJobUser.getId(), Role.ADMIN, JsonUtils.toJSONString(extra)); + powerJobPermissionService.grantRole(fetchRuleScope(), savedId, powerJobUser.getId(), Role.ADMIN, JsonUtils.toJSONString(extra)); } protected abstract RoleScope fetchRuleScope(); diff --git a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/auth/service/impl/WebAuthServiceImpl.java b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/auth/service/impl/WebAuthServiceImpl.java index 568ebad3..e70aa043 100644 --- a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/auth/service/impl/WebAuthServiceImpl.java +++ b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/auth/service/impl/WebAuthServiceImpl.java @@ -91,14 +91,14 @@ public class WebAuthServiceImpl implements WebAuthService { // 在 originUids 不在 currentUids,需要取消授权 allIds.removeAll(currentUids); allIds.forEach(cancelPermissionUid -> { - powerJobPermissionService.retrievePermission(roleScope, target, cancelPermissionUid, role); + powerJobPermissionService.retrieveRole(roleScope, target, cancelPermissionUid, role); log.info("[WebAuthService] [diffGrant] cancelPermission: roleScope={},target={},uid={},role={}", roleScope, target, cancelPermissionUid, role); }); // 在 currentUids 当不在 orignUids,需要增加授权 allIds2.removeAll(originUids); allIds2.forEach(addPermissionUid -> { - powerJobPermissionService.grantPermission(roleScope, target, addPermissionUid, role, extra); + powerJobPermissionService.grantRole(roleScope, target, addPermissionUid, role, extra); log.info("[WebAuthService] [diffGrant] grantPermission: roleScope={},target={},uid={},role={},extra={}", roleScope, target, addPermissionUid, role, extra); }); } diff --git a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/initializer/NewSystemInitializer.java b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/initializer/NewSystemInitializer.java new file mode 100644 index 00000000..130b18fd --- /dev/null +++ b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/initializer/NewSystemInitializer.java @@ -0,0 +1,102 @@ +package tech.powerjob.server.initializer; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.CommandLineRunner; +import org.springframework.stereotype.Component; +import tech.powerjob.common.utils.CommonUtils; +import tech.powerjob.server.extension.LockService; +import tech.powerjob.server.persistence.remote.model.SundryDO; +import tech.powerjob.server.persistence.remote.repository.SundryRepository; + +import javax.annotation.Resource; +import java.util.Date; +import java.util.Optional; +import java.util.function.Consumer; + +/** + * 新系统初始化器 + * + * @author tjq + * @since 2023/9/5 + */ +@Slf4j +@Component +public class NewSystemInitializer implements CommandLineRunner { + + + private static final String LOCK_PREFIX = "sys_init_lock_"; + + private static final int MAX_LOCK_TIME = 5000; + + + @Resource + private LockService lockService; + @Resource + private SundryRepository sundryRepository; + @Resource + private SystemInitializeService systemInitializeService; + + private static final String SUNDRY_PKEY = "sys_initialize"; + + @Override + public void run(String... args) throws Exception { + initSystemAdmin(); + initDefaultNamespace(); + } + + private void initSystemAdmin() { + clusterInit(SystemInitializeService.GOAL_INIT_ADMIN, Void -> systemInitializeService.initAdmin()); + } + + private void initDefaultNamespace() { + clusterInit(SystemInitializeService.GOAL_INIT_NAMESPACE, Void -> systemInitializeService.initNamespace()); + } + + private void clusterInit(String name, Consumer initFunc) { + + Optional sundryOpt = sundryRepository.findByPkeyAndSkey(SUNDRY_PKEY, name); + if (sundryOpt.isPresent()) { + log.info("[NewSystemInitializer] already initialized, skip: {}", name); + return; + } + + String lockName = LOCK_PREFIX.concat(name); + + while (true) { + try { + + boolean lockStatus = lockService.tryLock(lockName, MAX_LOCK_TIME); + + // 无论是否拿到锁,都重现检测一次,如果已完成初始化,则直接 return + Optional sundryOpt2 = sundryRepository.findByPkeyAndSkey(SUNDRY_PKEY, name); + if (sundryOpt2.isPresent()) { + log.info("[NewSystemInitializer] other server finished initialize, skip process: {}", name); + break; + } + + if (!lockStatus) { + CommonUtils.easySleep(277); + continue; + } + + log.info("[NewSystemInitializer] try to initialize: {}", name); + initFunc.accept(null); + log.info("[NewSystemInitializer] initialize [{}] successfully!", name); + + // 写入初始化成功标记 + SundryDO sundryDO = new SundryDO(); + sundryDO.setPkey(SUNDRY_PKEY); + sundryDO.setSkey(name); + sundryDO.setContent("A"); + sundryDO.setGmtCreate(new Date()); + sundryRepository.saveAndFlush(sundryDO); + log.info("[NewSystemInitializer] write initialized tag successfully: {}", sundryDO); + + break; + } finally { + lockService.unlock(lockName); + } + } + + } +} diff --git a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/initializer/SystemInitializeService.java b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/initializer/SystemInitializeService.java new file mode 100644 index 00000000..e5ec2ceb --- /dev/null +++ b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/initializer/SystemInitializeService.java @@ -0,0 +1,24 @@ +package tech.powerjob.server.initializer; + +/** + * 系统初始化服务 + * + * @author tjq + * @since 2024/2/15 + */ +public interface SystemInitializeService { + + String GOAL_INIT_ADMIN = "goal_init_admin"; + String GOAL_INIT_NAMESPACE = "goal_init_namespace"; + + + /** + * 初始化超级管理员 + */ + void initAdmin(); + + /** + * 初始化 namespace + */ + void initNamespace(); +} diff --git a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/initializer/SystemInitializeServiceImpl.java b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/initializer/SystemInitializeServiceImpl.java new file mode 100644 index 00000000..00a717c7 --- /dev/null +++ b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/initializer/SystemInitializeServiceImpl.java @@ -0,0 +1,105 @@ +package tech.powerjob.server.initializer; + +import com.google.common.collect.Maps; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.RandomStringUtils; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import tech.powerjob.common.serialize.JsonUtils; +import tech.powerjob.server.auth.PowerJobUser; +import tech.powerjob.server.auth.Role; +import tech.powerjob.server.auth.RoleScope; +import tech.powerjob.server.auth.common.AuthConstants; +import tech.powerjob.server.auth.service.login.LoginRequest; +import tech.powerjob.server.auth.service.login.PowerJobLoginService; +import tech.powerjob.server.auth.service.permission.PowerJobPermissionService; +import tech.powerjob.server.persistence.remote.model.NamespaceDO; +import tech.powerjob.server.persistence.remote.model.PwjbUserInfoDO; +import tech.powerjob.server.web.request.ModifyNamespaceRequest; +import tech.powerjob.server.web.request.ModifyUserInfoRequest; +import tech.powerjob.server.web.service.NamespaceWebService; +import tech.powerjob.server.web.service.PwjbUserWebService; + +import javax.annotation.Resource; +import javax.transaction.Transactional; +import java.util.Map; + +/** + * 初始化 PowerJob 首次部署相关的内容 + * 为了可维护性足够高,统一使用 WEB 请求进行初始化,不直接操作底层,防止后续内部逻辑变更后出现问题 + * + * @author tjq + * @since 2024/2/15 + */ +@Slf4j +@Service +public class SystemInitializeServiceImpl implements SystemInitializeService { + + @Value("${oms.auth.initiliaze.admin.password:#{null}}") + private String defaultAdminPassword; + @Resource + private PwjbUserWebService pwjbUserWebService; + @Resource + private NamespaceWebService namespaceWebService; + @Resource + private PowerJobLoginService powerJobLoginService; + @Resource + private PowerJobPermissionService powerJobPermissionService; + + private static final String SYSTEM_ADMIN_NAME = "ADMIN"; + + + private static final String SYSTEM_DEFAULT_NAMESPACE = "default_namespace"; + + @Override + @Transactional(rollbackOn = Exception.class) + public void initAdmin() { + + String username = SYSTEM_ADMIN_NAME; + String password = StringUtils.isEmpty(defaultAdminPassword) ? RandomStringUtils.randomAlphabetic(8) : defaultAdminPassword; + + // STEP1: 创建 PWJB 用户 + ModifyUserInfoRequest createUser = new ModifyUserInfoRequest(); + createUser.setUsername(username); + createUser.setNick(username); + createUser.setPassword(password); + + log.info("[SystemInitializeService] [S1] create default PWJB user by request: {}", createUser); + PwjbUserInfoDO savedPwjbUser = pwjbUserWebService.save(createUser); + log.info("[SystemInitializeService] [S1] create default PWJB user successfully: {}", savedPwjbUser); + + Map params = Maps.newHashMap(); + params.put(AuthConstants.PARAM_KEY_USERNAME, username); + params.put(AuthConstants.PARAM_KEY_PASSWORD, password); + + // STEP2: 创建 USER 对象 + LoginRequest loginRequest = new LoginRequest() + .setLoginType(AuthConstants.ACCOUNT_TYPE_POWER_JOB) + .setOriginParams(JsonUtils.toJSONString(params)); + log.info("[SystemInitializeService] [S2] createPowerJobUser user by request: {}", loginRequest); + PowerJobUser powerJobUser = powerJobLoginService.doLogin(loginRequest); + log.info("[SystemInitializeService] [S2] createPowerJobUser successfully: {}", powerJobUser); + + // STEP3: 授予全局管理员权限 + powerJobPermissionService.grantRole(RoleScope.GLOBAL, AuthConstants.GLOBAL_ADMIN_TARGET_ID, powerJobUser.getId(), Role.ADMIN, null); + log.info("[SystemInitializeService] [S3] GRANT ADMIN successfully!"); + + // 循环10遍,强提醒用户,第一次使用必须更改 admin 密码 + for (int i = 0; i < 10; i++) { + log.warn("[SystemInitializeService] The system has automatically created a super administrator account[username={},password={}], please log in and change the password immediately!", username, password); + } + } + + @Override + @Transactional(rollbackOn = Exception.class) + public void initNamespace() { + ModifyNamespaceRequest saveNamespaceReq = new ModifyNamespaceRequest(); + saveNamespaceReq.setName(SYSTEM_DEFAULT_NAMESPACE); + saveNamespaceReq.setCode(SYSTEM_DEFAULT_NAMESPACE); + + log.info("[SystemInitializeService] create default namespace by request: {}", saveNamespaceReq); + NamespaceDO savedNamespaceDO = namespaceWebService.save(saveNamespaceReq); + log.info("[SystemInitializeService] create default namespace successfully: {}", savedNamespaceDO); + } +} diff --git a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/controller/AuthController.java b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/controller/AuthController.java index cf80e3fd..26ba74f4 100644 --- a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/controller/AuthController.java +++ b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/controller/AuthController.java @@ -1,18 +1,25 @@ package tech.powerjob.server.web.controller; +import com.google.common.collect.Maps; import org.springframework.web.bind.annotation.*; import tech.powerjob.common.response.ResultDTO; -import tech.powerjob.server.auth.PowerJobUser; +import tech.powerjob.common.serialize.JsonUtils; +import tech.powerjob.server.auth.*; import tech.powerjob.server.auth.common.AuthConstants; +import tech.powerjob.server.auth.interceptor.ApiPermission; import tech.powerjob.server.auth.login.LoginTypeInfo; import tech.powerjob.server.auth.service.login.LoginRequest; import tech.powerjob.server.auth.service.login.PowerJobLoginService; +import tech.powerjob.server.auth.service.permission.PowerJobPermissionService; +import tech.powerjob.server.web.request.GrantPermissionRequest; import javax.annotation.Resource; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.Optional; /** @@ -27,6 +34,8 @@ public class AuthController { @Resource private PowerJobLoginService powerJobLoginService; + @Resource + private PowerJobPermissionService powerJobPermissionService; @GetMapping("/supportLoginTypes") public ResultDTO> listSupportLoginTypes() { @@ -87,6 +96,34 @@ public class AuthController { return powerJobUser.map(ResultDTO::success).orElseGet(() -> ResultDTO.success(null)); } + /* ****************** 授权相关 ****************** */ + @PostMapping("/grantAdmin") + @ApiPermission(name = "Auth-GrantAdmin", roleScope = RoleScope.GLOBAL, requiredPermission = Permission.SU) + public ResultDTO grantAppPermission(GrantPermissionRequest grantPermissionRequest) { + + grantPermissionRequest.setRole(Role.ADMIN.getV()); + grantPermissionRequest.setTargetId(AuthConstants.GLOBAL_ADMIN_TARGET_ID); + + grantPermission(RoleScope.GLOBAL, grantPermissionRequest); + return ResultDTO.success(null); + } + + + private void grantPermission(RoleScope roleScope, GrantPermissionRequest grantPermissionRequest) { + + Role role = Role.of(grantPermissionRequest.getRole()); + + Optional.ofNullable(grantPermissionRequest.getUserIds()).orElse(Collections.emptyList()).forEach(uid -> { + // 记录授权人信息 + Map extraInfo = Maps.newHashMap(); + extraInfo.put("grantor", LoginUserHolder.getUserName()); + String extra = JsonUtils.toJSONString(extraInfo); + + powerJobPermissionService.grantRole(roleScope, grantPermissionRequest.getTargetId(), uid, role, extra); + }); + + } + private void fillJwt4LoginUser(PowerJobUser powerJobUser, HttpServletResponse httpServletResponse) { httpServletResponse.addCookie(new Cookie(AuthConstants.JWT_NAME, powerJobUser.getJwtToken())); } diff --git a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/controller/NamespaceController.java b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/controller/NamespaceController.java index b7a199ce..fef34e3d 100644 --- a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/controller/NamespaceController.java +++ b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/controller/NamespaceController.java @@ -1,17 +1,9 @@ package tech.powerjob.server.web.controller; -import com.google.common.collect.Lists; import lombok.extern.slf4j.Slf4j; -import org.apache.commons.collections4.CollectionUtils; -import org.apache.commons.lang3.StringUtils; import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Pageable; -import org.springframework.data.jpa.domain.Specification; import org.springframework.web.bind.annotation.*; -import tech.powerjob.common.exception.PowerJobException; import tech.powerjob.common.response.ResultDTO; -import tech.powerjob.server.auth.LoginUserHolder; import tech.powerjob.server.auth.Permission; import tech.powerjob.server.auth.RoleScope; import tech.powerjob.server.auth.common.AuthConstants; @@ -19,27 +11,18 @@ import tech.powerjob.server.auth.interceptor.ApiPermission; import tech.powerjob.server.auth.plugin.ModifyOrCreateDynamicPermission; import tech.powerjob.server.auth.plugin.SaveNamespaceGrantPermissionPlugin; import tech.powerjob.server.auth.service.WebAuthService; -import tech.powerjob.server.common.SJ; -import tech.powerjob.server.common.constants.SwitchableStatus; import tech.powerjob.server.persistence.PageResult; -import tech.powerjob.server.persistence.QueryConvertUtils; -import tech.powerjob.server.persistence.remote.model.AppInfoDO; import tech.powerjob.server.persistence.remote.model.NamespaceDO; -import tech.powerjob.server.persistence.remote.repository.AppInfoRepository; -import tech.powerjob.server.persistence.remote.repository.NamespaceRepository; import tech.powerjob.server.web.converter.NamespaceConverter; import tech.powerjob.server.web.request.ComponentUserRoleInfo; import tech.powerjob.server.web.request.ModifyNamespaceRequest; import tech.powerjob.server.web.request.QueryNamespaceRequest; import tech.powerjob.server.web.response.NamespaceBaseVO; import tech.powerjob.server.web.response.NamespaceVO; +import tech.powerjob.server.web.service.NamespaceWebService; import javax.annotation.Resource; -import javax.persistence.criteria.Predicate; -import java.util.Date; import java.util.List; -import java.util.Optional; -import java.util.UUID; import java.util.stream.Collectors; /** @@ -56,67 +39,21 @@ public class NamespaceController { @Resource private WebAuthService webAuthService; @Resource - private AppInfoRepository appInfoRepository; - @Resource - private NamespaceRepository namespaceRepository; + private NamespaceWebService namespaceWebService; @ResponseBody @PostMapping("/save") @ApiPermission(name = "Namespace-Save", roleScope = RoleScope.NAMESPACE, dynamicPermissionPlugin = ModifyOrCreateDynamicPermission.class, grandPermissionPlugin = SaveNamespaceGrantPermissionPlugin.class) public ResultDTO save(@RequestBody ModifyNamespaceRequest req) { - req.valid(); - - Long id = req.getId(); - NamespaceDO namespaceDO; - - boolean isCreate = id == null; - - if (isCreate) { - namespaceDO = new NamespaceDO(); - namespaceDO.setGmtCreate(new Date()); - - // code 单独拷贝 - namespaceDO.setCode(req.getCode()); - // 创建时生成 token - namespaceDO.setToken(UUID.randomUUID().toString()); - namespaceDO.setCreator(LoginUserHolder.getUserName()); - - } else { - namespaceDO = fetchById(id); - namespaceDO.setModifier(LoginUserHolder.getUserName()); - - if (!namespaceDO.getCode().equalsIgnoreCase(req.getCode())) { - throw new IllegalArgumentException("NOT_ALLOW_CHANGE_THE_NAMESPACE_CODE"); - } - } - - // 拷贝通用变更属性(code 不允许更改) - namespaceDO.setTags(req.getTags()); - namespaceDO.setName(req.getName()); - namespaceDO.setExtra(req.getExtra()); - namespaceDO.setStatus(Optional.ofNullable(req.getStatus()).orElse(SwitchableStatus.ENABLE.getV())); - - namespaceDO.setGmtModified(new Date()); - NamespaceDO savedNamespace = namespaceRepository.save(namespaceDO); - - // 授权 - webAuthService.processPermissionOnSave(RoleScope.NAMESPACE, savedNamespace.getId(), req.getComponentUserRoleInfo()); - + NamespaceDO savedNamespace = namespaceWebService.save(req); return ResultDTO.success(NamespaceConverter.do2BaseVo(savedNamespace)); } @DeleteMapping("/delete") @ApiPermission(name = "Namespace-Delete", roleScope = RoleScope.NAMESPACE, requiredPermission = Permission.SU) public ResultDTO deleteNamespace(Long id) { - - List appInfosInNamespace = appInfoRepository.findAllByNamespaceId(id); - if (CollectionUtils.isNotEmpty(appInfosInNamespace)) { - List relatedApps = appInfosInNamespace.stream().map(AppInfoDO::getAppName).collect(Collectors.toList()); - throw new PowerJobException("Unable to delete due to associated apps: " + SJ.COMMA_JOINER.join(relatedApps)); - } - - namespaceRepository.deleteById(id); + namespaceWebService.delete(id); return ResultDTO.success(null); } @@ -124,33 +61,7 @@ public class NamespaceController { @ApiPermission(name = "Namespace-List", roleScope = RoleScope.NAMESPACE, requiredPermission = Permission.NONE) public ResultDTO> listNamespace(@RequestBody QueryNamespaceRequest queryNamespaceRequest) { - String codeLike = queryNamespaceRequest.getCodeLike(); - String nameLike = queryNamespaceRequest.getNameLike(); - String tagLike = queryNamespaceRequest.getTagLike(); - - Pageable pageable = PageRequest.of(queryNamespaceRequest.getIndex(), queryNamespaceRequest.getPageSize()); - Specification specification = (root, query, cb) -> { - - List predicates = Lists.newArrayList(); - - if (StringUtils.isNotEmpty(codeLike)) { - predicates.add(cb.like(root.get("code"), QueryConvertUtils.convertLikeParams(codeLike))); - } - - if (StringUtils.isNotEmpty(nameLike)) { - predicates.add(cb.like(root.get("name"), QueryConvertUtils.convertLikeParams(nameLike))); - } - if (StringUtils.isNotEmpty(tagLike)) { - predicates.add(cb.like(root.get("tags"), QueryConvertUtils.convertLikeParams(tagLike))); - } - - if (predicates.isEmpty()) { - return null; - } - return query.where(predicates.toArray(new Predicate[0])).getRestriction(); - }; - - Page namespacePageResult = namespaceRepository.findAll(specification, pageable); + Page namespacePageResult = namespaceWebService.list(queryNamespaceRequest); PageResult ret = new PageResult<>(namespacePageResult); ret.setData(namespacePageResult.get().map(x -> { @@ -166,7 +77,7 @@ public class NamespaceController { @ApiPermission(name = "Namespace-ListAll", roleScope = RoleScope.NAMESPACE, requiredPermission = Permission.NONE) public ResultDTO> listAll() { // 数量应该不是很多,先简单处理,不查询精简对象 - List namespaceRepositoryAll = namespaceRepository.findAll(); + List namespaceRepositoryAll = namespaceWebService.listAll(); List namespaceBaseVOList = namespaceRepositoryAll.stream().map(nd -> { NamespaceBaseVO nv = new NamespaceBaseVO(); nv.setId(nd.getId()); @@ -191,12 +102,4 @@ public class NamespaceController { namespaceVO.setToken(hasPermission ? namespaceDO.getToken() : AuthConstants.TIPS_NO_PERMISSION_TO_SEE); } - private NamespaceDO fetchById(Long id) { - Optional namespaceDoOpt = namespaceRepository.findById(id); - if (!namespaceDoOpt.isPresent()) { - throw new IllegalArgumentException("can't find namespace by id: " + id); - } - return namespaceDoOpt.get(); - } - } diff --git a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/controller/PwjbUserInfoController.java b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/controller/PwjbUserInfoController.java index 6699e820..af397b9e 100644 --- a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/controller/PwjbUserInfoController.java +++ b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/controller/PwjbUserInfoController.java @@ -1,24 +1,15 @@ package tech.powerjob.server.web.controller; -import org.apache.commons.lang3.StringUtils; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import tech.powerjob.common.response.ResultDTO; -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.server.persistence.remote.model.PwjbUserInfoDO; -import tech.powerjob.server.persistence.remote.repository.PwjbUserInfoRepository; import tech.powerjob.server.web.request.ChangePasswordRequest; import tech.powerjob.server.web.request.ModifyUserInfoRequest; +import tech.powerjob.server.web.service.PwjbUserWebService; import javax.annotation.Resource; -import java.util.Date; -import java.util.Optional; /** * PowerJob 自带的登录体系 @@ -32,7 +23,7 @@ import java.util.Optional; public class PwjbUserInfoController { @Resource - private PwjbUserInfoRepository pwjbUserInfoRepository; + private PwjbUserWebService pwjbUserWebService; /** * 创建第三方登录体系(PowerJob) 的账户,不允许修改 @@ -41,58 +32,14 @@ public class PwjbUserInfoController { */ @PostMapping("/create") public ResultDTO save(@RequestBody ModifyUserInfoRequest request) { - - String username = request.getUsername(); - CommonUtils.requireNonNull(username, "userName can't be null or empty!"); - Optional oldUserOpt = pwjbUserInfoRepository.findByUsername(username); - if (oldUserOpt.isPresent()) { - throw new IllegalArgumentException("username already exist, please change one!"); - } - - PwjbUserInfoDO pwjbUserInfoDO = new PwjbUserInfoDO(); - - pwjbUserInfoDO.setUsername(username); - pwjbUserInfoDO.setGmtCreate(new Date()); - pwjbUserInfoDO.setGmtModified(new Date()); - - // 二次加密密码 - final String password = request.getPassword(); - if (StringUtils.isNotEmpty(password)) { - pwjbUserInfoDO.setPassword(DigestUtils.rePassword(password, pwjbUserInfoDO.getUsername())); - } - - // 其他参数存入 extra,在回调创建真正的内部 USER 时回填 - request.setPassword(null); - pwjbUserInfoDO.setExtra(JsonUtils.toJSONString(request)); - - pwjbUserInfoRepository.save(pwjbUserInfoDO); + pwjbUserWebService.save(request); return ResultDTO.success(null); } @PostMapping("/changePassword") public ResultDTO changePassword(@RequestBody ChangePasswordRequest changePasswordRequest) { - if (StringUtils.equals(changePasswordRequest.getNewPassword(), changePasswordRequest.getNewPassword2())) { - throw new IllegalArgumentException("Inconsistent passwords"); - } - - String username = changePasswordRequest.getUsername(); - Optional userOpt = pwjbUserInfoRepository.findByUsername(username); - if (!userOpt.isPresent()) { - throw new IllegalArgumentException("can't find user by username: " + username); - } - - PwjbUserInfoDO dbUser = userOpt.get(); - String oldPasswordInDb = dbUser.getPassword(); - String oldPasswordInReq = DigestUtils.rePassword(changePasswordRequest.getOldPassword(), dbUser.getUsername()); - if (!StringUtils.equals(oldPasswordInDb, oldPasswordInReq)) { - throw new PowerJobAuthException(AuthErrorCode.INCORRECT_PASSWORD); - } - - dbUser.setPassword(DigestUtils.rePassword(changePasswordRequest.getNewPassword(), dbUser.getUsername())); - dbUser.setGmtModified(new Date()); - pwjbUserInfoRepository.saveAndFlush(dbUser); - + pwjbUserWebService.changePassword(changePasswordRequest); return ResultDTO.success(null); } } diff --git a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/service/NamespaceWebService.java b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/service/NamespaceWebService.java new file mode 100644 index 00000000..c272eae9 --- /dev/null +++ b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/service/NamespaceWebService.java @@ -0,0 +1,25 @@ +package tech.powerjob.server.web.service; + +import org.springframework.data.domain.Page; +import tech.powerjob.server.persistence.remote.model.NamespaceDO; +import tech.powerjob.server.web.request.ModifyNamespaceRequest; +import tech.powerjob.server.web.request.QueryNamespaceRequest; + +import java.util.List; + +/** + * namespace web 服务 + * + * @author tjq + * @since 2024/2/15 + */ +public interface NamespaceWebService { + + NamespaceDO save(ModifyNamespaceRequest req); + + void delete(Long id); + + Page list(QueryNamespaceRequest queryNamespaceRequest); + + List listAll(); +} diff --git a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/service/PwjbUserWebService.java b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/service/PwjbUserWebService.java new file mode 100644 index 00000000..36e6ea14 --- /dev/null +++ b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/service/PwjbUserWebService.java @@ -0,0 +1,18 @@ +package tech.powerjob.server.web.service; + +import tech.powerjob.server.persistence.remote.model.PwjbUserInfoDO; +import tech.powerjob.server.web.request.ChangePasswordRequest; +import tech.powerjob.server.web.request.ModifyUserInfoRequest; + +/** + * PwjbUserWebService + * + * @author tjq + * @since 2024/2/15 + */ +public interface PwjbUserWebService { + + PwjbUserInfoDO save(ModifyUserInfoRequest request); + + void changePassword(ChangePasswordRequest changePasswordRequest); +} diff --git a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/service/impl/NamespaceWebServiceImpl.java b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/service/impl/NamespaceWebServiceImpl.java new file mode 100644 index 00000000..7bacb2cb --- /dev/null +++ b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/service/impl/NamespaceWebServiceImpl.java @@ -0,0 +1,147 @@ +package tech.powerjob.server.web.service.impl; + +import com.google.common.collect.Lists; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.stereotype.Service; +import tech.powerjob.common.exception.PowerJobException; +import tech.powerjob.server.auth.LoginUserHolder; +import tech.powerjob.server.auth.RoleScope; +import tech.powerjob.server.auth.service.WebAuthService; +import tech.powerjob.server.common.SJ; +import tech.powerjob.server.common.constants.SwitchableStatus; +import tech.powerjob.server.persistence.QueryConvertUtils; +import tech.powerjob.server.persistence.remote.model.AppInfoDO; +import tech.powerjob.server.persistence.remote.model.NamespaceDO; +import tech.powerjob.server.persistence.remote.repository.AppInfoRepository; +import tech.powerjob.server.persistence.remote.repository.NamespaceRepository; +import tech.powerjob.server.web.request.ModifyNamespaceRequest; +import tech.powerjob.server.web.request.QueryNamespaceRequest; +import tech.powerjob.server.web.service.NamespaceWebService; + +import javax.annotation.Resource; +import javax.persistence.criteria.Predicate; +import java.util.Date; +import java.util.List; +import java.util.Optional; +import java.util.UUID; +import java.util.stream.Collectors; + +/** + * NamespaceWebService + * + * @author tjq + * @since 2024/2/15 + */ +@Service +public class NamespaceWebServiceImpl implements NamespaceWebService { + + @Resource + private WebAuthService webAuthService; + @Resource + private AppInfoRepository appInfoRepository; + @Resource + private NamespaceRepository namespaceRepository; + + @Override + public NamespaceDO save(ModifyNamespaceRequest req) { + req.valid(); + + Long id = req.getId(); + NamespaceDO namespaceDO; + + boolean isCreate = id == null; + + if (isCreate) { + namespaceDO = new NamespaceDO(); + namespaceDO.setGmtCreate(new Date()); + + // code 单独拷贝 + namespaceDO.setCode(req.getCode()); + // 创建时生成 token + namespaceDO.setToken(UUID.randomUUID().toString()); + namespaceDO.setCreator(LoginUserHolder.getUserName()); + + } else { + namespaceDO = fetchById(id); + namespaceDO.setModifier(LoginUserHolder.getUserName()); + + if (!namespaceDO.getCode().equalsIgnoreCase(req.getCode())) { + throw new IllegalArgumentException("NOT_ALLOW_CHANGE_THE_NAMESPACE_CODE"); + } + } + + // 拷贝通用变更属性(code 不允许更改) + namespaceDO.setTags(req.getTags()); + namespaceDO.setName(req.getName()); + namespaceDO.setExtra(req.getExtra()); + namespaceDO.setStatus(Optional.ofNullable(req.getStatus()).orElse(SwitchableStatus.ENABLE.getV())); + + namespaceDO.setGmtModified(new Date()); + NamespaceDO savedNamespace = namespaceRepository.save(namespaceDO); + + // 授权 + webAuthService.processPermissionOnSave(RoleScope.NAMESPACE, savedNamespace.getId(), req.getComponentUserRoleInfo()); + + return savedNamespace; + } + + @Override + public void delete(Long id) { + List appInfosInNamespace = appInfoRepository.findAllByNamespaceId(id); + if (CollectionUtils.isNotEmpty(appInfosInNamespace)) { + List relatedApps = appInfosInNamespace.stream().map(AppInfoDO::getAppName).collect(Collectors.toList()); + throw new PowerJobException("Unable to delete due to associated apps: " + SJ.COMMA_JOINER.join(relatedApps)); + } + + namespaceRepository.deleteById(id); + } + + @Override + public Page list(QueryNamespaceRequest queryNamespaceRequest) { + String codeLike = queryNamespaceRequest.getCodeLike(); + String nameLike = queryNamespaceRequest.getNameLike(); + String tagLike = queryNamespaceRequest.getTagLike(); + + Pageable pageable = PageRequest.of(queryNamespaceRequest.getIndex(), queryNamespaceRequest.getPageSize()); + Specification specification = (root, query, cb) -> { + + List predicates = Lists.newArrayList(); + + if (StringUtils.isNotEmpty(codeLike)) { + predicates.add(cb.like(root.get("code"), QueryConvertUtils.convertLikeParams(codeLike))); + } + + if (StringUtils.isNotEmpty(nameLike)) { + predicates.add(cb.like(root.get("name"), QueryConvertUtils.convertLikeParams(nameLike))); + } + if (StringUtils.isNotEmpty(tagLike)) { + predicates.add(cb.like(root.get("tags"), QueryConvertUtils.convertLikeParams(tagLike))); + } + + if (predicates.isEmpty()) { + return null; + } + return query.where(predicates.toArray(new Predicate[0])).getRestriction(); + }; + + return namespaceRepository.findAll(specification, pageable); + } + + @Override + public List listAll() { + return namespaceRepository.findAll(); + } + + private NamespaceDO fetchById(Long id) { + Optional namespaceDoOpt = namespaceRepository.findById(id); + if (!namespaceDoOpt.isPresent()) { + throw new IllegalArgumentException("can't find namespace by id: " + id); + } + return namespaceDoOpt.get(); + } +} 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 new file mode 100644 index 00000000..5b0c7ec9 --- /dev/null +++ b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/service/impl/PwjbUserWebServiceImplImpl.java @@ -0,0 +1,88 @@ +package tech.powerjob.server.web.service.impl; + +import lombok.SneakyThrows; +import org.apache.commons.lang3.StringUtils; +import org.springframework.stereotype.Service; +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.server.persistence.remote.model.PwjbUserInfoDO; +import tech.powerjob.server.persistence.remote.repository.PwjbUserInfoRepository; +import tech.powerjob.server.web.request.ChangePasswordRequest; +import tech.powerjob.server.web.request.ModifyUserInfoRequest; +import tech.powerjob.server.web.service.PwjbUserWebService; + +import javax.annotation.Resource; +import java.util.Date; +import java.util.Optional; + +/** + * PwjbUserWebService + * + * @author tjq + * @since 2024/2/15 + */ +@Service +public class PwjbUserWebServiceImplImpl implements PwjbUserWebService { + + @Resource + private PwjbUserInfoRepository pwjbUserInfoRepository; + + @Override + @SneakyThrows + public PwjbUserInfoDO save(ModifyUserInfoRequest request) { + String username = request.getUsername(); + CommonUtils.requireNonNull(username, "userName can't be null or empty!"); + Optional oldUserOpt = pwjbUserInfoRepository.findByUsername(username); + if (oldUserOpt.isPresent()) { + throw new IllegalArgumentException("username already exist, please change one!"); + } + + PwjbUserInfoDO pwjbUserInfoDO = new PwjbUserInfoDO(); + + pwjbUserInfoDO.setUsername(username); + pwjbUserInfoDO.setGmtCreate(new Date()); + pwjbUserInfoDO.setGmtModified(new Date()); + + // 二次加密密码 + final String password = request.getPassword(); + if (StringUtils.isNotEmpty(password)) { + pwjbUserInfoDO.setPassword(DigestUtils.rePassword(password, pwjbUserInfoDO.getUsername())); + } + + // 其他参数存入 extra,在回调创建真正的内部 USER 时回填 + ModifyUserInfoRequest cpRequest = JsonUtils.parseObject(JsonUtils.toJSONString(request), ModifyUserInfoRequest.class); + cpRequest.setPassword(null); + cpRequest.setUsername(null); + cpRequest.setNick(null); + pwjbUserInfoDO.setExtra(JsonUtils.toJSONString(cpRequest)); + + return pwjbUserInfoRepository.save(pwjbUserInfoDO); + } + + @Override + public void changePassword(ChangePasswordRequest changePasswordRequest) { + if (!StringUtils.equals(changePasswordRequest.getNewPassword(), changePasswordRequest.getNewPassword2())) { + throw new IllegalArgumentException("Inconsistent passwords"); + } + + String username = changePasswordRequest.getUsername(); + Optional userOpt = pwjbUserInfoRepository.findByUsername(username); + if (!userOpt.isPresent()) { + throw new IllegalArgumentException("can't find user by username: " + username); + } + + PwjbUserInfoDO dbUser = userOpt.get(); + String oldPasswordInDb = dbUser.getPassword(); + String oldPasswordInReq = DigestUtils.rePassword(changePasswordRequest.getOldPassword(), dbUser.getUsername()); + if (!StringUtils.equals(oldPasswordInDb, oldPasswordInReq)) { + throw new PowerJobAuthException(AuthErrorCode.INCORRECT_PASSWORD); + } + + dbUser.setPassword(DigestUtils.rePassword(changePasswordRequest.getNewPassword(), dbUser.getUsername())); + dbUser.setGmtModified(new Date()); + pwjbUserInfoRepository.saveAndFlush(dbUser); + } +} diff --git a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/service/z-package-info.java b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/service/z-package-info.java new file mode 100644 index 00000000..e1f263ce --- /dev/null +++ b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/service/z-package-info.java @@ -0,0 +1,9 @@ +/** + * 处理 WEB 服务的 service 层 + * 如果有共用逻辑可以单独抽成 service,否则直接写在 controller 即可。PowerJob 的 WEB 领域模型不复杂,没必要过度封装。 + * LESS IS MORE + * + * @author tjq + * @since 2024/2/15 + */ +package tech.powerjob.server.web.service; \ No newline at end of file