feat: 新增能力开放模块应用管理功能

This commit is contained in:
cz176
2024-10-27 09:48:22 +08:00
parent 4525cb3531
commit f7741832bd
19 changed files with 1186 additions and 5 deletions

View File

@@ -50,6 +50,12 @@
<artifactId>continew-admin-generator</artifactId>
</dependency>
<!-- 能力开放模块包括应用管理、API开放授权、API开发等 -->
<dependency>
<groupId>top.continew</groupId>
<artifactId>continew-admin-open</artifactId>
</dependency>
<!-- Liquibase用于管理数据库版本跟踪、管理和应用数据库变化 -->
<dependency>
<groupId>org.liquibase</groupId>

View File

@@ -16,8 +16,12 @@
package top.continew.admin.config.satoken;
import cn.dev33.satoken.SaManager;
import cn.dev33.satoken.context.SaHolder;
import cn.dev33.satoken.context.model.SaRequest;
import cn.dev33.satoken.interceptor.SaInterceptor;
import cn.dev33.satoken.router.SaRouter;
import cn.dev33.satoken.sign.SaSignUtil;
import cn.dev33.satoken.stp.StpInterface;
import cn.dev33.satoken.stp.StpUtil;
import lombok.RequiredArgsConstructor;
@@ -25,10 +29,13 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import top.continew.admin.common.context.UserContext;
import top.continew.admin.common.context.UserContextHolder;
import top.continew.admin.open.sign.OpenSignTemplate;
import top.continew.starter.auth.satoken.autoconfigure.SaTokenExtensionProperties;
import top.continew.starter.core.constant.StringConstants;
import top.continew.starter.core.util.validate.CheckUtils;
import java.util.List;
/**
* Sa-Token 配置
*
@@ -41,6 +48,7 @@ public class SaTokenConfiguration {
private final SaTokenExtensionProperties properties;
private final LoginPasswordProperties loginPasswordProperties;
private final OpenSignTemplate openSignTemplate;
/**
* Sa-Token 权限认证配置
@@ -55,15 +63,31 @@ public class SaTokenConfiguration {
*/
@Bean
public SaInterceptor saInterceptor() {
SaManager.setSaSignTemplate(openSignTemplate);
return new SaExtensionInterceptor(handle -> SaRouter.match(StringConstants.PATH_PATTERN)
.notMatch(properties.getSecurity().getExcludes())
.check(r -> {
StpUtil.checkLogin();
if (SaRouter.isMatchCurrURI(loginPasswordProperties.getExcludes())) {
return;
// 拦截验证sign
// 判断是否包含sign参数
SaRequest saRequest = SaHolder.getRequest();
List<String> paramNames = saRequest.getParamNames();
boolean matchParamSign = paramNames.stream().anyMatch(paramName -> paramName.equals("sign"));
// 如果包含sign参数走SaToken API接口参数签名验证
if (matchParamSign) {
try {
SaSignUtil.checkRequest(saRequest);
} catch (Exception e) {
CheckUtils.throwIf(true, e.getMessage());
}
} else {
// 如果不包含sign参数走登录token验证
StpUtil.checkLogin();
if (SaRouter.isMatchCurrURI(loginPasswordProperties.getExcludes())) {
return;
}
UserContext userContext = UserContextHolder.getContext();
CheckUtils.throwIf(userContext.isPasswordExpired(), "密码已过期,请修改密码");
}
UserContext userContext = UserContextHolder.getContext();
CheckUtils.throwIf(userContext.isPasswordExpired(), "密码已过期,请修改密码");
}));
}
}

View File

