Merge branch '5.0.0_v2' into 5.0.1_beta

# Conflicts:
#	others/powerjob-mysql.sql
#	pom.xml
#	powerjob-client/pom.xml
#	powerjob-common/pom.xml
#	powerjob-official-processors/pom.xml
#	powerjob-remote/pom.xml
#	powerjob-remote/powerjob-remote-benchmark/pom.xml
#	powerjob-remote/powerjob-remote-framework/pom.xml
#	powerjob-remote/powerjob-remote-impl-akka/pom.xml
#	powerjob-remote/powerjob-remote-impl-http/pom.xml
#	powerjob-server/pom.xml
#	powerjob-server/powerjob-server-common/pom.xml
#	powerjob-server/powerjob-server-core/pom.xml
#	powerjob-server/powerjob-server-extension/pom.xml
#	powerjob-server/powerjob-server-migrate/pom.xml
#	powerjob-server/powerjob-server-monitor/pom.xml
#	powerjob-server/powerjob-server-persistence/pom.xml
#	powerjob-server/powerjob-server-remote/pom.xml
#	powerjob-server/powerjob-server-starter/pom.xml
#	powerjob-server/powerjob-server-starter/src/main/resources/static/index.html
#	powerjob-server/powerjob-server-starter/src/main/resources/static/js/1.js
#	powerjob-server/powerjob-server-starter/src/main/resources/static/js/10.js
#	powerjob-server/powerjob-server-starter/src/main/resources/static/js/11.js
#	powerjob-server/powerjob-server-starter/src/main/resources/static/js/2.js
#	powerjob-server/powerjob-server-starter/src/main/resources/static/js/3.js
#	powerjob-server/powerjob-server-starter/src/main/resources/static/js/4.js
#	powerjob-server/powerjob-server-starter/src/main/resources/static/js/5.js
#	powerjob-server/powerjob-server-starter/src/main/resources/static/js/6.js
#	powerjob-server/powerjob-server-starter/src/main/resources/static/js/7.js
#	powerjob-server/powerjob-server-starter/src/main/resources/static/js/8.js
#	powerjob-server/powerjob-server-starter/src/main/resources/static/js/9.js
#	powerjob-server/powerjob-server-starter/src/main/resources/static/js/app.js
#	powerjob-worker-agent/pom.xml
#	powerjob-worker-samples/pom.xml
#	powerjob-worker-spring-boot-starter/pom.xml
#	powerjob-worker/pom.xml
This commit is contained in:
tjq 2024-03-09 15:40:54 +08:00
commit 02304fe921
150 changed files with 6127 additions and 623 deletions

View File