@@ -0,0 +1,79 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.admin.controller.open;
import cn.dev33.satoken.annotation.SaCheckPermission;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.enums.ParameterIn;
import lombok.RequiredArgsConstructor;
import top.continew.admin.open.model.resp.AppSecretGetResp;
import top.continew.starter.extension.crud.enums.Api;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.web.bind.annotation.*;
import top.continew.starter.extension.crud.annotation.CrudRequestMapping;
import top.continew.starter.extension.crud.controller.BaseController;
import top.continew.admin.open.model.query.AppQuery;
import top.continew.admin.open.model.req.AppReq;
import top.continew.admin.open.model.resp.AppDetailResp;
import top.continew.admin.open.model.resp.AppResp;
import top.continew.admin.open.service.AppService;
import top.continew.starter.extension.crud.model.resp.BaseIdResp;
/**
* 应用管理 API
*
* @author chengzi
* @since 2024/10/17 16:03
*/
@Tag(name = "应用管理 API")
@RestController
@RequiredArgsConstructor
@CrudRequestMapping(value = "/open/app", api = {Api.PAGE, Api.GET, Api.ADD, Api.UPDATE, Api.DELETE, Api.EXPORT})
public class AppController extends BaseController<AppService, AppResp, AppDetailResp, AppQuery, AppReq> {
private final AppService appService;
private final static String APP_DISABLED_KEY = "0";
@Operation(summary = "刷新应用密码", description = "刷新应用密码")
@Parameter(name = "id", description = "ID", example = "test", in = ParameterIn.PATH)
@SaCheckPermission("open:app:refreshas")
@GetMapping(value = "/{id}/refreshas")
public void refreshAppSecret(@PathVariable Long id) {
appService.refreshAppSecretByID(id);
}
@Operation(summary = "获取应用密码", description = "获取应用密码")
@Parameter(name = "appKey", description = "应用密钥", example = "test", in = ParameterIn.PATH)
@SaCheckPermission("open:app:getas")
@GetMapping("/{id}/appsecret")
public AppSecretGetResp getAppSecret(@PathVariable Long id) {
return appService.getAppSecretById(id);
}
@Override
public BaseIdResp<Long> add(AppReq req) {
BaseIdResp<Long> baseIdResp = super.add(req);
Long appId = baseIdResp.getId();
appService.refreshAppSecretByID(appId);
appService.resetAppSecretStatusById(appId, APP_DISABLED_KEY);
return baseIdResp;
}
}

View File