@ -1,6 +1,6 @@
/*
SQL MySQL8 SQL
使 SQL使 SpringDataJPA SQL
使 SQL使 SpringDataJPA SQL
Navicat Premium Data Transfer
@ -8,13 +8,13 @@
Source Server Type : MySQL
Source Server Version : 80300 (8.3.0)
Source Host : localhost:3306
Source Schema : powerjob4
Source Schema : powerjob500
Target Server Type : MySQL
Target Server Version : 80300 (8.3.0)
File Encoding : 65001
Date: 02/03/2024 18:51:36
Date: 17/02/2024 22:20:07
*/
SET NAMES utf8mb4;
@ -27,10 +27,16 @@ DROP TABLE IF EXISTS `app_info`;
CREATE TABLE `app_info` (
`id` bigint NOT NULL AUTO_INCREMENT,
`app_name` varchar(255) DEFAULT NULL,
`creator` bigint DEFAULT NULL,
`current_server` varchar(255) DEFAULT NULL,
`extra` varchar(255) DEFAULT NULL,
`gmt_create` datetime(6) DEFAULT NULL,
`gmt_modified` datetime(6) DEFAULT NULL,
`modifier` bigint DEFAULT NULL,
`namespace_id` bigint DEFAULT NULL,
`password` varchar(255) DEFAULT NULL,
`tags` varchar(255) DEFAULT NULL,
`title` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uidx01_app_info` (`app_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
@ -89,13 +95,11 @@ CREATE TABLE `instance_info` (
DROP TABLE IF EXISTS `job_info`;
CREATE TABLE `job_info` (
`id` bigint NOT NULL AUTO_INCREMENT,
`advanced_runtime_config` varchar(255) DEFAULT NULL,
`alarm_config` varchar(255) DEFAULT NULL,
`app_id` bigint DEFAULT NULL,
`concurrency` int DEFAULT NULL,
`designated_workers` varchar(255) DEFAULT NULL,
`dispatch_strategy` int DEFAULT NULL,
`dispatch_strategy_config` varchar(255) DEFAULT NULL,
`execute_type` int DEFAULT NULL,
`extra` varchar(255) DEFAULT NULL,
`gmt_create` datetime(6) DEFAULT NULL,
@ -125,6 +129,27 @@ CREATE TABLE `job_info` (
KEY `idx01_job_info` (`app_id`,`status`,`time_expression_type`,`next_trigger_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
-- ----------------------------
-- Table structure for namespace
-- ----------------------------
DROP TABLE IF EXISTS `namespace`;
CREATE TABLE `namespace` (
`id` bigint NOT NULL AUTO_INCREMENT,
`code` varchar(255) DEFAULT NULL,
`creator` bigint DEFAULT NULL,
`dept` varchar(255) DEFAULT NULL,
`extra` varchar(255) DEFAULT NULL,
`gmt_create` datetime(6) DEFAULT NULL,
`gmt_modified` datetime(6) DEFAULT NULL,
`modifier` bigint DEFAULT NULL,
`name` varchar(255) DEFAULT NULL,
`status` int DEFAULT NULL,
`tags` varchar(255) DEFAULT NULL,
`token` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uidx01_namespace` (`code`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
-- ----------------------------
-- Table structure for oms_lock
-- ----------------------------
@ -138,6 +163,40 @@ CREATE TABLE `oms_lock` (
`ownerip` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uidx01_oms_lock` (`lock_name`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
-- ----------------------------
-- Table structure for powerjob_files
-- ----------------------------
DROP TABLE IF EXISTS `powerjob_files`;
CREATE TABLE `powerjob_files` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT 'ID',
`bucket` varchar(255) NOT NULL COMMENT '分桶',
`name` varchar(255) NOT NULL COMMENT '文件名称',
`version` varchar(255) NOT NULL COMMENT '版本',
`meta` varchar(255) DEFAULT NULL COMMENT '元数据',
`length` bigint NOT NULL COMMENT '长度',
`status` int NOT NULL COMMENT '状态',
`data` longblob NOT NULL COMMENT '文件内容',
`extra` varchar(255) DEFAULT NULL COMMENT '其他信息',
`gmt_create` datetime NOT NULL COMMENT '创建时间',
`gmt_modified` datetime DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
-- ----------------------------
-- Table structure for pwjb_user_info
-- ----------------------------
DROP TABLE IF EXISTS `pwjb_user_info`;
CREATE TABLE `pwjb_user_info` (
`id` bigint NOT NULL AUTO_INCREMENT,
`extra` varchar(255) DEFAULT NULL,
`gmt_create` datetime(6) DEFAULT NULL,
`gmt_modified` datetime(6) DEFAULT NULL,
`password` varchar(255) DEFAULT NULL,
`username` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uidx01_username` (`username`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
-- ----------------------------
@ -154,24 +213,61 @@ CREATE TABLE `server_info` (
KEY `idx01_server_info` (`gmt_modified`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
-- ----------------------------
-- Table structure for sundry
-- ----------------------------
DROP TABLE IF EXISTS `sundry`;
CREATE TABLE `sundry` (
`id` bigint NOT NULL AUTO_INCREMENT,
`content` varchar(255) DEFAULT NULL,
`extra` varchar(255) DEFAULT NULL,
`gmt_create` datetime(6) DEFAULT NULL,
`gmt_modified` datetime(6) DEFAULT NULL,
`pkey` varchar(255) DEFAULT NULL,
`skey` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uidx01_sundry` (`pkey`,`skey`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
-- ----------------------------
-- Table structure for user_info
-- ----------------------------
DROP TABLE IF EXISTS `user_info`;
CREATE TABLE `user_info` (
`id` bigint NOT NULL AUTO_INCREMENT,
`account_type` varchar(255) DEFAULT NULL,
`email` varchar(255) DEFAULT NULL,
`extra` varchar(255) DEFAULT NULL,
`gmt_create` datetime(6) DEFAULT NULL,
`gmt_modified` datetime(6) DEFAULT NULL,
`nick` varchar(255) DEFAULT NULL,
`origin_username` varchar(255) DEFAULT NULL,
`password` varchar(255) DEFAULT NULL,
`phone` varchar(255) DEFAULT NULL,
`token_login_verify_info` varchar(255) DEFAULT NULL,
`username` varchar(255) DEFAULT NULL,
`web_hook` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `uidx01_user_info` (`username`),
UNIQUE KEY `uidx01_user_name` (`username`),
KEY `uidx02_user_info` (`email`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
-- ----------------------------
-- Table structure for user_role
-- ----------------------------
DROP TABLE IF EXISTS `user_role`;
CREATE TABLE `user_role` (
`id` bigint NOT NULL AUTO_INCREMENT,
`extra` varchar(255) DEFAULT NULL,
`gmt_create` datetime(6) DEFAULT NULL,
`gmt_modified` datetime(6) DEFAULT NULL,
`role` int DEFAULT NULL,
`scope` int DEFAULT NULL,
`target` bigint DEFAULT NULL,
`user_id` bigint DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `uidx01_user_id` (`user_id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
-- ----------------------------
-- Table structure for workflow_info

View File

@ -0,0 +1,88 @@
-- Upgrade SQL FROM 4.1.x to 4.2.x
-- ----------------------------
-- Table change for app_info
-- ----------------------------
SET FOREIGN_KEY_CHECKS=0;
ALTER TABLE `app_info` ADD COLUMN `creator` bigint NULL DEFAULT NULL;
ALTER TABLE `app_info` ADD COLUMN `extra` varchar(255) NULL DEFAULT NULL;
ALTER TABLE `app_info` ADD COLUMN `modifier` bigint NULL DEFAULT NULL;
ALTER TABLE `app_info` ADD COLUMN `namespace_id` bigint NULL DEFAULT NULL;
ALTER TABLE `app_info` ADD COLUMN `tags` varchar(255) NULL DEFAULT NULL;
ALTER TABLE `app_info` ADD COLUMN `title` varchar(255) NULL DEFAULT NULL;
-- ----------------------------
-- Table change for user_info
-- ----------------------------
ALTER TABLE `user_info` ADD COLUMN `account_type` varchar(255) NULL DEFAULT NULL;
ALTER TABLE `user_info` ADD COLUMN `nick` varchar(255) NULL DEFAULT NULL;
ALTER TABLE `user_info` ADD COLUMN `origin_username` varchar(255) NULL DEFAULT NULL;
ALTER TABLE `user_info` ADD COLUMN `token_login_verify_info` varchar(255) NULL DEFAULT NULL;
ALTER TABLE `user_info` ADD UNIQUE INDEX `uidx01_user_name`(`username` ASC) USING BTREE;
-- ----------------------------
-- new table 'namespace'
-- ----------------------------
CREATE TABLE `namespace` (
`id` bigint NOT NULL AUTO_INCREMENT,
`code` varchar(255) NULL DEFAULT NULL,
`creator` bigint NULL DEFAULT NULL,
`dept` varchar(255) NULL DEFAULT NULL,
`extra` varchar(255) NULL DEFAULT NULL,
`gmt_create` datetime(6) NULL DEFAULT NULL,
`gmt_modified` datetime(6) NULL DEFAULT NULL,
`modifier` bigint NULL DEFAULT NULL,
`name` varchar(255) NULL DEFAULT NULL,
`status` int NULL DEFAULT NULL,
`tags` varchar(255) NULL DEFAULT NULL,
`token` varchar(255) NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `uidx01_namespace`(`code` ASC) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- new table 'pwjb_user_info'
-- ----------------------------
CREATE TABLE `pwjb_user_info` (
`id` bigint NOT NULL AUTO_INCREMENT,
`extra` varchar(255) NULL DEFAULT NULL,
`gmt_create` datetime(6) NULL DEFAULT NULL,
`gmt_modified` datetime(6) NULL DEFAULT NULL,
`password` varchar(255) NULL DEFAULT NULL,
`username` varchar(255) NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `uidx01_username`(`username` ASC) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- new table 'sundry'
-- ----------------------------
CREATE TABLE `sundry` (
`id` bigint NOT NULL AUTO_INCREMENT,
`content` varchar(255) NULL DEFAULT NULL,
`extra` varchar(255) NULL DEFAULT NULL,
`gmt_create` datetime(6) NULL DEFAULT NULL,
`gmt_modified` datetime(6) NULL DEFAULT NULL,
`pkey` varchar(255) NULL DEFAULT NULL,
`skey` varchar(255) NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `uidx01_sundry`(`pkey` ASC, `skey` ASC) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- new table 'user_role'
-- ----------------------------
CREATE TABLE `user_role` (
`id` bigint NOT NULL AUTO_INCREMENT,
`extra` varchar(255) NULL DEFAULT NULL,
`gmt_create` datetime(6) NULL DEFAULT NULL,
`gmt_modified` datetime(6) NULL DEFAULT NULL,
`role` int NULL DEFAULT NULL,
`scope` int NULL DEFAULT NULL,
`target` bigint NULL DEFAULT NULL,
`user_id` bigint NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE,
INDEX `uidx01_user_id`(`user_id` ASC) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;

View File

@ -6,7 +6,7 @@
<groupId>tech.powerjob</groupId>
<artifactId>powerjob</artifactId>
<version>4.3.9</version>
<version>5.0.0-beta2</version>
<packaging>pom</packaging>
<name>powerjob</name>
<url>http://www.powerjob.tech</url>

View File

@ -5,18 +5,18 @@
<parent>
<artifactId>powerjob</artifactId>
<groupId>tech.powerjob</groupId>
<version>4.3.9</version>
<version>5.0.0-beta2</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>powerjob-client</artifactId>
<version>4.3.9</version>
<version>5.0.0-beta2</version>
<packaging>jar</packaging>
<properties>
<junit.version>5.9.1</junit.version>
<fastjson.version>1.2.83</fastjson.version>
<powerjob.common.version>4.3.9</powerjob.common.version>
<powerjob.common.version>5.0.0-beta2</powerjob.common.version>
<mvn.shade.plugin.version>3.2.4</mvn.shade.plugin.version>
</properties>

View File

@ -5,12 +5,12 @@
<parent>
<artifactId>powerjob</artifactId>
<groupId>tech.powerjob</groupId>
<version>4.3.9</version>
<version>5.0.0-beta2</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>powerjob-common</artifactId>
<version>4.3.9</version>
<version>5.0.0-beta2</version>
<packaging>jar</packaging>
<properties>

View File

@ -30,4 +30,6 @@ public class OmsConstant {
public static final String HTTP_HEADER_CONTENT_TYPE = "Content-Type";
public static final String JSON_MEDIA_TYPE = "application/json; charset=utf-8";
public static final String NULL = "null";
}

View File

@ -1,13 +1,20 @@
package tech.powerjob.common.exception;
import lombok.Getter;
import lombok.Setter;
/**
* PowerJob 运行时异常
*
* @author tjq
* @since 2020/5/26
*/
@Setter
@Getter
public class PowerJobException extends RuntimeException {
protected String code;
public PowerJobException() {
}

View File

@ -8,6 +8,11 @@ import org.apache.commons.lang3.exception.ExceptionUtils;
/**
* The result object returned by the request
* <p>
* 低版本由于 Jackson 序列化配置问题导致无法在此对象上新增任何字段了否则会报错 com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException: Unrecognized field "code" (class tech.powerjob.common.response.ObjectResultDTO), not marked as ignorable (3 known properties: "data", "success", "message"])
* at [Source: (String)"{"success":true,"code":null,"data":2,"message":null}"; line: 1, column: 28] (through reference chain: tech.powerjob.common.response.ObjectResultDTO["code"])
* <p>
* 短期内所有的新增字段需求都通过新对象继承实现
*
* @author tjq
* @since 2020/3/30
@ -17,9 +22,9 @@ import org.apache.commons.lang3.exception.ExceptionUtils;
@ToString
public class ResultDTO<T> implements PowerSerializable {
private boolean success;
private T data;
private String message;
protected boolean success;
protected T data;
protected String message;
public static <T> ResultDTO<T> success(T data) {
ResultDTO<T> r = new ResultDTO<>();

View File

@ -7,6 +7,7 @@ import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.time.DateFormatUtils;
import java.util.Collection;
import java.util.Date;
import java.util.UUID;
import java.util.function.Supplier;
@ -147,6 +148,13 @@ public class CommonUtils {
return OmsConstant.NONE;
}
public static String formatTime(Date date) {
if (date == null) {
return OmsConstant.NONE;
}
return formatTime(date.getTime());
}
/**
* 格式化字符串如果是 null 或空则显示 N/A
* @param str 字符串

View File

@ -5,12 +5,12 @@
<parent>
<artifactId>powerjob</artifactId>
<groupId>tech.powerjob</groupId>
<version>4.3.9</version>
<version>5.0.0-beta2</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>powerjob-official-processors</artifactId>
<version>4.3.9</version>
<version>5.0.0-beta2</version>
<packaging>jar</packaging>
<properties>
@ -20,7 +20,7 @@
<!-- 不会被打包的部分scope 只能是 test 或 provide -->
<junit.version>5.9.1</junit.version>
<logback.version>1.2.13</logback.version>
<powerjob.worker.version>4.3.9</powerjob.worker.version>
<powerjob.worker.version>5.0.0-beta2</powerjob.worker.version>
<h2.db.version>2.2.224</h2.db.version>
<mysql.version>8.0.28</mysql.version>
<spring.version>5.3.31</spring.version>

View File

@ -5,7 +5,7 @@
<parent>
<artifactId>powerjob</artifactId>
<groupId>tech.powerjob</groupId>
<version>4.3.9</version>
<version>5.0.0-beta2</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<packaging>pom</packaging>

View File

@ -5,7 +5,7 @@
<parent>
<artifactId>powerjob-remote</artifactId>
<groupId>tech.powerjob</groupId>
<version>4.3.9</version>
<version>5.0.0-beta2</version>
</parent>
<modelVersion>4.0.0</modelVersion>
@ -21,8 +21,8 @@
<logback.version>1.2.13</logback.version>
<springboot.version>2.7.18</springboot.version>
<powerjob-remote-impl-http.version>4.3.9</powerjob-remote-impl-http.version>
<powerjob-remote-impl-akka.version>4.3.9</powerjob-remote-impl-akka.version>
<powerjob-remote-impl-http.version>5.0.0-beta2</powerjob-remote-impl-http.version>
<powerjob-remote-impl-akka.version>5.0.0-beta2</powerjob-remote-impl-akka.version>
<gatling.version>3.9.0</gatling.version>
<gatling-maven-plugin.version>4.2.9</gatling-maven-plugin.version>

View File

@ -5,11 +5,11 @@
<parent>
<artifactId>powerjob-remote</artifactId>
<groupId>tech.powerjob</groupId>
<version>4.3.9</version>
<version>5.0.0-beta2</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<version>4.3.9</version>
<version>5.0.0-beta2</version>
<artifactId>powerjob-remote-framework</artifactId>
<properties>
@ -17,7 +17,7 @@
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<powerjob-common.version>4.3.9</powerjob-common.version>
<powerjob-common.version>5.0.0-beta2</powerjob-common.version>
<reflections.version>0.10.2</reflections.version>

View File

@ -5,19 +5,19 @@
<parent>
<artifactId>powerjob-remote</artifactId>
<groupId>tech.powerjob</groupId>
<version>4.3.9</version>
<version>5.0.0-beta2</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>powerjob-remote-impl-akka</artifactId>
<version>4.3.9</version>
<version>5.0.0-beta2</version>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<powerjob-remote-framework.version>4.3.9</powerjob-remote-framework.version>
<powerjob-remote-framework.version>5.0.0-beta2</powerjob-remote-framework.version>
<akka.version>2.6.13</akka.version>
</properties>

View File

@ -5,12 +5,12 @@
<parent>
<artifactId>powerjob-remote</artifactId>
<groupId>tech.powerjob</groupId>
<version>4.3.9</version>
<version>5.0.0-beta2</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>powerjob-remote-impl-http</artifactId>
<version>4.3.9</version>
<version>5.0.0-beta2</version>
<properties>
<maven.compiler.source>8</maven.compiler.source>
@ -18,7 +18,7 @@
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<vertx.version>4.3.7</vertx.version>
<powerjob-remote-framework.version>4.3.9</powerjob-remote-framework.version>
<powerjob-remote-framework.version>5.0.0-beta2</powerjob-remote-framework.version>
</properties>
<dependencies>

View File

@ -5,12 +5,12 @@
<parent>
<artifactId>powerjob</artifactId>
<groupId>tech.powerjob</groupId>
<version>4.3.9</version>
<version>5.0.0-beta2</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>powerjob-server</artifactId>
<version>4.3.9</version>
<version>5.0.0-beta2</version>
<packaging>pom</packaging>
<modules>
@ -22,6 +22,7 @@
<module>powerjob-server-migrate</module>
<module>powerjob-server-core</module>
<module>powerjob-server-monitor</module>
<module>powerjob-server-auth</module>
</modules>
@ -50,12 +51,12 @@
<groovy.version>3.0.10</groovy.version>
<cron-utils.version>9.2.1</cron-utils.version>
<powerjob-common.version>4.3.9</powerjob-common.version>
<powerjob-remote-impl-http.version>4.3.9</powerjob-remote-impl-http.version>
<powerjob-remote-impl-akka.version>4.3.9</powerjob-remote-impl-akka.version>
<powerjob-common.version>5.0.0-beta2</powerjob-common.version>
<powerjob-remote-impl-http.version>5.0.0-beta2</powerjob-remote-impl-http.version>
<powerjob-remote-impl-akka.version>5.0.0-beta2</powerjob-remote-impl-akka.version>
<springdoc-openapi-ui.version>1.6.14</springdoc-openapi-ui.version>
<aliyun-sdk-oss.version>3.17.1</aliyun-sdk-oss.version>
<aws-java-sdk-s3.version>1.12.665</aws-java-sdk-s3.version>
<minio.version>8.5.2</minio.version>
<commons-collections4.version>4.4</commons-collections4.version>
</properties>
@ -96,6 +97,11 @@
<artifactId>powerjob-server-migrate</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>tech.powerjob</groupId>
<artifactId>powerjob-server-auth</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>tech.powerjob</groupId>
<artifactId>powerjob-server-starter</artifactId>
@ -114,13 +120,12 @@
<artifactId>aliyun-sdk-oss</artifactId>
<version>${aliyun-sdk-oss.version}</version>
</dependency>
<!-- 存储扩展-Minio/S3,未使用可移除minio-client 依赖 OKHTTP4.x 版本,强制引入 kotlin 标准库,为了防止引入更多问题放弃) -->
<!-- 存储扩展-Minio,未使用可移除 -->
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-java-sdk-s3</artifactId>
<version>${aws-java-sdk-s3.version}</version>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
<version>${minio.version}</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-collections4 -->
<dependency>
<groupId>org.apache.commons</groupId>

View File

@ -0,0 +1,58 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>tech.powerjob</groupId>
<artifactId>powerjob-server</artifactId>
<version>5.0.0-beta2</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>powerjob-server-auth</artifactId>
<version>${project.parent.version}</version>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<jjwt.version>0.11.5</jjwt.version>
<dingtalk.version>1.1.86</dingtalk.version>
</properties>
<dependencies>
<dependency>
<groupId>tech.powerjob</groupId>
<artifactId>powerjob-server-persistence</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>${jjwt.version}</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>${jjwt.version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>${jjwt.version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>dingtalk</artifactId>
<version>${dingtalk.version}</version>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,48 @@
package tech.powerjob.server.auth;
/**
* LoginUserHolder
*
* @author tjq
* @since 2023/4/16
*/
public class LoginUserHolder {
private static final ThreadLocal<PowerJobUser> TL = new ThreadLocal<>();
public static PowerJobUser get() {
return TL.get();
}
public static void set(PowerJobUser powerJobUser) {
TL.set(powerJobUser);
}
public static void clean() {
TL.remove();
}
/**
* 获取用户名
* @return 存在则返回常规用户名否则返回 unknown
*/
public static String getUserName() {
PowerJobUser powerJobUser = get();
if (powerJobUser != null) {
return powerJobUser.getUsername();
}
return "UNKNOWN";
}
/**
* 获取用户ID
* @return 存在则返回否则返回 null
*/
public static Long getUserId() {
PowerJobUser powerJobUser = get();
if (powerJobUser != null) {
return powerJobUser.getId();
}
return null;
}
}

View File

@ -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;
}

View File

@ -0,0 +1,44 @@
package tech.powerjob.server.auth;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import java.io.Serializable;
/**
* PowerJob 登陆用户
*
* @author tjq
* @since 2023/3/20
*/
@Getter
@Setter
@ToString
public class PowerJobUser implements Serializable {
private Long id;
private String username;
/**
* 手机号
*/
private String phone;
/**
* 邮箱地址
*/
private String email;
/**
* webHook
*/
private String webHook;
/**
* 扩展字段
*/
private String extra;
/* ************** 以上为数据库字段 ************** */
private String jwtToken;
}

View File

@ -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<Permission> permissions;
public static Role of(int vv) {
for (Role role : values()) {
if (vv == role.v) {
return role;
}
}
throw new IllegalArgumentException("unknown role: " + vv);
}
}

View File

@ -0,0 +1,40 @@
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;
public static RoleScope of(int vv) {
for (RoleScope rs : values()) {
if (vv == rs.v) {
return rs;
}
}
throw new IllegalArgumentException("unknown RoleScope: " + vv);
}
}

View File

@ -0,0 +1,52 @@
package tech.powerjob.server.auth.common;
/**
* 常量
*
* @author tjq
* @since 2024/2/11
*/
public class AuthConstants {
/* ********** 账号体系唯一标识推荐开发者接入第三方登录体系时也使用4位编码便于前端统一做样式 ********** */
/**
* PowerJob自建账号体系
*/
public static final String ACCOUNT_TYPE_POWER_JOB = "PWJB";
/**
* 钉钉
*/
public static final String ACCOUNT_TYPE_DING = "DING";
/**
* 企业微信预留蹲一个 contributor
*/
public static final String ACCOUNT_TYPE_WX = "QYWX";
/**
* 飞书预留蹲一个 contributor +1
*/
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";
/* ********** 账号体系 ********** */
/**
* JWT key
* 前端 header 默认首字母大写保持一致方便处理
*/
public static final String JWT_NAME = "Power_jwt";
/**
* 前端跳转到指定页面指令
*/
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;
}

View File

@ -0,0 +1,36 @@
package tech.powerjob.server.auth.common;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 鉴权错误信息
*
* @author tjq
* @since 2024/2/11
*/
@Getter
@AllArgsConstructor
public enum AuthErrorCode {
USER_NOT_LOGIN("-100", "UserNotLoggedIn"),
USER_NOT_EXIST("-101", "UserNotExist"),
USER_AUTH_FAILED("-102", "UserAuthFailed"),
NO_PERMISSION("-200", "NoPermission"),
/**
* 无效请求一般是参数问题
*/
INVALID_REQUEST("-300", "INVALID_REQUEST"),
INCORRECT_PASSWORD("-400", "INCORRECT_PASSWORD"),
INVALID_TOKEN("-401", "INVALID_TOKEN"),
;
private final String code;
private final String msg;
}

View File

@ -0,0 +1,23 @@
package tech.powerjob.server.auth.common;
import lombok.Getter;
import tech.powerjob.common.exception.PowerJobException;
/**
* 鉴权相关错误
*
* @author tjq
* @since 2024/2/10
*/
@Getter
public class PowerJobAuthException extends PowerJobException {
public PowerJobAuthException(AuthErrorCode errorCode) {
this(errorCode, null);
}
public PowerJobAuthException(AuthErrorCode errorCode, String extraMsg) {
super(extraMsg == null ? errorCode.getMsg() : errorCode.getMsg().concat(":").concat(extraMsg));
this.code = errorCode.getCode();
}
}

View File

@ -0,0 +1,27 @@
package tech.powerjob.server.auth.common.utils;
import tech.powerjob.common.OmsConstant;
import javax.servlet.http.HttpServletRequest;
/**
* HttpServletUtils
*
* @author tjq
* @since 2024/2/12
*/
public class HttpServletUtils {
public static String fetchFromHeader(String key, HttpServletRequest httpServletRequest) {
// headercookie 都能获取
String v = httpServletRequest.getHeader(key);
// 解决 window.localStorage.getItem null 的问题
if (OmsConstant.NULL.equalsIgnoreCase(v) || "undefined".equalsIgnoreCase(v)) {
return null;
}
return v;
}
}

View File

@ -0,0 +1,45 @@
package tech.powerjob.server.auth.interceptor;
import tech.powerjob.server.auth.Permission;
import tech.powerjob.server.auth.RoleScope;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* API 权限
*
* @author tjq
* @since 2023/3/20
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiPermission {
/**
* API 名称
* @return 空使用服务.方法名代替
*/
String name() default "";
RoleScope roleScope() default RoleScope.APP;
/**
* 需要的权限
* @return 权限
*/
Permission requiredPermission() default Permission.SU;
/**
* 固定权限不支持的场景需要使用动态权限
* @return 动态权限
*/
Class<? extends DynamicPermissionPlugin> dynamicPermissionPlugin() default EmptyPlugin.class;
/**
* 新增场景需要授权插件执行授权
* @return 授权插件
*/
Class<? extends GrantPermissionPlugin> grandPermissionPlugin() default EmptyPlugin.class;
}

View File

@ -0,0 +1,65 @@
package tech.powerjob.server.auth.interceptor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
/**
* ApiPermission 切面
* 主要用于执行授权插件完成创建后授权
*
* @author tjq
* @since 2024/2/11
*/
@Slf4j
@Aspect
@Component
public class ApiPermissionAspect {
@Pointcut("@annotation(ApiPermission)")
public void apiPermissionPointcut() {
// 定义切入点
}
/**
* 后置返回
* 如果第一个参数为JoinPoint则第二个参数为返回值的信息
* 如果第一个参数不为JoinPoint则第一个参数为returning中对应的参数
* returning限定了只有目标方法返回值与通知方法参数类型匹配时才能执行后置返回通知否则不执行
* 参数为Object类型将匹配任何目标返回值
* After注解标注的方法会在目标方法执行后运行无论目标方法是正常完成还是抛出异常它相当于finally块因为它总是执行所以适用于释放资源等清理活动@After注解不能访问目标方法的返回值
* AfterReturning注解标注的方法仅在目标方法成功执行后即正常返回运行它可以访问目标方法的返回值使用@AfterReturning可以在方法正常返回后执行一些逻辑比如对返回值进行处理或验证
*/
@AfterReturning(value = "apiPermissionPointcut()", returning = "result")
public void doAfterReturningAdvice1(JoinPoint joinPoint, Object result) {
// 入参
Object[] args = joinPoint.getArgs();
// 获取目标方法
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
ApiPermission annotationAnno = AnnotationUtils.getAnnotation(method, ApiPermission.class);
assert annotationAnno != null;
Class<? extends GrantPermissionPlugin> grandPermissionPluginClz = annotationAnno.grandPermissionPlugin();
try {
GrantPermissionPlugin grandPermissionPlugin = grandPermissionPluginClz.getDeclaredConstructor().newInstance();
grandPermissionPlugin.grant(args, result, method, joinPoint.getTarget());
} catch (Exception e) {
log.error("[ApiPermissionAspect] process ApiPermission grant failed", e);
ExceptionUtils.rethrow(e);
}
}
}

View File

@ -0,0 +1,15 @@
package tech.powerjob.server.auth.interceptor;
import tech.powerjob.server.auth.Permission;
import javax.servlet.http.HttpServletRequest;
/**
* 动态权限
*
* @author tjq
* @since 2023/9/3
*/
public interface DynamicPermissionPlugin {
Permission calculate(HttpServletRequest request, Object handler);
}

View File

@ -0,0 +1,24 @@
package tech.powerjob.server.auth.interceptor;
import tech.powerjob.server.auth.Permission;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
/**
*
*
* @author tjq
* @since 2024/2/12
*/
public class EmptyPlugin implements DynamicPermissionPlugin, GrantPermissionPlugin {
@Override
public Permission calculate(HttpServletRequest request, Object handler) {
return null;
}
@Override
public void grant(Object[] args, Object result, Method method, Object originBean) {
}
}

View File

@ -0,0 +1,21 @@
package tech.powerjob.server.auth.interceptor;
import java.lang.reflect.Method;
/**
* 授予权限插件
*
* @author tjq
* @since 2024/2/11
*/
public interface GrantPermissionPlugin {
/**
* 授权
* @param args 入参
* @param result 响应
* @param method 被调用方法
* @param originBean 原始对象
*/
void grant(Object[] args, Object result, Method method, Object originBean);
}

View File

@ -0,0 +1,135 @@
package tech.powerjob.server.auth.interceptor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import tech.powerjob.common.exception.ImpossibleException;
import tech.powerjob.common.exception.PowerJobException;
import tech.powerjob.server.auth.LoginUserHolder;
import tech.powerjob.server.auth.Permission;
import tech.powerjob.server.auth.PowerJobUser;
import tech.powerjob.server.auth.RoleScope;
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.service.login.PowerJobLoginService;
import tech.powerjob.server.auth.service.permission.PowerJobPermissionService;
import tech.powerjob.server.common.Loggers;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;
import java.util.Optional;
/**
* login auth and permission check
*
* @author tjq
* @since 2023/3/25
*/
@Slf4j
@Component
public class PowerJobAuthInterceptor implements HandlerInterceptor {
@Resource
private PowerJobLoginService powerJobLoginService;
@Resource
private PowerJobPermissionService powerJobPermissionService;
@Override
public boolean preHandle(@NonNull HttpServletRequest request,@NonNull HttpServletResponse response,@NonNull Object handler) throws Exception {
if (!(handler instanceof HandlerMethod)) {
return true;
}
HandlerMethod handlerMethod = (HandlerMethod) handler;
final Method method = handlerMethod.getMethod();
final ApiPermission apiPermissionAnno = method.getAnnotation(ApiPermission.class);
// 无注解代表不需要权限无需登陆直接访问
if (apiPermissionAnno == null) {
return true;
}
// 尝试直接解析登陆
final Optional<PowerJobUser> loginUserOpt = powerJobLoginService.ifLogin(request);
// 未登录直接报错返回固定状态码前端拦截后跳转到登录页
if (!loginUserOpt.isPresent()) {
throw new PowerJobAuthException(AuthErrorCode.USER_NOT_LOGIN);
}
// 登陆用户进行权限校验
final PowerJobUser powerJobUser = loginUserOpt.get();
// 写入上下文
LoginUserHolder.set(powerJobUser);
Permission requiredPermission = parsePermission(request, handler, apiPermissionAnno);
RoleScope roleScope = apiPermissionAnno.roleScope();
Long targetId = null;
if (RoleScope.NAMESPACE.equals(roleScope)) {
final String namespaceIdStr = HttpServletUtils.fetchFromHeader("NamespaceId", request);
if (StringUtils.isNotEmpty(namespaceIdStr)) {
targetId = Long.valueOf(namespaceIdStr);
}
}
if (RoleScope.APP.equals(roleScope)) {
final String appIdStr = HttpServletUtils.fetchFromHeader("AppId", request);
if (StringUtils.isNotEmpty(appIdStr)) {
targetId = Long.valueOf(appIdStr);
}
}
final boolean hasPermission = powerJobPermissionService.hasPermission(powerJobUser.getId(), roleScope, targetId, requiredPermission);
if (hasPermission) {
return true;
}
final String resourceName = parseResourceName(apiPermissionAnno, handlerMethod);
Loggers.WEB.info("[PowerJobAuthInterceptor] user[{}] has no permission to access: {}", powerJobUser.getUsername(), resourceName);
throw new PowerJobException("Permission denied!");
}
@Override
public void afterCompletion(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull Object handler, Exception ex) throws Exception {
LoginUserHolder.clean();
}
private static String parseResourceName(ApiPermission apiPermission, HandlerMethod handlerMethod) {
final String name = apiPermission.name();
if (StringUtils.isNotEmpty(name)) {
return name;
}
try {
final String clzName = handlerMethod.getBean().getClass().getSimpleName();
final String methodName = handlerMethod.getMethod().getName();
return String.format("%s_%s", clzName, methodName);
} catch (Exception ignore) {
}
return "UNKNOWN";
}
private static Permission parsePermission(HttpServletRequest request, Object handler, ApiPermission apiPermission) {
Class<? extends DynamicPermissionPlugin> dynamicPermissionPlugin = apiPermission.dynamicPermissionPlugin();
if (EmptyPlugin.class.equals(dynamicPermissionPlugin)) {
return apiPermission.requiredPermission();
}
try {
DynamicPermissionPlugin dynamicPermission = dynamicPermissionPlugin.getDeclaredConstructor().newInstance();
return dynamicPermission.calculate(request, handler);
} catch (Throwable t) {
log.error("[PowerJobAuthService] process dynamicPermissionPlugin failed!", t);
ExceptionUtils.rethrow(t);
}
throw new ImpossibleException();
}
}

View File

@ -0,0 +1,16 @@
package tech.powerjob.server.auth.jwt;
import java.util.Map;
/**
* JWT 服务
*
* @author tjq
* @since 2023/3/20
*/
public interface JwtService {
String build(Map<String, Object> body, String extraSk);
Map<String, Object> parse(String jwt, String extraSk);
}

View File

@ -0,0 +1,13 @@
package tech.powerjob.server.auth.jwt;
/**
* JWT 安全性的核心
* 对安全性有要求的接入方可以自行重新该方法自定义自己的安全 token 生成策略
*
* @author tjq
* @since 2023/3/20
*/
public interface SecretProvider {
String fetchSecretKey();
}

View File

@ -0,0 +1,47 @@
package tech.powerjob.server.auth.jwt.impl;
import lombok.extern.slf4j.Slf4j;
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 javax.annotation.Resource;
/**
* PowerJob 默认实现
*
* @author tjq
* @since 2023/3/20
*/
@Slf4j
@Component
public class DefaultSecretProvider implements SecretProvider {
@Resource
private Environment environment;
private static final String PROPERTY_KEY = "spring.datasource.core.jdbc-url";
@Override
public String fetchSecretKey() {
// 考虑到大部分用户都是开箱即用此处还是提供一个相对安全的默认实现JDBC URL 部署时必会改skey 不固定更安全
try {
String propertyValue = environment.getProperty(PROPERTY_KEY);
if (StringUtils.isNotEmpty(propertyValue)) {
String md5 = DigestUtils.md5(propertyValue);
log.debug("[DefaultSecretProvider] propertyValue: {} ==> md5: {}", propertyValue, md5);
if (StringUtils.isNotEmpty(md5)) {
return md5;
}
}
} catch (Exception ignore) {
}
return "ZQQZJ";
}
}

View File

@ -0,0 +1,92 @@
package tech.powerjob.server.auth.jwt.impl;
import com.google.common.collect.Maps;
import io.jsonwebtoken.*;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import tech.powerjob.server.auth.jwt.JwtService;
import tech.powerjob.server.auth.jwt.SecretProvider;
import javax.annotation.Resource;
import java.security.Key;
import java.util.Date;
import java.util.Map;
import java.util.UUID;
/**
* JWT 默认实现
*
* @author tjq
* @since 2023/3/20
*/
@Service
public class JwtServiceImpl implements JwtService {
@Resource
private SecretProvider secretProvider;
/**
* JWT 客户端过期时间
*/
@Value("${oms.auth.security.jwt.expire-seconds:604800}")
private int jwtExpireTime;
/**
* <a href="https://music.163.com/#/song?id=167975">GoodSong</a>
*/
private static final String BASE_SECURITY =
"CengMengXiangZhangJianZouTianYa" +
"KanYiKanShiJieDeFanHua" +
"NianShaoDeXinZongYouXieQingKuang" +
"RuJinWoSiHaiWeiJia"
;
@Override
public String build(Map<String, Object> body, String extraSk) {
final String secret = fetchSk(extraSk);
return innerBuild(secret, jwtExpireTime, body);
}
static String innerBuild(String secret, int expireSeconds, Map<String, Object> body) {
JwtBuilder jwtBuilder = Jwts.builder()
.setHeaderParam("typ", "JWT")
.addClaims(body)
.setSubject("PowerJob")
.setExpiration(new Date(System.currentTimeMillis() + 1000L * expireSeconds))
.setId(UUID.randomUUID().toString())
.signWith(genSecretKey(secret), SignatureAlgorithm.HS256);
return jwtBuilder.compact();
}
@Override
public Map<String, Object> parse(String jwt, String extraSk) {
return innerParse(fetchSk(extraSk), jwt);
}
private String fetchSk(String extraSk) {
if (StringUtils.isEmpty(extraSk)) {
return secretProvider.fetchSecretKey();
}
return secretProvider.fetchSecretKey().concat(extraSk);
}
static Map<String, Object> innerParse(String secret, String jwtStr) {
final Jws<Claims> claimsJws = Jwts.parserBuilder()
.setSigningKey(genSecretKey(secret))
.build()
.parseClaimsJws(jwtStr);
Map<String, Object> ret = Maps.newHashMap();
ret.putAll(claimsJws.getBody());
return ret;
}
private static Key genSecretKey(String secret) {
byte[] keyBytes = Decoders.BASE64.decode(BASE_SECURITY.concat(secret));
return Keys.hmacShaKeyFor(keyBytes);
}
}

View File

@ -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;
}

View File

@ -0,0 +1,24 @@
package tech.powerjob.server.auth.login;
import lombok.Data;
import lombok.experimental.Accessors;
import javax.servlet.http.HttpServletRequest;
/**
* 第三方登录请求
*
* @author tjq
* @since 2024/2/10
*/
@Data
@Accessors(chain = true)
public class ThirdPartyLoginRequest {
/**
* 原始参数给第三方登录方式一个服务端和前端交互的数据通道PowerJob 本身不感知其中的内容
*/
private String originParams;
private transient HttpServletRequest httpServletRequest;
}

View File

@ -0,0 +1,42 @@
package tech.powerjob.server.auth.login;
import javax.servlet.http.HttpServletRequest;
/**
* 第三方登录服务
*
* @author tjq
* @since 2024/2/10
*/
public interface ThirdPartyLoginService {
/**
* 登陆服务的类型
* @return 登陆服务类型比如 PowerJob / DingTalk
*/
LoginTypeInfo loginType();
/**
* 生成登陆的重定向 URL
* @param httpServletRequest http请求
* @return 重定向地址
*/
String generateLoginUrl(HttpServletRequest httpServletRequest);
/**
* 执行第三方登录
* @param loginRequest 上下文
* @return 登录地址
*/
ThirdPartyUser login(ThirdPartyLoginRequest loginRequest);
/**
* JWT 登录的回调校验
* @param username 用户名称
* @param tokenLoginVerifyInfo 二次校验信息
* @return 是否通过
*/
default boolean tokenLoginVerify(String username, TokenLoginVerifyInfo tokenLoginVerifyInfo) {
return true;
}
}

View File

@ -0,0 +1,47 @@
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;
/**
* JWT 登录的二次校验配置
* 可空空则代表放弃二次校验会出现第三方登录改了密码当 PowerJob JWT 登录依然可用的情况
*/
private TokenLoginVerifyInfo tokenLoginVerifyInfo;
/* ******** 以下全部选填即可,只是方便数据同步,后续都可以去 PowerJob 控制台更改 ******** */
/**
* 用户昵称
*/
private String nick;
/**
* 手机号
*/
private String phone;
/**
* 邮箱地址
*/
private String email;
/**
* web 回调地址
*/
private String webHook;
/**
* 扩展字段
*/
private String extra;
}

View File

@ -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;
}

View File

@ -0,0 +1,142 @@
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 org.springframework.stereotype.Service;
import tech.powerjob.common.exception.PowerJobException;
import tech.powerjob.server.auth.common.AuthConstants;
import tech.powerjob.server.auth.login.*;
import tech.powerjob.server.common.Loggers;
import javax.servlet.http.HttpServletRequest;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
/**
* <a href="https://open.dingtalk.com/document/orgapp/tutorial-obtaining-user-personal-information">钉钉账号体系登录第三方网站</a>
* PowerJob 官方支持钉钉账号体系登录原因
* 1. 钉钉作为当下用户体量最大的企业级办公软件覆盖率足够高提供钉钉支持能让更多开发者开箱即用
* 2. 钉钉的 API 设计和 PowerJob 设想一致算是个最佳实践其他企业内部的账号体系可参考这套流程进行接入
* - PowerJob 重定向到第三方账号体系登陆页 -> 第三方完成登陆 -> 跳转回调 PowerJob auth 接口 -> PowerJob 解析回调登陆信息完整用户关联
*
* @author tjq
* @since 2023/3/26
*/
@Service
public class DingTalkLoginService implements ThirdPartyLoginService {
/*
配置示例
oms.auth.dingtalk.appkey=dinggzqqzqqzqqzqq
oms.auth.dingtalk.appSecret=iY-FS8mzqqzqq_xEizqqzqqzqqzqqzqqzqqYEbkZOal
oms.auth.dingtalk.callbackUrl=http://localhost:7700
*/
/**
* 钉钉应用 AppKey
*/
@Value("${oms.auth.dingtalk.appkey:#{null}}")
private String dingTalkAppKey;
/**
* 钉钉应用 AppSecret
*/
@Value("${oms.auth.dingtalk.appSecret:#{null}}")
private String dingTalkAppSecret;
/**
* 回调地址powerjob 前端控制台地址 powerjob-console 地址
* 比如本地调试时为 <a href="http://localhost:7700">LocalDemoCallbackUrl</a>
* 部署后则为 <a href="http://try.powerjob.tech">demoCallBackUrl</a>
*/
@Value("${oms.auth.dingtalk.callbackUrl:#{null}}")
private String dingTalkCallbackUrl;
@Override
public LoginTypeInfo loginType() {
return new LoginTypeInfo()
.setType(AuthConstants.ACCOUNT_TYPE_DING)
.setName("DingTalk")
;
}
@Override
@SneakyThrows
public String generateLoginUrl(HttpServletRequest httpServletRequest) {
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=" + AuthConstants.ACCOUNT_TYPE_DING +
"&prompt=consent";
Loggers.WEB.info("[DingTalkBizLoginService] login url: {}", url);
return url;
}
@Override
@SneakyThrows
public ThirdPartyUser login(ThirdPartyLoginRequest loginRequest) {
try {
com.aliyun.dingtalkoauth2_1_0.Client client = authClient();
GetUserTokenRequest getUserTokenRequest = new GetUserTokenRequest()
//应用基础信息-应用信息的AppKey,请务必替换为开发的应用AppKey
.setClientId(dingTalkAppKey)
//应用基础信息-应用信息的AppSecret,请务必替换为开发的应用AppSecret
.setClientSecret(dingTalkAppSecret)
.setCode(loginRequest.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();
}
}

View File

@ -0,0 +1,121 @@
package tech.powerjob.server.auth.login.impl;
import org.apache.commons.collections4.MapUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import tech.powerjob.common.exception.PowerJobException;
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.*;
import tech.powerjob.server.common.Loggers;
import tech.powerjob.server.common.utils.DigestUtils;
import tech.powerjob.server.persistence.remote.model.PwjbUserInfoDO;
import tech.powerjob.server.persistence.remote.repository.PwjbUserInfoRepository;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.util.Map;
import java.util.Optional;
/**
* PowerJob 自带的登陆服务
* 和应用主框架无关依然属于第三方登录体系
*
* @author tjq
* @since 2023/3/20
*/
@Service
public class PwjbAccountLoginService implements ThirdPartyLoginService {
@Resource
private PwjbUserInfoRepository pwjbUserInfoRepository;
@Override
public LoginTypeInfo loginType() {
return new LoginTypeInfo()
.setType(AuthConstants.ACCOUNT_TYPE_POWER_JOB)
.setName("PowerJob Account")
;
}
@Override
public String generateLoginUrl(HttpServletRequest httpServletRequest) {
// 前端实现跳转服务端返回特殊指令
return AuthConstants.FE_REDIRECT_KEY.concat("powerjobLogin");
}
@Override
public ThirdPartyUser login(ThirdPartyLoginRequest loginRequest) {
final String loginInfo = loginRequest.getOriginParams();
if (StringUtils.isEmpty(loginInfo)) {
throw new IllegalArgumentException("can't find login Info");
}
Map<String, Object> loginInfoMap = JsonUtils.parseMap(loginInfo);
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!");
throw new PowerJobAuthException(AuthErrorCode.INVALID_REQUEST);
}
final Optional<PwjbUserInfoDO> userInfoOpt = pwjbUserInfoRepository.findByUsername(username);
if (!userInfoOpt.isPresent()) {
Loggers.WEB.debug("[PowerJobLoginService] can't find user by username: {}", username);
throw new PowerJobAuthException(AuthErrorCode.USER_NOT_EXIST);
}
final PwjbUserInfoDO dbUser = userInfoOpt.get();
if (DigestUtils.rePassword(password, username).equals(dbUser.getPassword())) {
ThirdPartyUser bizUser = new ThirdPartyUser();
bizUser.setUsername(username);
// 回填第一次创建的信息
String extra = dbUser.getExtra();
if (StringUtils.isNotEmpty(extra)) {
ThirdPartyUser material = JsonUtils.parseObjectIgnoreException(extra, ThirdPartyUser.class);
if (material != null) {
bizUser.setEmail(material.getEmail());
bizUser.setNick(material.getNick());
bizUser.setPhone(material.getPhone());
bizUser.setWebHook(material.getWebHook());
}
}
// 下发加密的密码作为 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<PwjbUserInfoDO> userInfoOpt = pwjbUserInfoRepository.findByUsername(username);
if (userInfoOpt.isPresent()) {
String dbPassword = userInfoOpt.get().getPassword();
return StringUtils.equals(dbPassword, tokenLoginVerifyInfo.getEncryptedToken());
}
return false;
}
}

View File

@ -0,0 +1,32 @@
package tech.powerjob.server.auth.service.login;
import lombok.Data;
import lombok.experimental.Accessors;
import javax.servlet.http.HttpServletRequest;
/**
* 执行登录的请求
*
* @author tjq
* @since 2024/2/10
*/
@Data
@Accessors(chain = true)
public class LoginRequest {
/**
* 登录类型
*/
private String loginType;
/**
* 原始参数给第三方登录方式一个服务端和前端交互的数据通道PowerJob 本身不感知其中的内容
*/
private String originParams;
/**
* http原始请求第三方回调参数传递类型无法枚举直接传递 HttpServletRequest 满足扩展性要求
*/
private transient HttpServletRequest httpServletRequest;
}

View File

@ -0,0 +1,48 @@
package tech.powerjob.server.auth.service.login;
import tech.powerjob.server.auth.PowerJobUser;
import tech.powerjob.server.auth.common.PowerJobAuthException;
import tech.powerjob.server.auth.login.LoginTypeInfo;
import javax.servlet.http.HttpServletRequest;
import java.util.List;
import java.util.Optional;
/**
* PowerJob 登录服务
*
* @author tjq
* @since 2024/2/10
*/
public interface PowerJobLoginService {
/**
* 获取全部可登录的类型
* @return 全部可登录类型
*/
List<LoginTypeInfo> fetchSupportLoginTypes();
/**
* 获取第三方登录链接
* @param loginType 登录类型
* @param httpServletRequest http请求
* @return 重定向地址
*/
String fetchThirdPartyLoginUrl(String loginType, HttpServletRequest httpServletRequest);
/**
* 执行真正的登录请求底层调用第三方登录服务完成登录
* @param loginRequest 登录请求
* @return 登录完成的 PowerJobUser
* @throws PowerJobAuthException 鉴权失败抛出异常
*/
PowerJobUser doLogin(LoginRequest loginRequest) throws PowerJobAuthException;
/**
* JWT 信息中解析用户登录信息
* @param httpServletRequest httpServletRequest
* @return PowerJob 用户
*/
Optional<PowerJobUser> ifLogin(HttpServletRequest httpServletRequest);
}

View File

@ -0,0 +1,236 @@
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;
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.*;
import tech.powerjob.server.auth.service.login.LoginRequest;
import tech.powerjob.server.auth.service.login.PowerJobLoginService;
import tech.powerjob.server.common.Loggers;
import tech.powerjob.server.persistence.remote.model.UserInfoDO;
import tech.powerjob.server.persistence.remote.repository.UserInfoRepository;
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;
import java.util.stream.Collectors;
/**
* PowerJob 登录服务
*
* @author tjq
* @since 2024/2/10
*/
@Slf4j
@Service
public class PowerJobLoginServiceImpl implements PowerJobLoginService {
private final JwtService jwtService;
private final UserInfoRepository userInfoRepository;
private final Map<String, ThirdPartyLoginService> code2ThirdPartyLoginService;
@Autowired
public PowerJobLoginServiceImpl(JwtService jwtService, UserInfoRepository userInfoRepository, List<ThirdPartyLoginService> thirdPartyLoginServices) {
this.jwtService = jwtService;
this.userInfoRepository = userInfoRepository;
code2ThirdPartyLoginService = Maps.newHashMap();
thirdPartyLoginServices.forEach(s -> {
code2ThirdPartyLoginService.put(s.loginType().getType(), s);
log.info("[PowerJobLoginService] register ThirdPartyLoginService: {}", s.loginType());
});
}
@Override
public List<LoginTypeInfo> fetchSupportLoginTypes() {
return Lists.newArrayList(code2ThirdPartyLoginService.values()).stream().map(ThirdPartyLoginService::loginType).collect(Collectors.toList());
}
@Override
public String fetchThirdPartyLoginUrl(String type, HttpServletRequest httpServletRequest) {
final ThirdPartyLoginService thirdPartyLoginService = fetchBizLoginService(type);
return thirdPartyLoginService.generateLoginUrl(httpServletRequest);
}
@Override
public PowerJobUser doLogin(LoginRequest loginRequest) throws PowerJobAuthException {
final String loginType = loginRequest.getLoginType();
final ThirdPartyLoginService thirdPartyLoginService = fetchBizLoginService(loginType);
ThirdPartyLoginRequest thirdPartyLoginRequest = new ThirdPartyLoginRequest()
.setOriginParams(loginRequest.getOriginParams())
.setHttpServletRequest(loginRequest.getHttpServletRequest());
final ThirdPartyUser bizUser = thirdPartyLoginService.login(thirdPartyLoginRequest);
String dbUserName = String.format("%s_%s", loginType, bizUser.getUsername());
Optional<UserInfoDO> powerJobUserOpt = userInfoRepository.findByUsername(dbUserName);
// 如果不存在用户先同步创建用户
if (!powerJobUserOpt.isPresent()) {
UserInfoDO newUser = new UserInfoDO();
newUser.setUsername(dbUserName);
// 写入账号体系类型
newUser.setAccountType(loginType);
newUser.setOriginUsername(bizUser.getUsername());
newUser.setTokenLoginVerifyInfo(JsonUtils.toJSONString(bizUser.getTokenLoginVerifyInfo()));
// 同步素材
newUser.setEmail(bizUser.getEmail());
newUser.setPhone(bizUser.getPhone());
newUser.setNick(bizUser.getNick());
newUser.setWebHook(bizUser.getWebHook());
newUser.setExtra(bizUser.getExtra());
Loggers.WEB.info("[PowerJobLoginService] sync user to PowerJobUserSystem: {}", dbUserName);
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();
// 理论上 100% 存在
if (powerJobUserOpt.isPresent()) {
final UserInfoDO dbUser = powerJobUserOpt.get();
BeanUtils.copyProperties(dbUser, ret);
ret.setUsername(dbUserName);
}
fillJwt(ret, Optional.ofNullable(bizUser.getTokenLoginVerifyInfo()).map(TokenLoginVerifyInfo::getEncryptedToken).orElse(null));
return ret;
}
@Override
public Optional<PowerJobUser> ifLogin(HttpServletRequest httpServletRequest) {
final Optional<JwtBody> jwtBodyOpt = parseJwt(httpServletRequest);
if (!jwtBodyOpt.isPresent()) {
return Optional.empty();
}
JwtBody jwtBody = jwtBodyOpt.get();
Optional<UserInfoDO> 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) {
final ThirdPartyLoginService loginService = code2ThirdPartyLoginService.get(loginType);
if (loginService == null) {
throw new PowerJobAuthException(AuthErrorCode.INVALID_REQUEST, "can't find ThirdPartyLoginService by type: " + loginType);
}
return loginService;
}
private void fillJwt(PowerJobUser powerJobUser, String encryptedToken) {
// 不能下发 userId容易被轮询爆破
JwtBody jwtBody = new JwtBody();
jwtBody.setUsername(powerJobUser.getUsername());
if (StringUtils.isNotEmpty(encryptedToken)) {
jwtBody.setEncryptedToken(encryptedToken);
}
Map<String, Object> jwtMap = JsonUtils.parseMap(JsonUtils.toJSONString(jwtBody));
powerJobUser.setJwtToken(jwtService.build(jwtMap, null));
}
@SneakyThrows
private Optional<JwtBody> parseJwt(HttpServletRequest httpServletRequest) {
// headercookie 都能获取
String jwtStr = HttpServletUtils.fetchFromHeader(AuthConstants.JWT_NAME, httpServletRequest);
/*
开发阶段跨域无法简单传输 cookies暂时采取 header 方案传输 JWT
if (StringUtils.isEmpty(jwtStr)) {
for (Cookie cookie : Optional.ofNullable(httpServletRequest.getCookies()).orElse(new Cookie[]{})) {
if (cookie.getName().equals(AuthConstants.JWT_NAME)) {
jwtStr = cookie.getValue();
}
}
}
*/
if (StringUtils.isEmpty(jwtStr)) {
return Optional.empty();
}
final Map<String, Object> jwtBodyMap = jwtService.parse(jwtStr, null);
if (MapUtils.isEmpty(jwtBodyMap)) {
return Optional.empty();
}
return Optional.ofNullable(JsonUtils.parseObject(JsonUtils.toJSONString(jwtBodyMap), JwtBody.class));
}
@Data
static class JwtBody implements Serializable {
private String username;
private String encryptedToken;
}
}

View File

@ -0,0 +1,63 @@
package tech.powerjob.server.auth.service.permission;
import tech.powerjob.server.auth.Permission;
import tech.powerjob.server.auth.Role;
import tech.powerjob.server.auth.RoleScope;
import java.util.List;
import java.util.Map;
/**
* PowerJob 鉴权服务
*
* @author tjq
* @since 2024/2/11
*/
public interface PowerJobPermissionService {
/**
* 判断用户是否有访问权限
* @param userId userId
* @param roleScope 权限范围
* @param target 权限目标ID
* @param permission 要求的权限
* @return 是否有权限
*/
boolean hasPermission(Long userId, RoleScope roleScope, Long target, Permission permission);
/**
* 授予用户角色
* @param roleScope 权限范围
* @param target 权限目标
* @param userId 用户ID
* @param role 角色
* @param extra 其他
*/
void grantRole(RoleScope roleScope, Long target, Long userId, Role role, String extra);
/**
* 回收用户角色
* @param roleScope 权限范围
* @param target 权限目标
* @param userId 用户ID
* @param role 角色
*/
void retrieveRole(RoleScope roleScope, Long target, Long userId, Role role);
/**
* 获取有相关权限的用户
* @param roleScope 角色范围
* @param target 目标
* @return 角色对应的用户列表
*/
Map<Role, List<Long>> fetchUserWithPermissions(RoleScope roleScope, Long target);
/**
* 获取用户有权限的目标
* @param roleScope 角色范围
* @param userId 用户ID
* @return result
*/
Map<Role, List<Long>> fetchUserHadPermissionTargets(RoleScope roleScope, Long userId);
}

View File

@ -0,0 +1,177 @@
package tech.powerjob.server.auth.service.permission;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimap;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import tech.powerjob.server.auth.Permission;
import tech.powerjob.server.auth.Role;
import tech.powerjob.server.auth.RoleScope;
import tech.powerjob.server.persistence.remote.model.AppInfoDO;
import tech.powerjob.server.persistence.remote.model.UserRoleDO;
import tech.powerjob.server.persistence.remote.repository.AppInfoRepository;
import tech.powerjob.server.persistence.remote.repository.UserRoleRepository;
import javax.annotation.Resource;
import java.util.*;
/**
* PowerJobPermissionService
*
* @author tjq
* @since 2024/2/11
*/
@Slf4j
@Service
public class PowerJobPermissionServiceImpl implements PowerJobPermissionService {
@Resource
private AppInfoRepository appInfoRepository;
@Resource
private UserRoleRepository userRoleRepository;
@Override
public boolean hasPermission(Long userId, RoleScope roleScope, Long target, Permission requiredPermission) {
final List<UserRoleDO> userRoleList = Optional.ofNullable(userRoleRepository.findAllByUserId(userId)).orElse(Collections.emptyList());
Multimap<Long, Role> appId2Role = ArrayListMultimap.create();
Multimap<Long, Role> namespaceId2Role = ArrayListMultimap.create();
List<Role> globalRoles = Lists.newArrayList();
for (UserRoleDO userRole : userRoleList) {
final Role role = Role.of(userRole.getRole());
// 处理全局权限
if (RoleScope.GLOBAL.getV() == userRole.getScope()) {
if (Role.ADMIN.equals(role)) {
return true;
}
globalRoles.add(role);
}
if (RoleScope.NAMESPACE.getV() == userRole.getScope()) {
namespaceId2Role.put(userRole.getTarget(), role);
}
if (RoleScope.APP.getV() == userRole.getScope()) {
appId2Role.put(userRole.getTarget(), role);
}
}
// 前置判断需要的权限新增场景还没有 appId or namespaceId
if (requiredPermission == Permission.NONE) {
return true;
}
// 检验全局穿透权限
for (Role role : globalRoles) {
if (role.getPermissions().contains(requiredPermission)) {
return true;
}
}
// 无超级管理员权限校验普通权限
if (RoleScope.APP.equals(roleScope)) {
return checkAppPermission(target, requiredPermission, appId2Role, namespaceId2Role);
}
if (RoleScope.NAMESPACE.equals(roleScope)) {
return checkNamespacePermission(target, requiredPermission, namespaceId2Role);
}
return false;
}
@Override
public void grantRole(RoleScope roleScope, Long target, Long userId, Role role, String extra) {
UserRoleDO userRoleDO = new UserRoleDO();
userRoleDO.setGmtCreate(new Date());
userRoleDO.setGmtModified(new Date());
userRoleDO.setExtra(extra);
userRoleDO.setScope(roleScope.getV());
userRoleDO.setTarget(target);
userRoleDO.setUserId(userId);
userRoleDO.setRole(role.getV());
userRoleRepository.saveAndFlush(userRoleDO);
log.info("[PowerJobPermissionService] [grantPermission] saveAndFlush userRole successfully: {}", userRoleDO);
}
@Override
public void retrieveRole(RoleScope roleScope, Long target, Long userId, Role role) {
List<UserRoleDO> originUserRole = userRoleRepository.findAllByScopeAndTargetAndRoleAndUserId(roleScope.getV(), target, role.getV(), userId);
log.info("[PowerJobPermissionService] [retrievePermission] origin rule: {}", originUserRole);
Optional.ofNullable(originUserRole).orElse(Collections.emptyList()).forEach(r -> {
userRoleRepository.deleteById(r.getId());
log.info("[PowerJobPermissionService] [retrievePermission] delete UserRole: {}", r);
});
}
@Override
public Map<Role, List<Long>> fetchUserWithPermissions(RoleScope roleScope, Long target) {
List<UserRoleDO> permissionUserList = userRoleRepository.findAllByScopeAndTarget(roleScope.getV(), target);
Map<Role, List<Long>> ret = Maps.newHashMap();
Optional.ofNullable(permissionUserList).orElse(Collections.emptyList()).forEach(userRoleDO -> {
Role role = Role.of(userRoleDO.getRole());
List<Long> userIds = ret.computeIfAbsent(role, ignore -> Lists.newArrayList());
userIds.add(userRoleDO.getUserId());
});
return ret;
}
@Override
public Map<Role, List<Long>> fetchUserHadPermissionTargets(RoleScope roleScope, Long userId) {
Map<Role, List<Long>> ret = Maps.newHashMap();
List<UserRoleDO> userRoleDOList = userRoleRepository.findAllByUserIdAndScope(userId, roleScope.getV());
Optional.ofNullable(userRoleDOList).orElse(Collections.emptyList()).forEach(r -> {
Role role = Role.of(r.getRole());
List<Long> targetIds = ret.computeIfAbsent(role, ignore -> Lists.newArrayList());
targetIds.add(r.getTarget());
});
return ret;
}
private boolean checkAppPermission(Long targetId, Permission requiredPermission, Multimap<Long, Role> appId2Role, Multimap<Long, Role> namespaceId2Role) {
final Collection<Role> appRoles = appId2Role.get(targetId);
for (Role role : appRoles) {
if (role.getPermissions().contains(requiredPermission)) {
return true;
}
}
// 校验 namespace 穿透权限
Optional<AppInfoDO> appInfoOpt = appInfoRepository.findById(targetId);
if (!appInfoOpt.isPresent()) {
throw new IllegalArgumentException("can't find appInfo by appId in permission check: " + targetId);
}
Long namespaceId = Optional.ofNullable(appInfoOpt.get().getNamespaceId()).orElse(-1L);
Collection<Role> namespaceRoles = namespaceId2Role.get(namespaceId);
for (Role role : namespaceRoles) {
if (role.getPermissions().contains(requiredPermission)) {
return true;
}
}
return false;
}
private boolean checkNamespacePermission(Long targetId, Permission requiredPermission, Multimap<Long, Role> namespaceId2Role) {
Collection<Role> namespaceRoles = namespaceId2Role.get(targetId);
for (Role role : namespaceRoles) {
if (role.getPermissions().contains(requiredPermission)) {
return true;
}
}
return false;
}
}

View File

@ -5,7 +5,7 @@
<parent>
<artifactId>powerjob-server</artifactId>
<groupId>tech.powerjob</groupId>
<version>4.3.9</version>
<version>5.0.0-beta2</version>
<relativePath>../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@ -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");
}

View File

@ -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<String, String> splitKvString(String kvString) {
return MAP_SPLITTER.split(kvString);
}
}

View File

@ -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));
}
}

View File

@ -5,7 +5,7 @@
<parent>
<artifactId>powerjob-server</artifactId>
<groupId>tech.powerjob</groupId>
<version>4.3.9</version>
<version>5.0.0-beta2</version>
<relativePath>../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@ -1,14 +1,13 @@
package tech.powerjob.server.core.service;
import tech.powerjob.server.persistence.remote.model.UserInfoDO;
import tech.powerjob.server.persistence.remote.repository.UserInfoRepository;
import com.google.common.base.Splitter;
import com.google.common.collect.Lists;
import org.springframework.stereotype.Service;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import tech.powerjob.server.persistence.remote.model.UserInfoDO;
import tech.powerjob.server.persistence.remote.repository.UserInfoRepository;
import javax.annotation.Resource;
import java.util.Date;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
@ -25,16 +24,6 @@ public class UserService {
@Resource
private UserInfoRepository userInfoRepository;
/**
* 保存/修改 用户
* @param userInfoDO user
*/
public void save(UserInfoDO userInfoDO) {
userInfoDO.setGmtCreate(new Date());
userInfoDO.setGmtModified(userInfoDO.getGmtCreate());
userInfoRepository.saveAndFlush(userInfoDO);
}
/**
* 根据用户ID字符串获取用户信息详细列表
* @param userIds 逗号分割的用户ID信息

View File

@ -1,7 +1,6 @@
package tech.powerjob.server.core.uid;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import tech.powerjob.server.remote.server.self.ServerInfoService;
@ -22,7 +21,7 @@ public class IdGenerateService {
private static final int DATA_CENTER_ID = 0;
public IdGenerateService(ServerInfoService serverInfoService) {
long id = serverInfoService.fetchServiceInfo().getId();
long id = serverInfoService.fetchCurrentServerInfo().getId();
snowFlakeIdGenerator = new SnowFlakeIdGenerator(DATA_CENTER_ID, id);
log.info("[IdGenerateService] initialize IdGenerateService successfully, ID:{}", id);
}

View File

@ -5,7 +5,7 @@
<parent>
<artifactId>powerjob-server</artifactId>
<groupId>tech.powerjob</groupId>
<version>4.3.9</version>
<version>5.0.0-beta2</version>
<relativePath>../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@ -5,7 +5,7 @@
<parent>
<artifactId>powerjob-server</artifactId>
<groupId>tech.powerjob</groupId>
<version>4.3.9</version>
<version>5.0.0-beta2</version>
<relativePath>../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@ -5,7 +5,7 @@
<parent>
<artifactId>powerjob-server</artifactId>
<groupId>tech.powerjob</groupId>
<version>4.3.9</version>
<version>5.0.0-beta2</version>
<relativePath>../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@ -5,7 +5,7 @@
<parent>
<artifactId>powerjob-server</artifactId>
<groupId>tech.powerjob</groupId>
<version>4.3.9</version>
<version>5.0.0-beta2</version>
<relativePath>../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
@ -41,8 +41,8 @@
<artifactId>aliyun-sdk-oss</artifactId>
</dependency>
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-java-sdk-s3</artifactId>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
</dependency>
</dependencies>

View File

@ -86,7 +86,7 @@ public class QueryConvertUtils {
};
}
private static String convertLikeParams(Object o) {
public static String convertLikeParams(Object o) {
String s = (String) o;
if (!s.startsWith("%")) {
s = "%" + s;

View File

@ -22,7 +22,14 @@ public class AppInfoDO {
@GenericGenerator(name = "native", strategy = "native")
private Long id;
private String appName;
/**
* 描述
*/
private String title;
/**
* 应用分组密码
*/
@ -35,7 +42,24 @@ public class AppInfoDO {
*/
private String currentServer;
/**
* 命名空间ID外键关联
*/
private Long namespaceId;
/**
* 管理标签
*/
private String tags;
/**
* 扩展字段
*/
private String extra;
private Date gmtCreate;
private Date gmtModified;
private Long creator;
private Long modifier;
}

View File

@ -0,0 +1,65 @@
package tech.powerjob.server.persistence.remote.model;
import lombok.Data;
import org.hibernate.annotations.GenericGenerator;
import javax.persistence.*;
import java.util.Date;
/**
* 命名空间用于组织管理 App
*
* @author tjq
* @since 2023/9/3
*/
@Data
@Entity
@Table(uniqueConstraints = {@UniqueConstraint(name = "uidx01_namespace", columnNames = {"code"})})
public class NamespaceDO {
@Id
@GeneratedValue(strategy = GenerationType.AUTO, generator = "native")
@GenericGenerator(name = "native", strategy = "native")
private Long id;
/**
* 空间唯一标识
*/
private String code;
/**
* 空间名称比如中文描述XX部门XX空间
*/
private String name;
/**
* 鉴权 token后续 OpenAPI 调用需要
*/
private String token;
private Integer status;
/**
* 部门组织架构相关属性
* 预留数据库字段方便基于组织架构二次开发
*/
private String dept;
/**
* 标签扩展性之王多值逗号分割
*/
private String tags;
/**
* 扩展字段
*/
private String extra;
private Date gmtCreate;
private Date gmtModified;
private Long creator;
private Long modifier;
}

View File

@ -0,0 +1,36 @@
package tech.powerjob.server.persistence.remote.model;
import lombok.Data;
import org.hibernate.annotations.GenericGenerator;
import javax.persistence.*;
import java.util.Date;
/**
* PowerJob 自建登录体系的用户表只存储使用 PowerJob 自带登录方式登录的用户信息
*
* @author tjq
* @since 2024/2/13
*/
@Data
@Entity
@Table(uniqueConstraints = {
@UniqueConstraint(name = "uidx01_username", columnNames = {"username"})
})
public class PwjbUserInfoDO {
@Id
@GeneratedValue(strategy = GenerationType.AUTO, generator = "native")
@GenericGenerator(name = "native", strategy = "native")
private Long id;
private String username;
private String password;
private String extra;
private Date gmtCreate;
private Date gmtModified;
}

View File

@ -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;
}

View File

@ -8,16 +8,20 @@ import java.util.Date;
/**
* 用户信息表
* PowerJob 自身维护的全部用户体系数据
* 5.0.0 可能不兼容改动为了支持第三方登录需要通过 username 与第三方登录系统做匹配该列需要声明为唯一索引确保全局唯一
*
* @author tjq
* @since 2020/4/12
*/
@Data
@Entity
@Table(indexes = {
@Index(name = "uidx01_user_info", columnList = "username"),
@Index(name = "uidx02_user_info", columnList = "email")
})
@Table(uniqueConstraints = {
@UniqueConstraint(name = "uidx01_user_name", columnNames = {"username"})
},
indexes = {
@Index(name = "uidx02_user_info", columnList = "email")
})
public class UserInfoDO {
@Id
@ -25,7 +29,17 @@ public class UserInfoDO {
@GenericGenerator(name = "native", strategy = "native")
private Long id;
/**
* 账号类型
*/
private String accountType;
private String username;
/**
* since 5.0.0
* 昵称第三方登陆的 username 很难识别方便后续展示引入 nick
*/
private String nick;
private String password;
/**
@ -40,11 +54,23 @@ public class UserInfoDO {
* webHook
*/
private String webHook;
/**
* 扩展字段
* JWT 登录的二次校验信息
*/
private String tokenLoginVerifyInfo;
/**
* 扩展字段 for 第三方
* PowerJob 内部不允许使用该字段
*/
private String extra;
/**
* 原始账号 username
*/
private String originUsername;
private Date gmtCreate;
private Date gmtModified;

View File

@ -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;
}

View File

@ -1,12 +1,14 @@
package tech.powerjob.server.persistence.remote.repository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import tech.powerjob.server.persistence.remote.model.AppInfoDO;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import tech.powerjob.server.persistence.remote.model.AppInfoDO;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
@ -16,7 +18,7 @@ import java.util.Optional;
* @author tjq
* @since 2020/4/1
*/
public interface AppInfoRepository extends JpaRepository<AppInfoDO, Long> {
public interface AppInfoRepository extends JpaRepository<AppInfoDO, Long>, JpaSpecificationExecutor<AppInfoDO> {
Optional<AppInfoDO> findByAppName(String appName);
@ -31,4 +33,8 @@ public interface AppInfoRepository extends JpaRepository<AppInfoDO, Long> {
@Query(value = "select id from AppInfoDO where currentServer = :currentServer")
List<Long> listAppIdByCurrentServer(@Param("currentServer")String currentServer);
List<AppInfoDO> findAllByNamespaceId(Long namespaceId);
List<AppInfoDO> findAllByIdIn(Collection<Long> ids);
}

View File

@ -0,0 +1,22 @@
package tech.powerjob.server.persistence.remote.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import tech.powerjob.server.persistence.remote.model.NamespaceDO;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
/**
* 命名空间
*
* @author tjq
* @since 2023/9/3
*/
public interface NamespaceRepository extends JpaRepository<NamespaceDO, Long>, JpaSpecificationExecutor<NamespaceDO> {
Optional<NamespaceDO> findByCode(String code);
List<NamespaceDO> findAllByIdIn(Collection<Long> ids);
}

View File

@ -0,0 +1,17 @@
package tech.powerjob.server.persistence.remote.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import tech.powerjob.server.persistence.remote.model.PwjbUserInfoDO;
import java.util.Optional;
/**
* PwjbUserInfoRepository
*
* @author tjq
* @since 2024/2/13
*/
public interface PwjbUserInfoRepository extends JpaRepository<PwjbUserInfoDO, Long> {
Optional<PwjbUserInfoDO> findByUsername(String username);
}

View File

@ -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<SundryDO, Long> {
List<SundryDO> findAllByPkey(String pkey);
Optional<SundryDO> findByPkeyAndSkey(String pkey, String skey);
}

View File

@ -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<UserInfoDO, Long> {
Optional<UserInfoDO> findByUsername(String username);
List<UserInfoDO> findByUsernameLike(String username);
List<UserInfoDO> findByIdIn(List<Long> userIds);

View File

@ -0,0 +1,23 @@
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<UserRoleDO, Long> {
List<UserRoleDO> findAllByUserId(Long userId);
List<UserRoleDO> findAllByScopeAndTarget(Integer scope, Long target);
List<UserRoleDO> findAllByScopeAndTargetAndRoleAndUserId(Integer scope, Long target, Integer role, Long userId);
List<UserRoleDO> findAllByUserIdAndScope(Long userId, Integer scope);
}

View File

@ -5,7 +5,7 @@
<parent>
<artifactId>powerjob-server</artifactId>
<groupId>tech.powerjob</groupId>
<version>4.3.9</version>
<version>5.0.0-beta2</version>
<relativePath>../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@ -14,6 +14,13 @@ public interface ServerInfoService {
* fetch current server info
* @return ServerInfo
*/
ServerInfo fetchServiceInfo();
ServerInfo fetchCurrentServerInfo();
/**
* fetch schedule server info
* @param appId appId
* @return ServerInfo
*/
ServerInfo fetchAppServerInfo(Long appId);
}

View File

@ -15,6 +15,7 @@ import tech.powerjob.server.common.module.ServerInfo;
import tech.powerjob.server.extension.LockService;
import tech.powerjob.server.persistence.remote.model.ServerInfoDO;
import tech.powerjob.server.persistence.remote.repository.ServerInfoRepository;
import tech.powerjob.server.remote.server.redirector.DesignateServer;
import java.util.Date;
import java.util.List;
@ -138,7 +139,13 @@ public class ServerInfoServiceImpl implements ServerInfoService {
}
@Override
public ServerInfo fetchServiceInfo() {
public ServerInfo fetchCurrentServerInfo() {
return serverInfo;
}
@Override
@DesignateServer
public ServerInfo fetchAppServerInfo(Long appId) {
return serverInfo;
}
}

View File

@ -5,7 +5,7 @@
<parent>
<artifactId>powerjob-server</artifactId>
<groupId>tech.powerjob</groupId>
<version>4.3.9</version>
<version>5.0.0-beta2</version>
<relativePath>../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
@ -43,6 +43,10 @@
<groupId>tech.powerjob</groupId>
<artifactId>powerjob-server-core</artifactId>
</dependency>
<dependency>
<groupId>tech.powerjob</groupId>
<artifactId>powerjob-server-auth</artifactId>
</dependency>
<dependency>
<groupId>tech.powerjob</groupId>
<artifactId>powerjob-server-migrate</artifactId>

View File

@ -0,0 +1,46 @@
package tech.powerjob.server.auth.plugin;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StreamUtils;
import tech.powerjob.common.serialize.JsonUtils;
import tech.powerjob.server.auth.Permission;
import tech.powerjob.server.auth.interceptor.DynamicPermissionPlugin;
import javax.servlet.http.HttpServletRequest;
import java.util.Map;
/**
* 针对 namespace app 两大鉴权纬度创建不需要任何权限但任何修改操作都需要 WRITE 权限
* 创建不需要权限修改需要校验权限
*
* @author tjq
* @since 2023/9/3
*/
@Slf4j
public class ModifyOrCreateDynamicPermission implements DynamicPermissionPlugin {
@Override
public Permission calculate(HttpServletRequest request, Object handler) {
try {
//获取请求body
byte[] bodyBytes = StreamUtils.copyToByteArray(request.getInputStream());
String body = new String(bodyBytes, request.getCharacterEncoding());
Map<String, Object> inputParams = JsonUtils.parseMap(body);
Object id = inputParams.get("id");
// 创建不需要权限
if (id == null) {
return Permission.NONE;
}
return Permission.WRITE;
} catch (Exception e) {
log.error("[ModifyOrCreateDynamicPermission] check permission failed, please fix the bug!!!", e);
}
// 异常情况先放行不影响功能使用后续修复 BUG
return Permission.NONE;
}
}

View File

@ -0,0 +1,16 @@
package tech.powerjob.server.auth.plugin;
import tech.powerjob.server.auth.RoleScope;
/**
* desc
*
* @author tjq
* @since 2024/2/11
*/
public class SaveAppGrantPermissionPlugin extends SaveGrantPermissionPlugin {
@Override
protected RoleScope fetchRuleScope() {
return RoleScope.APP;
}
}

View File

@ -0,0 +1,76 @@
package tech.powerjob.server.auth.plugin;
import com.google.common.collect.Maps;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.MapUtils;
import tech.powerjob.common.response.ResultDTO;
import tech.powerjob.common.serialize.JsonUtils;
import tech.powerjob.server.auth.LoginUserHolder;
import tech.powerjob.server.auth.PowerJobUser;
import tech.powerjob.server.auth.Role;
import tech.powerjob.server.auth.RoleScope;
import tech.powerjob.server.auth.interceptor.GrantPermissionPlugin;
import tech.powerjob.server.auth.service.permission.PowerJobPermissionService;
import tech.powerjob.server.common.utils.SpringUtils;
import java.lang.reflect.Method;
import java.util.Map;
/**
* WEB 类保存&修改一体型请求-授权插件
*
* @author tjq
* @since 2024/2/11
*/
@Slf4j
public abstract class SaveGrantPermissionPlugin implements GrantPermissionPlugin {
private static final String KEY_ID = "id";
@Override
public void grant(Object[] args, Object result, Method method, Object originBean) {
if (args == null || args.length != 1) {
throw new IllegalArgumentException("[GrantPermission] args not match, maybe there has some bug");
}
// 理论上不可能前置已完成判断
PowerJobUser powerJobUser = LoginUserHolder.get();
if (powerJobUser == null) {
throw new IllegalArgumentException("[GrantPermission] user not login, can't grant permission");
}
// 解析ID非空代表更新不授权
Map<String, Object> saveRequest = JsonUtils.parseMap(JsonUtils.toJSONString(args[0]));
Long id = MapUtils.getLong(saveRequest, KEY_ID);
if (id != null) {
return;
}
if (!(result instanceof ResultDTO)) {
throw new IllegalArgumentException("[GrantPermission] result not instanceof ResultDTO, maybe there has some bug");
}
ResultDTO<?> resultDTO = (ResultDTO<?>) result;
if (!resultDTO.isSuccess()) {
log.warn("[GrantPermission] result not success, skip grant permission!");
return;
}
Map<String, Object> saveResult = JsonUtils.parseMap(JsonUtils.toJSONString(resultDTO.getData()));
Long savedId = MapUtils.getLong(saveResult, KEY_ID);
if (savedId == null) {
throw new IllegalArgumentException("[GrantPermission] result success but id not exits, maybe there has some bug, please fix it!!!");
}
PowerJobPermissionService powerJobPermissionService = SpringUtils.getBean(PowerJobPermissionService.class);
Map<String, Object> extra = Maps.newHashMap();
extra.put("source", "SaveGrantPermissionPlugin");
powerJobPermissionService.grantRole(fetchRuleScope(), savedId, powerJobUser.getId(), Role.ADMIN, JsonUtils.toJSONString(extra));
}
protected abstract RoleScope fetchRuleScope();
}

View File

@ -0,0 +1,16 @@
package tech.powerjob.server.auth.plugin;
import tech.powerjob.server.auth.RoleScope;
/**
* namespace 授权插件
*
* @author tjq
* @since 2024/2/11
*/
public class SaveNamespaceGrantPermissionPlugin extends SaveGrantPermissionPlugin {
@Override
protected RoleScope fetchRuleScope() {
return RoleScope.NAMESPACE;
}
}

View File

@ -0,0 +1,56 @@
package tech.powerjob.server.auth.service;
import tech.powerjob.server.auth.Permission;
import tech.powerjob.server.auth.Role;
import tech.powerjob.server.auth.RoleScope;
import tech.powerjob.server.web.request.ComponentUserRoleInfo;
import java.util.List;
import java.util.Map;
/**
* Web Auth 服务
* 写在 starter 包下抽取 controller 的公共逻辑
* powerjob service/core 包核心处理调度核心逻辑admin 部分代码收口在 stater
*
* @author tjq
* @since 2024/2/12
*/
public interface WebAuthService {
/**
* 对当前登录用户授予角色
* @param roleScope 角色范围
* @param target 目标
* @param role 角色
* @param extra 其他信息
*/
void grantRole2LoginUser(RoleScope roleScope, Long target, Role role, String extra);
/**
* 处理授权
* @param roleScope 权限范围
* @param target 权限目标
* @param componentUserRoleInfo 人员角色信息
*/
void processPermissionOnSave(RoleScope roleScope, Long target, ComponentUserRoleInfo componentUserRoleInfo);
/**
* 获取目标相关权限人员列表
* @param roleScope 权限范围
* @param target 权限目标
* @return ComponentUserRoleInfo
*/
ComponentUserRoleInfo fetchComponentUserRoleInfo(RoleScope roleScope, Long target);
/**
* 判断当前用户是否有权限
* @param roleScope 权限范围
* @param target 权限目标
* @param permission 要求的权限
* @return 是否有权限
*/
boolean hasPermission(RoleScope roleScope, Long target, Permission permission);
Map<Role, List<Long>> fetchMyPermissionTargets(RoleScope roleScope);
}

View File

@ -0,0 +1,114 @@
package tech.powerjob.server.auth.service.impl;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import tech.powerjob.common.serialize.JsonUtils;
import tech.powerjob.server.auth.*;
import tech.powerjob.server.auth.common.AuthErrorCode;
import tech.powerjob.server.auth.common.PowerJobAuthException;
import tech.powerjob.server.auth.service.WebAuthService;
import tech.powerjob.server.auth.service.permission.PowerJobPermissionService;
import tech.powerjob.server.web.request.ComponentUserRoleInfo;
import javax.annotation.Resource;
import java.util.*;
/**
* WebAuthService
*
* @author tjq
* @since 2024/2/12
*/
@Slf4j
@Service
public class WebAuthServiceImpl implements WebAuthService {
@Resource
private PowerJobPermissionService powerJobPermissionService;
@Override
public void grantRole2LoginUser(RoleScope roleScope, Long target, Role role, String extra) {
Long userId = LoginUserHolder.getUserId();
if (userId == null) {
throw new PowerJobAuthException(AuthErrorCode.USER_NOT_LOGIN);
}
powerJobPermissionService.grantRole(roleScope, target, userId, role, extra);
}
@Override
public void processPermissionOnSave(RoleScope roleScope, Long target, ComponentUserRoleInfo o) {
ComponentUserRoleInfo componentUserRoleInfo = Optional.ofNullable(o).orElse(new ComponentUserRoleInfo());
Map<Role, List<Long>> role2Uids = powerJobPermissionService.fetchUserWithPermissions(roleScope, target);
diffGrant(roleScope, target, Role.OBSERVER, componentUserRoleInfo.getObserver(), role2Uids);
diffGrant(roleScope, target, Role.QA, componentUserRoleInfo.getQa(), role2Uids);
diffGrant(roleScope, target, Role.DEVELOPER, componentUserRoleInfo.getDeveloper(), role2Uids);
diffGrant(roleScope, target, Role.ADMIN, componentUserRoleInfo.getAdmin(), role2Uids);
}
@Override
public ComponentUserRoleInfo fetchComponentUserRoleInfo(RoleScope roleScope, Long target) {
Map<Role, List<Long>> role2Uids = powerJobPermissionService.fetchUserWithPermissions(roleScope, target);
return new ComponentUserRoleInfo()
.setObserver(role2Uids.getOrDefault(Role.OBSERVER, Collections.emptyList()))
.setQa(role2Uids.getOrDefault(Role.QA, Collections.emptyList()))
.setDeveloper(role2Uids.getOrDefault(Role.DEVELOPER, Collections.emptyList()))
.setAdmin(role2Uids.getOrDefault(Role.ADMIN, Collections.emptyList()));
}
@Override
public boolean hasPermission(RoleScope roleScope, Long target, Permission permission) {
PowerJobUser powerJobUser = LoginUserHolder.get();
if (powerJobUser == null) {
return false;
}
return powerJobPermissionService.hasPermission(powerJobUser.getId(), roleScope, target, permission);
}
@Override
public Map<Role, List<Long>> fetchMyPermissionTargets(RoleScope roleScope) {
PowerJobUser powerJobUser = LoginUserHolder.get();
if (powerJobUser == null) {
throw new PowerJobAuthException(AuthErrorCode.USER_NOT_LOGIN);
}
// 展示不考虑穿透权限的问题即拥有 namespace 权限也可以看到全部的 apps
return powerJobPermissionService.fetchUserHadPermissionTargets(roleScope, powerJobUser.getId());
}
private void diffGrant(RoleScope roleScope, Long target, Role role, List<Long> uids, Map<Role, List<Long>> originRole2Uids) {
Set<Long> originUids = Sets.newHashSet(Optional.ofNullable(originRole2Uids.get(role)).orElse(Collections.emptyList()));
Set<Long> currentUids = Sets.newHashSet(Optional.ofNullable(uids).orElse(Collections.emptyList()));
Map<String, Object> extraInfo = Maps.newHashMap();
extraInfo.put("grantor", LoginUserHolder.getUserName());
extraInfo.put("source", "diffGrant");
String extra = JsonUtils.toJSONString(extraInfo);
Set<Long> allIds = Sets.newHashSet(originUids);
allIds.addAll(currentUids);
Set<Long> allIds2 = Sets.newHashSet(allIds);
// originUids 不在 currentUids需要取消授权
allIds.removeAll(currentUids);
allIds.forEach(cancelPermissionUid -> {
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.grantRole(roleScope, target, addPermissionUid, role, extra);
log.info("[WebAuthService] [diffGrant] grantPermission: roleScope={},target={},uid={},role={},extra={}", roleScope, target, addPermissionUid, role, extra);
});
}
}

View File

@ -0,0 +1,96 @@
package tech.powerjob.server.config;
import org.springframework.stereotype.Component;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.*;
/**
* 解决 HttpServletRequest 只能被读取一次的问题方便全局日志 & 鉴权切面提前读取数据
* 在请求进入Servlet容器之前先经过Filter的过滤器链在请求进入Controller之前先经过 HandlerInterceptor 的拦截器链Filter 一定先于 HandlerInterceptor 执行
*
* @author tjq
* @since 2024/2/11
*/
@Component
public class CachingRequestBodyFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
if (request instanceof HttpServletRequest) {
CustomHttpServletRequestWrapper wrappedRequest = new CustomHttpServletRequestWrapper((HttpServletRequest) request);
chain.doFilter(wrappedRequest, response);
} else {
chain.doFilter(request, response);
}
}
// Implement other required methods like init() and destroy() if necessary
public static class CustomHttpServletRequestWrapper extends HttpServletRequestWrapper {
private final String body;
public CustomHttpServletRequestWrapper(HttpServletRequest request) throws IOException {
super(request);
StringBuilder stringBuilder = new StringBuilder();
BufferedReader bufferedReader = null;
try {
InputStream inputStream = request.getInputStream();
if (inputStream != null) {
bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
char[] charBuffer = new char[128];
int bytesRead = -1;
while ((bytesRead = bufferedReader.read(charBuffer)) > 0) {
stringBuilder.append(charBuffer, 0, bytesRead);
}
}
} finally {
if (bufferedReader != null) {
bufferedReader.close();
}
}
body = stringBuilder.toString();
}
@Override
public ServletInputStream getInputStream() throws IOException {
final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(body.getBytes());
return new ServletInputStream() {
public int read() throws IOException {
return byteArrayInputStream.read();
}
@Override
public boolean isFinished() {
return byteArrayInputStream.available() == 0;
}
@Override
public boolean isReady() {
return true;
}
@Override
public void setReadListener(ReadListener readListener) {
throw new UnsupportedOperationException("Not implemented");
}
};
}
@Override
public BufferedReader getReader() throws IOException {
return new BufferedReader(new InputStreamReader(this.getInputStream()));
}
public String getBody() {
return this.body;
}
}
}

View File

@ -41,7 +41,7 @@ public class SwaggerConfig {
return new OpenAPI()
.info(new Info().title("PowerJob")
.description("Distributed scheduling and computing framework.")
.version(serverInfoService.fetchServiceInfo().getVersion())
.version(serverInfoService.fetchCurrentServerInfo().getVersion())
.contact(contact)
.license(new License().name("Apache License 2.0").url("https://github.com/PowerJob/PowerJob/blob/master/LICENSE")));
}

View File

@ -3,9 +3,13 @@ package tech.powerjob.server.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
import tech.powerjob.server.auth.interceptor.PowerJobAuthInterceptor;
import javax.annotation.Resource;
/**
* CORS
@ -16,6 +20,10 @@ import org.springframework.web.socket.server.standard.ServerEndpointExporter;
@Configuration
@EnableWebSocket
public class WebConfig implements WebMvcConfigurer {
@Resource
private PowerJobAuthInterceptor powerJobAuthInterceptor;
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
@ -26,4 +34,19 @@ public class WebConfig implements WebMvcConfigurer {
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
/*
可以添加多个拦截器
addPathPatterns("/**") 表示对所有请求都拦截
.excludePathPatterns("/base/index") 表示排除对/base/index请求的拦截
多个拦截器可以设置order顺序值越小preHandle越先执行postHandle和afterCompletion越后执行
order默认的值是0如果只添加一个拦截器可以不显示设置order的值
*/
registry.addInterceptor(powerJobAuthInterceptor)
.addPathPatterns("/**")
.excludePathPatterns("/css/**", "/js/**", "/images/**", "/img/**", "/fonts/**", "/favicon.ico")
.order(0);
}
}

View File

@ -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<Void> initFunc) {
Optional<SundryDO> 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<SundryDO> 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);
}
}
}
}

View File

@ -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();
}

View File

@ -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<String, Object> 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);
}
}

View File

@ -19,7 +19,7 @@ import java.util.List;
public class ServerInfoAwareProcessor {
public ServerInfoAwareProcessor(ServerInfoService serverInfoService, List<ServerInfoAware> awareList) {
final ServerInfo serverInfo = serverInfoService.fetchServiceInfo();
final ServerInfo serverInfo = serverInfoService.fetchCurrentServerInfo();
log.info("[ServerInfoAwareProcessor] current server info: {}", serverInfo);
awareList.forEach(aware -> {
aware.setServerInfo(serverInfo);

View File

@ -1,7 +1,5 @@
package tech.powerjob.server.web;
import tech.powerjob.common.exception.PowerJobException;
import tech.powerjob.common.response.ResultDTO;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.springframework.http.converter.HttpMessageNotReadableException;
@ -10,6 +8,9 @@ import org.springframework.web.HttpRequestMethodNotSupportedException;
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;
/**
* 统一处理 web 层异常信息
@ -23,11 +24,16 @@ public class ControllerExceptionHandler {
@ResponseBody
@ExceptionHandler(Exception.class)
public ResultDTO<Void> exceptionHandler(Exception e) {
public WebResultDTO<Void> exceptionHandler(Exception e) {
WebResultDTO<Void> ret = new WebResultDTO<>(ResultDTO.failed(ExceptionUtils.getMessage(e)));
// 不是所有异常都需要打印完整堆栈后续可以定义内部的Exception便于判断
if (e instanceof IllegalArgumentException || e instanceof PowerJobException) {
log.warn("[ControllerException] http request failed, message is {}.", e.getMessage());
if (e instanceof PowerJobException) {
ret.setCode(((PowerJobException) e).getCode());
log.warn("[ControllerException] PowerJobException, message is {}.", e.getMessage());
} else if (e instanceof IllegalArgumentException) {
log.warn("[ControllerException] http request failed due to IllegalArgument, message is {}.", e.getMessage());
} else if (e instanceof HttpMessageNotReadableException || e instanceof MethodArgumentTypeMismatchException) {
log.warn("[ControllerException] invalid http request params, exception is {}.", e.getMessage());
} else if (e instanceof HttpRequestMethodNotSupportedException) {
@ -35,6 +41,7 @@ public class ControllerExceptionHandler {
} else {
log.error("[ControllerException] http request failed.", e);
}
return ResultDTO.failed(ExceptionUtils.getMessage(e));
return ret;
}
}

View File

@ -1,26 +1,50 @@
package tech.powerjob.server.web.controller;
import lombok.RequiredArgsConstructor;
import tech.powerjob.common.exception.PowerJobException;
import tech.powerjob.common.response.ResultDTO;
import tech.powerjob.server.persistence.remote.model.AppInfoDO;
import tech.powerjob.server.persistence.remote.repository.AppInfoRepository;
import tech.powerjob.server.core.service.AppInfoService;
import tech.powerjob.server.web.request.AppAssertRequest;
import tech.powerjob.server.web.request.ModifyAppInfoRequest;
import com.google.common.collect.Lists;
import lombok.Data;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanUtils;
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.util.CollectionUtils;
import org.springframework.web.bind.annotation.*;
import tech.powerjob.common.response.ResultDTO;
import tech.powerjob.common.serialize.JsonUtils;
import tech.powerjob.common.utils.CommonUtils;
import tech.powerjob.server.auth.LoginUserHolder;
import tech.powerjob.server.auth.Permission;
import tech.powerjob.server.auth.Role;
import tech.powerjob.server.auth.RoleScope;
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.interceptor.ApiPermission;
import tech.powerjob.server.auth.plugin.ModifyOrCreateDynamicPermission;
import tech.powerjob.server.auth.plugin.SaveAppGrantPermissionPlugin;
import tech.powerjob.server.auth.service.WebAuthService;
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.web.converter.NamespaceConverter;
import tech.powerjob.server.web.request.AppAssertRequest;
import tech.powerjob.server.web.request.ComponentUserRoleInfo;
import tech.powerjob.server.web.request.ModifyAppInfoRequest;
import tech.powerjob.server.web.request.QueryAppInfoRequest;
import tech.powerjob.server.web.response.AppInfoVO;
import tech.powerjob.server.web.response.NamespaceBaseVO;
import tech.powerjob.server.web.response.UserBaseVO;
import tech.powerjob.server.web.service.NamespaceWebService;
import tech.powerjob.server.web.service.UserWebService;
import javax.annotation.Resource;
import java.util.Date;
import java.util.List;
import java.util.Objects;
import javax.persistence.criteria.Predicate;
import java.util.*;
import java.util.stream.Collectors;
/**
@ -35,14 +59,17 @@ import java.util.stream.Collectors;
@RequiredArgsConstructor
public class AppInfoController {
private final AppInfoService appInfoService;
private final WebAuthService webAuthService;
private final UserWebService userWebService;
private final AppInfoRepository appInfoRepository;
private static final int MAX_APP_NUM = 200;
private final NamespaceWebService namespaceWebService;
@PostMapping("/save")
public ResultDTO<Void> saveAppInfo(@RequestBody ModifyAppInfoRequest req) {
@ApiPermission(name = "App-Save", roleScope = RoleScope.APP, dynamicPermissionPlugin = ModifyOrCreateDynamicPermission.class, grandPermissionPlugin = SaveAppGrantPermissionPlugin.class)
public ResultDTO<AppInfoVO> saveAppInfo(@RequestBody ModifyAppInfoRequest req) {
req.valid();
AppInfoDO appInfoDO;
@ -51,59 +78,154 @@ public class AppInfoController {
if (id == null) {
appInfoDO = new AppInfoDO();
appInfoDO.setGmtCreate(new Date());
}else {
appInfoDO.setCreator(LoginUserHolder.getUserId());
} else {
appInfoDO = appInfoRepository.findById(id).orElseThrow(() -> new IllegalArgumentException("can't find appInfo by id:" + id));
// 对比密码
if (!Objects.equals(req.getOldPassword(), appInfoDO.getPassword())) {
throw new PowerJobException("The password is incorrect.");
// 不允许修改 appName
if (!appInfoDO.getAppName().equalsIgnoreCase(req.getAppName())) {
throw new IllegalArgumentException("NOT_ALLOW_CHANGE_THE_APP_NAME");
}
}
BeanUtils.copyProperties(req, appInfoDO);
appInfoDO.setAppName(req.getAppName());
appInfoDO.setTitle(req.getTitle());
appInfoDO.setPassword(req.getPassword());
appInfoDO.setNamespaceId(req.getNamespaceId());
appInfoDO.setTags(req.getTags());
appInfoDO.setExtra(req.getExtra());
appInfoDO.setGmtModified(new Date());
appInfoDO.setModifier(LoginUserHolder.getUserId());
appInfoRepository.saveAndFlush(appInfoDO);
return ResultDTO.success(null);
}
AppInfoDO savedAppInfo = appInfoRepository.saveAndFlush(appInfoDO);
@PostMapping("/assert")
public ResultDTO<Long> assertApp(@RequestBody AppAssertRequest request) {
return ResultDTO.success(appInfoService.assertApp(request.getAppName(), request.getPassword()));
// 重现授权
webAuthService.processPermissionOnSave(RoleScope.APP, savedAppInfo.getId(), req.getComponentUserRoleInfo());
return ResultDTO.success(convert(Lists.newArrayList(savedAppInfo), false).get(0));
}
@GetMapping("/delete")
@ApiPermission(name = "App-Delete", roleScope = RoleScope.APP, requiredPermission = Permission.SU)
public ResultDTO<Void> deleteAppInfo(Long appId) {
appInfoRepository.deleteById(appId);
return ResultDTO.success(null);
}
@GetMapping("/list")
public ResultDTO<List<AppInfoVO>> listAppInfo(@RequestParam(required = false) String condition) {
List<AppInfoDO> result;
Pageable limit = PageRequest.of(0, MAX_APP_NUM);
if (StringUtils.isEmpty(condition)) {
result = appInfoRepository.findAll(limit).getContent();
}else {
result = appInfoRepository.findByAppNameLike("%" + condition + "%", limit).getContent();
@PostMapping("/list")
@ApiPermission(name = "App-List", roleScope = RoleScope.APP, requiredPermission = Permission.NONE)
public ResultDTO<PageResult<AppInfoVO>> listAppInfoByQuery(@RequestBody QueryAppInfoRequest queryAppInfoRequest) {
Pageable pageable = PageRequest.of(queryAppInfoRequest.getIndex(), queryAppInfoRequest.getPageSize());
// 相关权限先查处关联 ids
Set<Long> queryAppIds;
Boolean showMyRelated = queryAppInfoRequest.getShowMyRelated();
if (BooleanUtils.isTrue(showMyRelated)) {
Set<Long> targetIds = Sets.newHashSet();
webAuthService.fetchMyPermissionTargets(RoleScope.APP).values().forEach(targetIds::addAll);
queryAppIds = targetIds;
} else {
queryAppIds = Collections.emptySet();
}
return ResultDTO.success(convert(result));
Specification<AppInfoDO> specification = (root, query, criteriaBuilder) -> {
List<Predicate> predicates = Lists.newArrayList();
Long appId = queryAppInfoRequest.getAppId();
Long namespaceId = queryAppInfoRequest.getNamespaceId();
if (appId != null) {
predicates.add(criteriaBuilder.equal(root.get("id"), appId));
}
if (namespaceId != null) {
predicates.add(criteriaBuilder.equal(root.get("namespaceId"), namespaceId));
}
if (StringUtils.isNotEmpty(queryAppInfoRequest.getAppNameLike())) {
predicates.add(criteriaBuilder.like(root.get("appName"), QueryConvertUtils.convertLikeParams(queryAppInfoRequest.getAppNameLike())));
}
if (StringUtils.isNotEmpty(queryAppInfoRequest.getTagLike())) {
predicates.add(criteriaBuilder.like(root.get("tags"), QueryConvertUtils.convertLikeParams(queryAppInfoRequest.getTagLike())));
}
if (!queryAppIds.isEmpty()) {
predicates.add(criteriaBuilder.in(root.get("id")).value(queryAppIds));
}
return query.where(predicates.toArray(new Predicate[0])).getRestriction();
};
Page<AppInfoDO> pageAppInfoResult = appInfoRepository.findAll(specification, pageable);
PageResult<AppInfoVO> pageRet = new PageResult<>(pageAppInfoResult);
List<AppInfoDO> appInfoDos = pageAppInfoResult.get().collect(Collectors.toList());
pageRet.setData(convert(appInfoDos, true));
return ResultDTO.success(pageRet);
}
private static List<AppInfoVO> convert(List<AppInfoDO> data) {
@PostMapping("/becomeAdmin")
@ApiPermission(name = "App-BecomeAdmin", roleScope = RoleScope.GLOBAL, requiredPermission = Permission.NONE)
public ResultDTO<Void> becomeAdminByAppNameAndPassword(@RequestBody AppAssertRequest appAssertRequest) {
String appName = appAssertRequest.getAppName();
Optional<AppInfoDO> appInfoOpt = appInfoRepository.findByAppName(appName);
if (!appInfoOpt.isPresent()) {
throw new IllegalArgumentException("can't find app by appName: " + appName);
}
if (!StringUtils.equals(appInfoOpt.get().getPassword(), appAssertRequest.getPassword())) {
throw new PowerJobAuthException(AuthErrorCode.INCORRECT_PASSWORD);
}
Map<String, Object> extra = Maps.newHashMap();
extra.put("source", "becomeAdminByAppNameAndPassword");
webAuthService.grantRole2LoginUser(RoleScope.APP, appInfoOpt.get().getId(), Role.ADMIN, JsonUtils.toJSONString(extra));
return ResultDTO.success(null);
}
private List<AppInfoVO> convert(List<AppInfoDO> data, boolean fillDetail) {
if (CollectionUtils.isEmpty(data)) {
return Lists.newLinkedList();
}
return data.stream().map(appInfoDO -> {
return data.parallelStream().map(appInfoDO -> {
AppInfoVO appInfoVO = new AppInfoVO();
BeanUtils.copyProperties(appInfoDO, appInfoVO);
appInfoVO.setGmtCreateStr(CommonUtils.formatTime(appInfoDO.getGmtCreate()));
appInfoVO.setGmtModifiedStr(CommonUtils.formatTime(appInfoDO.getGmtModified()));
if (fillDetail) {
// 人员面板
ComponentUserRoleInfo componentUserRoleInfo = webAuthService.fetchComponentUserRoleInfo(RoleScope.APP, appInfoDO.getId());
appInfoVO.setComponentUserRoleInfo(componentUserRoleInfo);
// 密码
boolean hasPermission = webAuthService.hasPermission(RoleScope.APP, appInfoDO.getId(), Permission.READ);
appInfoVO.setPassword(hasPermission ? appInfoDO.getPassword() : AuthConstants.TIPS_NO_PERMISSION_TO_SEE);
// namespace
Optional<NamespaceDO> namespaceOpt = namespaceWebService.findById(appInfoDO.getNamespaceId());
if (namespaceOpt.isPresent()) {
NamespaceBaseVO baseNamespace = NamespaceConverter.do2BaseVo(namespaceOpt.get());
appInfoVO.setNamespace(baseNamespace);
appInfoVO.setNamespaceName(baseNamespace.getName());
}
// user 信息
appInfoVO.setCreatorShowName(userWebService.fetchBaseUserInfo(appInfoDO.getCreator()).map(UserBaseVO::getShowName).orElse(null));
appInfoVO.setModifierShowName(userWebService.fetchBaseUserInfo(appInfoDO.getModifier()).map(UserBaseVO::getShowName).orElse(null));
}
return appInfoVO;
}).collect(Collectors.toList());
}
@Data
private static class AppInfoVO {
private Long id;
private String appName;
}
}

View File

@ -0,0 +1,123 @@
package tech.powerjob.server.web.controller;
import org.springframework.web.bind.annotation.*;
import tech.powerjob.common.response.ResultDTO;
import tech.powerjob.common.utils.CollectionUtils;
import tech.powerjob.server.auth.Permission;
import tech.powerjob.server.auth.PowerJobUser;
import tech.powerjob.server.auth.RoleScope;
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.WebAuthService;
import tech.powerjob.server.auth.service.login.LoginRequest;
import tech.powerjob.server.auth.service.login.PowerJobLoginService;
import tech.powerjob.server.web.request.ComponentUserRoleInfo;
import javax.annotation.Resource;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.List;
import java.util.Optional;
/**
* 登录 & 权限相关
*
* @author tjq
* @since 2023/4/16
*/
@RestController
@RequestMapping("/auth")
public class AuthController {
@Resource
private WebAuthService webAuthService;
@Resource
private PowerJobLoginService powerJobLoginService;
@GetMapping("/supportLoginTypes")
public ResultDTO<List<LoginTypeInfo>> listSupportLoginTypes() {
return ResultDTO.success(powerJobLoginService.fetchSupportLoginTypes());
}
@GetMapping("/thirdPartyLoginUrl")
public ResultDTO<String> getThirdPartyLoginUrl(String type, HttpServletRequest request) {
String url = powerJobLoginService.fetchThirdPartyLoginUrl(type, request);
return ResultDTO.success(url);
}
/**
* 第三方账号体系回调登录接口eg, 接受钉钉登录回调
* @param httpServletRequest 请求
* @param httpServletResponse 响应
* @return 登录结果
*/
@RequestMapping(value = "/thirdPartyLoginCallback", method = {RequestMethod.GET, RequestMethod.POST})
public ResultDTO<PowerJobUser> loginCallback(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) {
LoginRequest loginContext = new LoginRequest().setHttpServletRequest(httpServletRequest);
// 常见登录组件的标准规范钉钉企业微信飞书第三方原样透传开发者在对接第三方登录体系时可能需要修改此处 type 回填
final String state = httpServletRequest.getParameter("state");
loginContext.setLoginType(state);
final PowerJobUser powerJobUser = powerJobLoginService.doLogin(loginContext);
fillJwt4LoginUser(powerJobUser, httpServletResponse);
return ResultDTO.success(powerJobUser);
}
/**
* 第三方账号体系直接登录接口eg, 接受 PowerJob 自带账号密码体系的登录请求
* @param loginRequest 登录请求
* @param httpServletResponse 响应
* @return 登录结果
*/
@PostMapping("/thirdPartyLoginDirect")
public ResultDTO<PowerJobUser> selfLogin(@RequestBody LoginRequest loginRequest, HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) {
loginRequest.setHttpServletRequest(httpServletRequest);
try {
final PowerJobUser powerJobUser = powerJobLoginService.doLogin(loginRequest);
if (powerJobUser == null) {
return ResultDTO.failed("USER_NOT_FOUND");
}
fillJwt4LoginUser(powerJobUser, httpServletResponse);
return ResultDTO.success(powerJobUser);
} catch (Exception e) {
return ResultDTO.failed(e.getMessage());
}
}
@GetMapping(value = "/ifLogin")
public ResultDTO<PowerJobUser> ifLogin(HttpServletRequest httpServletRequest) {
final Optional<PowerJobUser> powerJobUser = powerJobLoginService.ifLogin(httpServletRequest);
return powerJobUser.map(ResultDTO::success).orElseGet(() -> ResultDTO.success(null));
}
/* ****************** 授权相关 ****************** */
@GetMapping("/listGlobalAdmin")
public ResultDTO<List<Long>> listGlobalAdmin() {
// 全局只设置超级管理员权限
ComponentUserRoleInfo componentUserRoleInfo = webAuthService.fetchComponentUserRoleInfo(RoleScope.GLOBAL, AuthConstants.GLOBAL_ADMIN_TARGET_ID);
return ResultDTO.success(componentUserRoleInfo.getAdmin());
}
@PostMapping("/saveGlobalAdmin")
@ApiPermission(name = "Auth-SaveGlobalAdmin", roleScope = RoleScope.GLOBAL, requiredPermission = Permission.SU)
public ResultDTO<Void> saveGlobalAdmin(@RequestBody ComponentUserRoleInfo componentUserRoleInfo) {
if (CollectionUtils.isEmpty(componentUserRoleInfo.getAdmin())) {
throw new IllegalArgumentException("At least one super administrator is required!");
}
webAuthService.processPermissionOnSave(RoleScope.GLOBAL, AuthConstants.GLOBAL_ADMIN_TARGET_ID, componentUserRoleInfo);
return ResultDTO.success(null);
}
private void fillJwt4LoginUser(PowerJobUser powerJobUser, HttpServletResponse httpServletResponse) {
httpServletResponse.addCookie(new Cookie(AuthConstants.JWT_NAME, powerJobUser.getJwtToken()));
}
}

View File

@ -4,11 +4,13 @@ import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.time.DateFormatUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import tech.powerjob.common.OmsConstant;
import tech.powerjob.common.response.ResultDTO;
import tech.powerjob.server.auth.Permission;
import tech.powerjob.server.auth.RoleScope;
import tech.powerjob.server.auth.interceptor.ApiPermission;
import tech.powerjob.server.common.constants.ContainerSourceType;
import tech.powerjob.server.common.constants.SwitchableStatus;
import tech.powerjob.server.common.utils.OmsFileUtils;
@ -39,22 +41,25 @@ import java.util.stream.Collectors;
@RequestMapping("/container")
public class ContainerController {
private final int port;
private final ContainerService containerService;
private final AppInfoRepository appInfoRepository;
private final ContainerInfoRepository containerInfoRepository;
public ContainerController(@Value("${server.port}") int port, ContainerService containerService, AppInfoRepository appInfoRepository, ContainerInfoRepository containerInfoRepository) {
this.port = port;
public ContainerController(ContainerService containerService, AppInfoRepository appInfoRepository, ContainerInfoRepository containerInfoRepository) {
this.containerService = containerService;
this.appInfoRepository = appInfoRepository;
this.containerInfoRepository = containerInfoRepository;
}
/**
* 暴露给 worker 的下载端口制品本身 version 不可枚举不单独鉴权
* 如果对此有安全性需求可自行实现加密鉴权逻辑或者干脆走自己的下载通道下载制品
* @param version 容器版本
* @param response 响应
* @throws IOException 异常
*/
@GetMapping("/downloadJar")
public void downloadJar(String version, HttpServletResponse response) throws IOException {
File file = containerService.fetchContainerJarFile(version);
@ -66,12 +71,14 @@ public class ContainerController {
}
@PostMapping("/downloadContainerTemplate")
@ApiPermission(name = "Container-DownloadContainerTemplate", roleScope = RoleScope.APP, requiredPermission = Permission.READ)
public void downloadContainerTemplate(@RequestBody GenerateContainerTemplateRequest req, HttpServletResponse response) throws IOException {
File zipFile = ContainerTemplateGenerator.generate(req.getGroup(), req.getArtifact(), req.getName(), req.getPackageName(), req.getJavaVersion());
OmsFileUtils.file2HttpResponse(zipFile, response);
}
@PostMapping("/jarUpload")
@ApiPermission(name = "Container-JarUpload", roleScope = RoleScope.APP, requiredPermission = Permission.OPS)
public ResultDTO<String> fileUpload(@RequestParam("file") MultipartFile file) throws Exception {
if (file == null || file.isEmpty()) {
return ResultDTO.failed("empty file");
@ -80,6 +87,7 @@ public class ContainerController {
}
@PostMapping("/save")
@ApiPermission(name = "Container-Save", roleScope = RoleScope.APP, requiredPermission = Permission.OPS)
public ResultDTO<Void> saveContainer(@RequestBody SaveContainerInfoRequest request) {
request.valid();
@ -93,12 +101,14 @@ public class ContainerController {
}
@GetMapping("/delete")
@ApiPermission(name = "Container-Delete", roleScope = RoleScope.APP, requiredPermission = Permission.OPS)
public ResultDTO<Void> deleteContainer(Long appId, Long containerId) {
containerService.delete(appId, containerId);
return ResultDTO.success(null);
}
@GetMapping("/list")
@ApiPermission(name = "Container-List", roleScope = RoleScope.APP, requiredPermission = Permission.READ)
public ResultDTO<List<ContainerInfoVO>> listContainers(Long appId) {
List<ContainerInfoVO> res = containerInfoRepository.findByAppIdAndStatusNot(appId, SwitchableStatus.DELETED.getV())
.stream().map(ContainerController::convert).collect(Collectors.toList());
@ -106,6 +116,7 @@ public class ContainerController {
}
@GetMapping("/listDeployedWorker")
@ApiPermission(name = "Container-ListDeployedWorker", roleScope = RoleScope.APP, requiredPermission = Permission.READ)
public ResultDTO<String> listDeployedWorker(Long appId, Long containerId, HttpServletResponse response) {
AppInfoDO appInfoDO = appInfoRepository.findById(appId).orElseThrow(() -> new IllegalArgumentException("can't find app by id:" + appId));
String targetServer = appInfoDO.getCurrentServer();

View File

@ -3,6 +3,9 @@ package tech.powerjob.server.web.controller;
import tech.powerjob.common.OmsConstant;
import tech.powerjob.common.enums.InstanceStatus;
import tech.powerjob.common.response.ResultDTO;
import tech.powerjob.server.auth.Permission;
import tech.powerjob.server.auth.RoleScope;
import tech.powerjob.server.auth.interceptor.ApiPermission;
import tech.powerjob.server.common.utils.OmsFileUtils;
import tech.powerjob.server.persistence.PageResult;
import tech.powerjob.server.persistence.StringPage;
@ -46,8 +49,6 @@ import java.util.stream.Collectors;
@RequestMapping("/instance")
public class InstanceController {
@Resource
private InstanceService instanceService;
@Resource
@ -59,18 +60,21 @@ public class InstanceController {
private InstanceInfoRepository instanceInfoRepository;
@GetMapping("/stop")
@ApiPermission(name = "Instance-Stop", roleScope = RoleScope.APP, requiredPermission = Permission.OPS)
public ResultDTO<Void> stopInstance(Long appId,Long instanceId) {
instanceService.stopInstance(appId,instanceId);
return ResultDTO.success(null);
}
@GetMapping("/retry")
@ApiPermission(name = "Instance-Retry", roleScope = RoleScope.APP, requiredPermission = Permission.OPS)
public ResultDTO<Void> retryInstance(String appId, Long instanceId) {
instanceService.retryInstance(Long.valueOf(appId), instanceId);
return ResultDTO.success(null);
}
@GetMapping("/detail")
@ApiPermission(name = "Instance-Detail", roleScope = RoleScope.APP, requiredPermission = Permission.READ)
public ResultDTO<InstanceDetailVO> getInstanceDetail(Long appId, Long instanceId) {
QueryInstanceDetailRequest queryInstanceDetailRequest = new QueryInstanceDetailRequest();
queryInstanceDetailRequest.setAppId(appId);
@ -97,11 +101,13 @@ public class InstanceController {
}
@GetMapping("/log")
@ApiPermission(name = "Instance-Log", roleScope = RoleScope.APP, requiredPermission = Permission.OPS)
public ResultDTO<StringPage> getInstanceLog(Long appId, Long instanceId, Long index) {
return ResultDTO.success(instanceLogService.fetchInstanceLog(appId, instanceId, index));
}
@GetMapping("/downloadLogUrl")
@ApiPermission(name = "Instance-FetchDownloadLogUrl", roleScope = RoleScope.APP, requiredPermission = Permission.READ)
public ResultDTO<String> getDownloadUrl(Long appId, Long instanceId) {
return ResultDTO.success(instanceLogService.fetchDownloadUrl(appId, instanceId));
}
@ -133,6 +139,7 @@ public class InstanceController {
}
@PostMapping("/list")
@ApiPermission(name = "Instance-List", roleScope = RoleScope.APP, requiredPermission = Permission.READ)
public ResultDTO<PageResult<InstanceInfoVO>> list(@RequestBody QueryInstanceRequest request) {
Sort sort = Sort.by(Sort.Direction.DESC, "gmtModified");

View File

@ -3,6 +3,9 @@ package tech.powerjob.server.web.controller;
import org.apache.commons.lang3.StringUtils;
import tech.powerjob.common.request.http.SaveJobInfoRequest;
import tech.powerjob.common.response.ResultDTO;
import tech.powerjob.server.auth.Permission;
import tech.powerjob.server.auth.RoleScope;
import tech.powerjob.server.auth.interceptor.ApiPermission;
import tech.powerjob.server.common.constants.SwitchableStatus;
import tech.powerjob.server.persistence.PageResult;
import tech.powerjob.server.persistence.remote.model.JobInfoDO;
@ -39,39 +42,46 @@ public class JobController {
private JobInfoRepository jobInfoRepository;
@PostMapping("/save")
@ApiPermission(name = "Job-Save", roleScope = RoleScope.APP, requiredPermission = Permission.WRITE)
public ResultDTO<Void> saveJobInfo(@RequestBody SaveJobInfoRequest request) {
jobService.saveJob(request);
return ResultDTO.success(null);
}
@PostMapping("/copy")
@ApiPermission(name = "Job-Copy", roleScope = RoleScope.APP, requiredPermission = Permission.WRITE)
public ResultDTO<JobInfoVO> copyJob(String jobId) {
return ResultDTO.success(JobInfoVO.from(jobService.copyJob(Long.valueOf(jobId))));
}
@GetMapping("/export")
@ApiPermission(name = "Job-Export", roleScope = RoleScope.APP, requiredPermission = Permission.READ)
public ResultDTO<SaveJobInfoRequest> exportJob(String jobId) {
return ResultDTO.success(jobService.exportJob(Long.valueOf(jobId)));
}
@GetMapping("/disable")
@ApiPermission(name = "Job-Disable", roleScope = RoleScope.APP, requiredPermission = Permission.WRITE)
public ResultDTO<Void> disableJob(String jobId) {
jobService.disableJob(Long.valueOf(jobId));
return ResultDTO.success(null);
}
@GetMapping("/delete")
@ApiPermission(name = "Job-Delete", roleScope = RoleScope.APP, requiredPermission = Permission.WRITE)
public ResultDTO<Void> deleteJob(String jobId) {
jobService.deleteJob(Long.valueOf(jobId));
return ResultDTO.success(null);
}
@GetMapping("/run")
@ApiPermission(name = "Job-Copy", roleScope = RoleScope.APP, requiredPermission = Permission.OPS)
public ResultDTO<Long> runImmediately(String appId, String jobId, @RequestParam(required = false) String instanceParams) {
return ResultDTO.success(jobService.runJob(Long.valueOf(appId), Long.valueOf(jobId), instanceParams, 0L));
}
@PostMapping("/list")
@ApiPermission(name = "Job-Copy", roleScope = RoleScope.APP, requiredPermission = Permission.READ)
public ResultDTO<PageResult<JobInfoVO>> listJobs(@RequestBody QueryJobInfoRequest request) {
Sort sort = Sort.by(Sort.Direction.ASC, "id");

View File

@ -0,0 +1,117 @@
package tech.powerjob.server.web.controller;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.data.domain.Page;
import org.springframework.web.bind.annotation.*;
import tech.powerjob.common.response.ResultDTO;
import tech.powerjob.server.auth.Permission;
import tech.powerjob.server.auth.RoleScope;
import tech.powerjob.server.auth.common.AuthConstants;
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.persistence.PageResult;
import tech.powerjob.server.persistence.remote.model.NamespaceDO;
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.response.UserBaseVO;
import tech.powerjob.server.web.service.NamespaceWebService;
import tech.powerjob.server.web.service.UserWebService;
import javax.annotation.Resource;
import java.util.List;
import java.util.stream.Collectors;
/**
* 命名空间 Controller
*
* @author tjq
* @since 2023/9/3
*/
@Slf4j
@RestController
@RequestMapping("/namespace")
public class NamespaceController {
@Resource
private WebAuthService webAuthService;
@Resource
private UserWebService userWebService;
@Resource
private NamespaceWebService namespaceWebService;
@ResponseBody
@PostMapping("/save")
@ApiPermission(name = "Namespace-Save", roleScope = RoleScope.NAMESPACE, dynamicPermissionPlugin = ModifyOrCreateDynamicPermission.class, grandPermissionPlugin = SaveNamespaceGrantPermissionPlugin.class)
public ResultDTO<NamespaceBaseVO> save(@RequestBody ModifyNamespaceRequest req) {
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<Void> deleteNamespace(Long id) {
namespaceWebService.delete(id);
return ResultDTO.success(null);
}
@PostMapping("/list")
@ApiPermission(name = "Namespace-List", roleScope = RoleScope.NAMESPACE, requiredPermission = Permission.NONE)
public ResultDTO<PageResult<NamespaceVO>> listNamespace(@RequestBody QueryNamespaceRequest queryNamespaceRequest) {
Page<NamespaceDO> namespacePageResult = namespaceWebService.list(queryNamespaceRequest);
PageResult<NamespaceVO> ret = new PageResult<>(namespacePageResult);
ret.setData(namespacePageResult.get().map(x -> {
NamespaceVO detailVo = new NamespaceVO();
NamespaceBaseVO baseVO = NamespaceConverter.do2BaseVo(x);
BeanUtils.copyProperties(baseVO, detailVo);
fillDetail(x, detailVo);
return detailVo;
}).collect(Collectors.toList()));
return ResultDTO.success(ret);
}
@PostMapping("/listAll")
@ApiPermission(name = "Namespace-ListAll", roleScope = RoleScope.NAMESPACE, requiredPermission = Permission.NONE)
public ResultDTO<List<NamespaceBaseVO>> listAll() {
// 数量应该不是很多先简单处理不查询精简对象
List<NamespaceDO> namespaceRepositoryAll = namespaceWebService.listAll();
List<NamespaceBaseVO> namespaceBaseVOList = namespaceRepositoryAll.stream().map(nd -> {
NamespaceBaseVO nv = new NamespaceBaseVO();
nv.setId(nd.getId());
nv.setCode(nd.getCode());
nv.setName(nd.getName());
nv.genShowName();
return nv;
}).collect(Collectors.toList());
return ResultDTO.success(namespaceBaseVOList);
}
private void fillDetail(NamespaceDO namespaceDO, NamespaceVO namespaceVO) {
Long namespaceId = namespaceVO.getId();
// 权限用户关系
ComponentUserRoleInfo componentUserRoleInfo = webAuthService.fetchComponentUserRoleInfo(RoleScope.NAMESPACE, namespaceId);
namespaceVO.setComponentUserRoleInfo(componentUserRoleInfo);
// 有权限用户填充 token
boolean hasPermission = webAuthService.hasPermission(RoleScope.NAMESPACE, namespaceId, Permission.READ);
namespaceVO.setToken(hasPermission ? namespaceDO.getToken() : AuthConstants.TIPS_NO_PERMISSION_TO_SEE);
// 用户信息
namespaceVO.setCreatorShowName(userWebService.fetchBaseUserInfo(namespaceDO.getCreator()).map(UserBaseVO::getShowName).orElse(null));
namespaceVO.setModifierShowName(userWebService.fetchBaseUserInfo(namespaceDO.getModifier()).map(UserBaseVO::getShowName).orElse(null));
}
}

View File

@ -0,0 +1,45 @@
package tech.powerjob.server.web.controller;
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.server.web.request.ChangePasswordRequest;
import tech.powerjob.server.web.request.ModifyUserInfoRequest;
import tech.powerjob.server.web.service.PwjbUserWebService;
import javax.annotation.Resource;
/**
* PowerJob 自带的登录体系
* 同样视为第三方服务与主框架没有任何关系
*
* @author tjq
* @since 2024/2/13
*/
@RestController
@RequestMapping("/pwjbUser")
public class PwjbUserInfoController {
@Resource
private PwjbUserWebService pwjbUserWebService;
/**
* 创建第三方登录体系PowerJob 的账户不允许修改
* @param request 请求此处复用了主框架请求便于用户一次性把所有参数都填入
* @return 创建结果
*/
@PostMapping("/create")
public ResultDTO<Void> save(@RequestBody ModifyUserInfoRequest request) {
pwjbUserWebService.save(request);
return ResultDTO.success(null);
}
@PostMapping("/changePassword")
public ResultDTO<Void> changePassword(@RequestBody ChangePasswordRequest changePasswordRequest) {
pwjbUserWebService.changePassword(changePasswordRequest);
return ResultDTO.success(null);
}
}

View File

@ -12,6 +12,8 @@ import tech.powerjob.common.enums.InstanceStatus;
import tech.powerjob.common.response.ResultDTO;
import tech.powerjob.server.common.constants.SwitchableStatus;
import tech.powerjob.server.common.module.WorkerInfo;
import tech.powerjob.server.persistence.remote.model.AppInfoDO;
import tech.powerjob.server.persistence.remote.repository.AppInfoRepository;
import tech.powerjob.server.persistence.remote.repository.InstanceInfoRepository;
import tech.powerjob.server.persistence.remote.repository.JobInfoRepository;
import tech.powerjob.server.remote.server.self.ServerInfoService;
@ -21,6 +23,7 @@ import tech.powerjob.server.web.response.WorkerStatusVO;
import java.util.Date;
import java.util.List;
import java.util.Optional;
import java.util.TimeZone;
import java.util.stream.Collectors;
@ -36,6 +39,8 @@ import java.util.stream.Collectors;
@RequiredArgsConstructor
public class SystemInfoController {
private final AppInfoRepository appInfoRepository;
private final JobInfoRepository jobInfoRepository;
private final InstanceInfoRepository instanceInfoRepository;
@ -56,6 +61,14 @@ public class SystemInfoController {
SystemOverviewVO overview = new SystemOverviewVO();
Optional<AppInfoDO> appInfoOpt = appInfoRepository.findById(appId);
if (appInfoOpt.isPresent()) {
AppInfoDO appInfo = appInfoOpt.get();
overview.setAppId(appId);
overview.setAppName(appInfo.getAppName());
}
// 总任务数量
overview.setJobCount(jobInfoRepository.countByAppIdAndStatusNot(appId, SwitchableStatus.DELETED.getV()));
// 运行任务数
@ -69,7 +82,8 @@ public class SystemInfoController {
// 服务器时间
overview.setServerTime(DateFormatUtils.format(new Date(), OmsConstant.TIME_PATTERN));
overview.setServerInfo(serverInfoService.fetchServiceInfo());
overview.setWebServerInfo(serverInfoService.fetchCurrentServerInfo());
overview.setScheduleServerInfo(serverInfoService.fetchAppServerInfo(appId));
return ResultDTO.success(overview);
}

View File

@ -1,21 +1,38 @@
package tech.powerjob.server.web.controller;
import tech.powerjob.common.response.ResultDTO;
import org.springframework.beans.BeanUtils;
import tech.powerjob.server.persistence.remote.model.UserInfoDO;
import tech.powerjob.server.persistence.remote.repository.UserInfoRepository;
import tech.powerjob.server.core.service.UserService;
import tech.powerjob.server.web.request.ModifyUserInfoRequest;
import com.google.common.collect.Lists;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.springframework.util.CollectionUtils;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import lombok.SneakyThrows;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.util.CollectionUtils;
import org.springframework.web.bind.annotation.*;
import tech.powerjob.common.response.ResultDTO;
import tech.powerjob.server.auth.PowerJobUser;
import tech.powerjob.server.auth.Role;
import tech.powerjob.server.auth.RoleScope;
import tech.powerjob.server.auth.common.AuthErrorCode;
import tech.powerjob.server.auth.common.PowerJobAuthException;
import tech.powerjob.server.auth.service.WebAuthService;
import tech.powerjob.server.auth.service.login.PowerJobLoginService;
import tech.powerjob.server.persistence.remote.model.AppInfoDO;
import tech.powerjob.server.persistence.remote.model.NamespaceDO;
import tech.powerjob.server.persistence.remote.model.UserInfoDO;
import tech.powerjob.server.persistence.remote.repository.AppInfoRepository;
import tech.powerjob.server.persistence.remote.repository.NamespaceRepository;
import tech.powerjob.server.persistence.remote.repository.UserInfoRepository;
import tech.powerjob.server.web.converter.NamespaceConverter;
import tech.powerjob.server.web.converter.UserConverter;
import tech.powerjob.server.web.request.ModifyUserInfoRequest;
import tech.powerjob.server.web.response.AppBaseVO;
import tech.powerjob.server.web.response.NamespaceBaseVO;
import tech.powerjob.server.web.response.UserBaseVO;
import tech.powerjob.server.web.response.UserDetailVO;
import javax.annotation.Resource;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import java.util.*;
import java.util.stream.Collectors;
/**
@ -27,22 +44,63 @@ import java.util.stream.Collectors;
@RestController
@RequestMapping("/user")
public class UserInfoController {
@Resource
private UserService userService;
@Resource
private UserInfoRepository userInfoRepository;
@Resource
private PowerJobLoginService powerJobLoginService;
@Resource
private WebAuthService webAuthService;
@Resource
private NamespaceRepository namespaceRepository;
@Resource
private AppInfoRepository appInfoRepository;
@SneakyThrows
@PostMapping("/modify")
public ResultDTO<Void> modifyUser(@RequestBody ModifyUserInfoRequest modifyUserInfoRequest, HttpServletRequest httpServletRequest) {
Optional<PowerJobUser> powerJobUserOpt = powerJobLoginService.ifLogin(httpServletRequest);
if (!powerJobUserOpt.isPresent()) {
throw new PowerJobAuthException(AuthErrorCode.USER_NOT_LOGIN);
}
Long userId = modifyUserInfoRequest.getId();
Optional<UserInfoDO> userOpt = userInfoRepository.findById(userId);
if (!userOpt.isPresent()) {
throw new IllegalArgumentException("can't find user by userId:" + userId);
}
if (!Objects.equals(powerJobUserOpt.get().getId(), userId)) {
throw new IllegalAccessException("no permission to change others user info");
}
UserInfoDO dbUser = userOpt.get();
// 拷入允许修改的内容
if (StringUtils.isNotEmpty(modifyUserInfoRequest.getNick())) {
dbUser.setNick(modifyUserInfoRequest.getNick());
}
if (StringUtils.isNotEmpty(modifyUserInfoRequest.getPhone())) {
dbUser.setPhone(modifyUserInfoRequest.getPhone());
}
if (StringUtils.isNotEmpty(modifyUserInfoRequest.getEmail())) {
dbUser.setEmail(modifyUserInfoRequest.getEmail());
}
if (StringUtils.isNotEmpty(modifyUserInfoRequest.getWebHook())) {
dbUser.setWebHook(modifyUserInfoRequest.getWebHook());
}
if (StringUtils.isNotEmpty(modifyUserInfoRequest.getExtra())) {
dbUser.setExtra(modifyUserInfoRequest.getExtra());
}
dbUser.setGmtModified(new Date());
userInfoRepository.saveAndFlush(dbUser);
@PostMapping("save")
public ResultDTO<Void> save(@RequestBody ModifyUserInfoRequest request) {
UserInfoDO userInfoDO = new UserInfoDO();
BeanUtils.copyProperties(request, userInfoDO);
userService.save(userInfoDO);
return ResultDTO.success(null);
}
@GetMapping("list")
public ResultDTO<List<UserItemVO>> list(@RequestParam(required = false) String name) {
@GetMapping("/list")
public ResultDTO<List<UserBaseVO>> list(@RequestParam(required = false) String name) {
List<UserInfoDO> result;
if (StringUtils.isEmpty(name)) {
@ -53,18 +111,76 @@ public class UserInfoController {
return ResultDTO.success(convert(result));
}
private static List<UserItemVO> convert(List<UserInfoDO> data) {
@GetMapping("/detail")
public ResultDTO<UserDetailVO> getUserDetail(HttpServletRequest httpServletRequest) {
Optional<PowerJobUser> powerJobUserOpt = powerJobLoginService.ifLogin(httpServletRequest);
if (!powerJobUserOpt.isPresent()) {
throw new PowerJobAuthException(AuthErrorCode.USER_NOT_LOGIN);
}
Optional<UserInfoDO> userinfoDoOpt = userInfoRepository.findById(powerJobUserOpt.get().getId());
if (!userinfoDoOpt.isPresent()) {
throw new IllegalArgumentException("can't find user by id: " + powerJobUserOpt.get().getId());
}
UserDetailVO userDetailVO = new UserDetailVO();
BeanUtils.copyProperties(userinfoDoOpt.get(), userDetailVO);
userDetailVO.genShowName();
// 权限信息
Map<Role, List<Long>> globalPermissions = webAuthService.fetchMyPermissionTargets(RoleScope.GLOBAL);
userDetailVO.setGlobalRoles(globalPermissions.keySet().stream().map(Enum::name).collect(Collectors.toList()));
Map<Role, List<Long>> namespacePermissions = webAuthService.fetchMyPermissionTargets(RoleScope.NAMESPACE);
List<NamespaceDO> nsList = namespaceRepository.findAllByIdIn(mergeIds(namespacePermissions));
Map<Long, NamespaceDO> id2NamespaceDo = Maps.newHashMap();
nsList.forEach(x -> id2NamespaceDo.put(x.getId(), x));
Map<String, List<NamespaceBaseVO>> role2NamespaceBaseVo = Maps.newHashMap();
namespacePermissions.forEach((k, v) -> {
List<NamespaceBaseVO> namespaceBaseVOS = Lists.newArrayList();
role2NamespaceBaseVo.put(k.name(), namespaceBaseVOS);
v.forEach(nId -> {
NamespaceDO namespaceDO = id2NamespaceDo.get(nId);
if (namespaceDO == null) {
return;
}
NamespaceBaseVO namespaceBaseVO = NamespaceConverter.do2BaseVo(namespaceDO);
namespaceBaseVOS.add(namespaceBaseVO);
});
});
userDetailVO.setRole2NamespaceList(role2NamespaceBaseVo);
Map<Role, List<Long>> appPermissions = webAuthService.fetchMyPermissionTargets(RoleScope.APP);
List<AppInfoDO> appList = appInfoRepository.findAllByIdIn(mergeIds(appPermissions));
Map<Long, AppInfoDO> id2AppInfo = Maps.newHashMap();
appList.forEach(x -> id2AppInfo.put(x.getId(), x));
Map<String, List<AppBaseVO>> role2AppBaseVo = Maps.newHashMap();
appPermissions.forEach((k, v) -> {
List<AppBaseVO> appBaseVOS = Lists.newArrayList();
role2AppBaseVo.put(k.name(), appBaseVOS);
v.forEach(nId -> {
AppInfoDO appInfoDO = id2AppInfo.get(nId);
if (appInfoDO == null) {
return;
}
AppBaseVO appBaseVO = new AppBaseVO();
BeanUtils.copyProperties(appInfoDO, appBaseVO);
appBaseVOS.add(appBaseVO);
});
});
userDetailVO.setRole2AppList(role2AppBaseVo);
return ResultDTO.success(userDetailVO);
}
private static List<UserBaseVO> convert(List<UserInfoDO> data) {
if (CollectionUtils.isEmpty(data)) {
return Lists.newLinkedList();
}
return data.stream().map(x -> new UserItemVO(x.getId(), x.getUsername())).collect(Collectors.toList());
return data.stream().map(UserConverter::do2BaseVo).collect(Collectors.toList());
}
@Getter
@NoArgsConstructor
@AllArgsConstructor
public static final class UserItemVO {
private Long id;
private String username;
private static Set<Long> mergeIds(Map<?, List<Long>> map) {
Set<Long> ids = Sets.newHashSet();
map.values().forEach(ids::addAll);
return ids;
}
}

View File

@ -1,26 +1,28 @@
package tech.powerjob.server.web.controller;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.web.bind.annotation.*;
import tech.powerjob.common.request.http.SaveWorkflowNodeRequest;
import tech.powerjob.common.request.http.SaveWorkflowRequest;
import tech.powerjob.common.response.ResultDTO;
import tech.powerjob.server.auth.Permission;
import tech.powerjob.server.auth.RoleScope;
import tech.powerjob.server.auth.interceptor.ApiPermission;
import tech.powerjob.server.common.constants.SwitchableStatus;
import tech.powerjob.server.core.workflow.WorkflowService;
import tech.powerjob.server.persistence.PageResult;
import tech.powerjob.server.persistence.remote.model.WorkflowInfoDO;
import tech.powerjob.server.persistence.remote.model.WorkflowNodeInfoDO;
import tech.powerjob.server.persistence.remote.repository.WorkflowInfoRepository;
import tech.powerjob.server.core.workflow.WorkflowService;
import tech.powerjob.server.web.request.QueryWorkflowInfoRequest;
import tech.powerjob.server.web.response.WorkflowInfoVO;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.apache.commons.lang3.StringUtils;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.text.ParseException;
import java.util.List;
import java.util.function.LongToDoubleFunction;
import java.util.stream.Collectors;
/**
@ -40,34 +42,40 @@ public class WorkflowController {
private WorkflowInfoRepository workflowInfoRepository;
@PostMapping("/save")
@ApiPermission(name = "Workflow-Save", roleScope = RoleScope.APP, requiredPermission = Permission.WRITE)
public ResultDTO<Long> save(@RequestBody SaveWorkflowRequest req) throws ParseException {
return ResultDTO.success(workflowService.saveWorkflow(req));
}
@PostMapping("/copy")
@ApiPermission(name = "Workflow-Copy", roleScope = RoleScope.APP, requiredPermission = Permission.WRITE)
public ResultDTO<Long> copy(Long workflowId, Long appId) {
return ResultDTO.success(workflowService.copyWorkflow(workflowId,appId));
}
@GetMapping("/disable")
@ApiPermission(name = "Workflow-Disable", roleScope = RoleScope.APP, requiredPermission = Permission.WRITE)
public ResultDTO<Void> disableWorkflow(Long workflowId, Long appId) {
workflowService.disableWorkflow(workflowId, appId);
return ResultDTO.success(null);
}
@GetMapping("/enable")
@ApiPermission(name = "Workflow-Enable", roleScope = RoleScope.APP, requiredPermission = Permission.WRITE)
public ResultDTO<Void> enableWorkflow(Long workflowId, Long appId) {
workflowService.enableWorkflow(workflowId, appId);
return ResultDTO.success(null);
}
@GetMapping("/delete")
@ApiPermission(name = "Workflow-Delete", roleScope = RoleScope.APP, requiredPermission = Permission.WRITE)
public ResultDTO<Void> deleteWorkflow(Long workflowId, Long appId) {
workflowService.deleteWorkflow(workflowId, appId);
return ResultDTO.success(null);
}
@PostMapping("/list")
@ApiPermission(name = "Workflow-List", roleScope = RoleScope.APP, requiredPermission = Permission.READ)
public ResultDTO<PageResult<WorkflowInfoVO>> list(@RequestBody QueryWorkflowInfoRequest req) {
Sort sort = Sort.by(Sort.Direction.DESC, "gmtCreate");
@ -89,6 +97,7 @@ public class WorkflowController {
}
@GetMapping("/run")
@ApiPermission(name = "Workflow-Run", roleScope = RoleScope.APP, requiredPermission = Permission.OPS)
public ResultDTO<Long> runWorkflow(Long workflowId, Long appId,
@RequestParam(required = false,defaultValue = "0") Long delay,
@RequestParam(required = false) String initParams
@ -97,12 +106,14 @@ public class WorkflowController {
}
@GetMapping("/fetch")
@ApiPermission(name = "Workflow-Fetch", roleScope = RoleScope.APP, requiredPermission = Permission.READ)
public ResultDTO<WorkflowInfoVO> fetchWorkflow(Long workflowId, Long appId) {
WorkflowInfoDO workflowInfoDO = workflowService.fetchWorkflow(workflowId, appId);
return ResultDTO.success(WorkflowInfoVO.from(workflowInfoDO));
}
@PostMapping("/saveNode")
@ApiPermission(name = "Workflow-SaveNode", roleScope = RoleScope.APP, requiredPermission = Permission.WRITE)
public ResultDTO<List<WorkflowNodeInfoDO>> addWorkflowNode(@RequestBody List<SaveWorkflowNodeRequest> request) {
return ResultDTO.success(workflowService.saveWorkflowNode(request));
}

Some files were not shown because too many files have changed in this diff Show More