@@ -0,0 +1,193 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.extension.crud.controller;
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.lang.tree.Tree;
import cn.hutool.core.text.CharSequenceUtil;
import com.feiniaojin.gracefulresponse.api.ExcludeFromGracefulResponse;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.enums.ParameterIn;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import top.continew.admin.open.util.ApiSignCheckUtils;
import top.continew.starter.core.constant.StringConstants;
import top.continew.starter.extension.crud.annotation.CrudRequestMapping;
import top.continew.starter.extension.crud.enums.Api;
import top.continew.starter.extension.crud.model.query.PageQuery;
import top.continew.starter.extension.crud.model.query.SortQuery;
import top.continew.starter.extension.crud.model.req.BaseReq;
import top.continew.starter.extension.crud.model.resp.BaseIdResp;
import top.continew.starter.extension.crud.model.resp.PageResp;
import top.continew.starter.extension.crud.service.BaseService;
import top.continew.starter.extension.crud.util.ValidateGroup;
import java.util.List;
/**
* 控制器基类
*
* @param <S> 业务接口
* @param <L> 列表类型
* @param <D> 详情类型
* @param <Q> 查询条件
* @param <C> 创建或修改类型
* @author Charles7c
* @since 1.0.0
*/
public abstract class BaseController<S extends BaseService<L, D, Q, C>, L, D, Q, C extends BaseReq> {
@Autowired
protected S baseService;
/**
* 分页查询列表
*
* @param query 查询条件
* @param pageQuery 分页查询条件
* @return 分页信息
*/
@Operation(summary = "分页查询列表", description = "分页查询列表")
@ResponseBody
@GetMapping
public PageResp<L> page(Q query, @Validated PageQuery pageQuery) {
this.checkPermission(Api.LIST);
return baseService.page(query, pageQuery);
}
/**
* 查询树列表
*
* @param query 查询条件
* @param sortQuery 排序查询条件
* @return 树列表信息
*/
@Operation(summary = "查询树列表", description = "查询树列表")
@ResponseBody
@GetMapping("/tree")
public List<Tree<Long>> tree(Q query, SortQuery sortQuery) {
this.checkPermission(Api.LIST);
return baseService.tree(query, sortQuery, false);
}
/**
* 查询列表
*
* @param query 查询条件
* @param sortQuery 排序查询条件
* @return 列表信息
*/
@Operation(summary = "查询列表", description = "查询列表")
@ResponseBody
@GetMapping("/list")
public List<L> list(Q query, SortQuery sortQuery) {
this.checkPermission(Api.LIST);
return baseService.list(query, sortQuery);
}
/**
* 查询详情
*
* @param id ID
* @return 详情信息
*/
@Operation(summary = "查询详情", description = "查询详情")
@Parameter(name = "id", description = "ID", example = "1", in = ParameterIn.PATH)
@ResponseBody
@GetMapping("/{id}")
public D get(@PathVariable("id") Long id) {
this.checkPermission(Api.LIST);
return baseService.get(id);
}
/**
* 新增
*
* @param req 创建信息
* @return 自增 ID
*/
@Operation(summary = "新增数据", description = "新增数据")
@ResponseBody
@PostMapping
public BaseIdResp<Long> add(@Validated(ValidateGroup.Crud.Add.class) @RequestBody C req) {
this.checkPermission(Api.ADD);
return BaseIdResp.<Long>builder().id(baseService.add(req)).build();
}
/**
* 修改
*
* @param req 修改信息
* @param id ID
*/
@Operation(summary = "修改数据", description = "修改数据")
@Parameter(name = "id", description = "ID", example = "1", in = ParameterIn.PATH)
@ResponseBody
@PutMapping("/{id}")
public void update(@Validated(ValidateGroup.Crud.Update.class) @RequestBody C req, @PathVariable("id") Long id) {
this.checkPermission(Api.UPDATE);
baseService.update(req, id);
}
/**
* 删除
*
* @param ids ID 列表
*/
@Operation(summary = "删除数据", description = "删除数据")
@Parameter(name = "ids", description = "ID 列表", example = "1,2", in = ParameterIn.PATH)
@ResponseBody
@DeleteMapping("/{ids}")
public void delete(@PathVariable("ids") List<Long> ids) {
this.checkPermission(Api.DELETE);
baseService.delete(ids);
}
/**
* 导出
*
* @param query 查询条件
* @param sortQuery 排序查询条件
* @param response 响应对象
*/
@ExcludeFromGracefulResponse
@Operation(summary = "导出数据", description = "导出数据")
@GetMapping("/export")
public void export(Q query, SortQuery sortQuery, HttpServletResponse response) {
this.checkPermission(Api.EXPORT);
baseService.export(query, sortQuery, response);
}
/**
* 根据 API 类型进行权限验证
*
* @param api API 类型
*/
protected void checkPermission(Api api) {
// 判断是否包含sign参数
if (!ApiSignCheckUtils.isExistSignParam()) {
CrudRequestMapping crudRequestMapping = this.getClass().getDeclaredAnnotation(CrudRequestMapping.class);
String path = crudRequestMapping.value();
String permissionPrefix = String.join(StringConstants.COLON, CharSequenceUtil
.splitTrim(path, StringConstants.SLASH));
StpUtil.checkPermission("%s:%s".formatted(permissionPrefix, api.name().toLowerCase()));
}
}
}

View File

@@ -4,3 +4,34 @@ ALTER TABLE sys_notice
ADD COLUMN notice_scope INT NOT NULL COMMENT '通知范围' AFTER terminate_time,
ADD COLUMN notice_users JSON DEFAULT NULL COMMENT '通知用户' AFTER notice_scope;
COMMIT;
-- changeset chengzi
-- comment 新增应用表
CREATE TABLE IF NOT EXISTS `sys_app` (
`id` bigint(0) NOT NULL COMMENT 'ID',
`name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '应用名称',
`app_key` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '应用密钥',
`app_secret` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '应用密码',
`status` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '应用状态(0:未激活1:激活)',
`expiration_time` datetime(0) NULL DEFAULT NULL COMMENT '失效时间',
`app_desc` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '应用描述',
`secret_status` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '应用密码查看状态(0:未查看1:已查看)',
`create_user` bigint(0) NOT NULL COMMENT '创建人',
`create_time` datetime(0) NOT NULL COMMENT '创建时间',
`update_user` bigint(0) NULL DEFAULT NULL COMMENT '修改人',
`update_time` datetime(0) NULL DEFAULT NULL COMMENT '修改时间',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='应用表';
INSERT INTO `sys_app` VALUES (639144999463690263, 'ContiNewAdmin', 'admin', '9efcf8859d754288941e61adc72cd250', '1', '2024-10-31 16:53:52', 'ContiNew AdminContinue New Admin持续迭代优化的前后端分离中后台管理系统框架。开箱即用重视每一处代码规范重视每一种解决方案细节持续提供舒适的前、后端开发体验。', '1', 1, '2024-10-22 16:54:03', NULL, NULL);
-- 应用管理菜单数据
INSERT INTO `sys_menu` VALUES (635516486647025735, '能力开放', 0, 1, '/open', 'Open', 'Layout', NULL, 'expand', b'0', b'0', b'0', NULL, 2, 1, 1, '2024-10-12 16:35:38', 1, '2024-10-12 16:35:53');
INSERT INTO `sys_menu` VALUES (635516794676711501, '应用管理', 635516486647025735, 2, '/open/app', 'OpenApp', 'open/app/index', NULL, 'common', b'0', b'0', b'0', NULL, 1, 1, 1, '2024-10-12 16:36:51', 1, '2024-10-12 16:49:11');
INSERT INTO `sys_menu` VALUES (636598391530328174, '新增', 635516794676711501, 3, NULL, NULL, NULL, NULL, NULL, b'0', b'0', b'0', 'open:app:add', 1, 1, 1, '2024-10-15 16:14:44', 1, '2024-10-15 16:45:36');
INSERT INTO `sys_menu` VALUES (636599310447808642, '查看', 635516794676711501, 3, NULL, NULL, NULL, NULL, NULL, b'0', b'0', b'0', 'open:app:list', 999, 1, 1, '2024-10-15 16:18:23', NULL, NULL);
INSERT INTO `sys_menu` VALUES (636599448054534277, '导出', 635516794676711501, 3, NULL, NULL, NULL, NULL, NULL, b'0', b'0', b'0', 'open:app:export', 999, 1, 1, '2024-10-15 16:18:56', NULL, NULL);
INSERT INTO `sys_menu` VALUES (637299919924760580, '删除', 635516794676711501, 3, NULL, NULL, NULL, NULL, NULL, b'0', b'0', b'0', 'open:app:delete', 1, 1, 1, '2024-10-17 14:42:21', NULL, NULL);
INSERT INTO `sys_menu` VALUES (637300206014042119, '修改', 635516794676711501, 3, NULL, NULL, NULL, NULL, NULL, b'0', b'0', b'0', 'open:app:update', 1, 1, 1, '2024-10-17 14:43:30', NULL, NULL);
-- 应用管理字典数据
INSERT INTO `sys_dict` VALUES (639152724557963332, '应用状态', 'app_type', NULL, b'0', 1, '2024-10-22 17:24:44', NULL, NULL);
INSERT INTO `sys_dict_item` VALUES (639152915579150411, '禁用', '0', 'blue', 999, NULL, 1, 639152724557963332, 1, '2024-10-22 17:25:30', NULL, NULL);
INSERT INTO `sys_dict_item` VALUES (639153003290435665, '启用', '1', 'green', 999, NULL, 1, 639152724557963332, 1, '2024-10-22 17:25:51', NULL, NULL);