diff --git a/continew-admin-open/pom.xml b/continew-admin-open/pom.xml new file mode 100644 index 00000000..a1724ac0 --- /dev/null +++ b/continew-admin-open/pom.xml @@ -0,0 +1,23 @@ + + + 4.0.0 + + top.continew + continew-admin + ${revision} + + + continew-admin-open + 能力开放模块(包括应用管理、API开放授权、API开发等) + + + + + top.continew + continew-admin-common + + + + \ No newline at end of file diff --git a/continew-admin-open/src/main/java/top/continew/admin/open/handler/SaCheckPermissionHandler.java b/continew-admin-open/src/main/java/top/continew/admin/open/handler/SaCheckPermissionHandler.java new file mode 100644 index 00000000..3a311119 --- /dev/null +++ b/continew-admin-open/src/main/java/top/continew/admin/open/handler/SaCheckPermissionHandler.java @@ -0,0 +1,49 @@ +/* + * 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.open.handler; + +import cn.dev33.satoken.annotation.SaCheckPermission; +import cn.dev33.satoken.annotation.handler.SaAnnotationHandlerInterface; +import org.springframework.stereotype.Component; +import top.continew.admin.open.util.ApiSignCheckUtils; + +import java.lang.reflect.Method; + +import static cn.dev33.satoken.annotation.handler.SaCheckPermissionHandler._checkMethod; + +/** + * 重定义注解 SaCheckPermission 的处理器 + * + * @author chengzi + * @since 2024/10/25 12:03 + */ +@Component +public class SaCheckPermissionHandler implements SaAnnotationHandlerInterface { + + @Override + public Class getHandlerAnnotationClass() { + return SaCheckPermission.class; + } + + @Override + public void checkMethod(SaCheckPermission at, Method method) { + if (!ApiSignCheckUtils.isExistSignParam()) { + _checkMethod(at.type(), at.value(), at.mode(), at.orRole()); + } + } + +} diff --git a/continew-admin-open/src/main/java/top/continew/admin/open/mapper/AppMapper.java b/continew-admin-open/src/main/java/top/continew/admin/open/mapper/AppMapper.java new file mode 100644 index 00000000..121ac571 --- /dev/null +++ b/continew-admin-open/src/main/java/top/continew/admin/open/mapper/AppMapper.java @@ -0,0 +1,28 @@ +/* + * 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.open.mapper; + +import top.continew.starter.data.mp.base.BaseMapper; +import top.continew.admin.open.model.entity.AppDO; + +/** + * 应用 Mapper + * + * @author chengzi + * @since 2024/10/17 16:03 + */ +public interface AppMapper extends BaseMapper {} \ No newline at end of file diff --git a/continew-admin-open/src/main/java/top/continew/admin/open/model/entity/AppDO.java b/continew-admin-open/src/main/java/top/continew/admin/open/model/entity/AppDO.java new file mode 100644 index 00000000..6e11eca5 --- /dev/null +++ b/continew-admin-open/src/main/java/top/continew/admin/open/model/entity/AppDO.java @@ -0,0 +1,80 @@ +/* + * 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.open.model.entity; + +import java.io.Serial; +import java.time.*; + +import lombok.Data; + +import com.baomidou.mybatisplus.annotation.TableName; + +import top.continew.starter.extension.crud.model.entity.BaseDO; + +/** + * 应用实体 + * + * @author chengzi + * @since 2024/10/17 16:03 + */ +@Data +@TableName("sys_app") +public class AppDO extends BaseDO { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * ID + */ + private Long id; + + /** + * 应用名称 + */ + private String name; + + /** + * APPKEY + */ + private String appKey; + + /** + * APPSECRET + */ + private String appSecret; + + /** + * 应用状态 + */ + private String status; + + /** + * 失效时间 + */ + private LocalDateTime expirationTime; + + /** + * 应用描述 + */ + private String appDesc; + + /** + * secret查看状态 + */ + private String secretStatus; +} \ No newline at end of file diff --git a/continew-admin-open/src/main/java/top/continew/admin/open/model/query/AppQuery.java b/continew-admin-open/src/main/java/top/continew/admin/open/model/query/AppQuery.java new file mode 100644 index 00000000..0c843751 --- /dev/null +++ b/continew-admin-open/src/main/java/top/continew/admin/open/model/query/AppQuery.java @@ -0,0 +1,55 @@ +/* + * 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.open.model.query; + +import java.io.Serial; +import java.io.Serializable; + +import lombok.Data; + +import io.swagger.v3.oas.annotations.media.Schema; + +import top.continew.starter.data.core.annotation.Query; +import top.continew.starter.data.core.enums.QueryType; + +/** + * 应用查询条件 + * + * @author chengzi + * @since 2024/10/17 16:03 + */ +@Data +@Schema(description = "应用查询条件") +public class AppQuery implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 应用名称 + */ + @Schema(description = "应用名称") + @Query(type = QueryType.LIKE) + private String name; + + /** + * APPKEY + */ + @Schema(description = "APPKEY") + @Query(type = QueryType.LIKE) + private String appKey; +} \ No newline at end of file diff --git a/continew-admin-open/src/main/java/top/continew/admin/open/model/req/AppReq.java b/continew-admin-open/src/main/java/top/continew/admin/open/model/req/AppReq.java new file mode 100644 index 00000000..de21a2b6 --- /dev/null +++ b/continew-admin-open/src/main/java/top/continew/admin/open/model/req/AppReq.java @@ -0,0 +1,82 @@ +/* + * 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.open.model.req; + +import java.io.Serial; +import java.time.*; + +import jakarta.validation.constraints.*; + +import lombok.Data; + +import io.swagger.v3.oas.annotations.media.Schema; + +import org.hibernate.validator.constraints.Length; + +import top.continew.starter.extension.crud.model.req.BaseReq; + +/** + * 创建或修改应用信息 + * + * @author chengzi + * @since 2024/10/17 16:03 + */ +@Data +@Schema(description = "创建或修改应用信息") +public class AppReq extends BaseReq { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 应用名称 + */ + @Schema(description = "应用名称") + @NotBlank(message = "应用名称不能为空") + @Length(max = 255, message = "应用名称长度不能超过 {max} 个字符") + private String name; + + /** + * APPKEY + */ + @Schema(description = "应用密钥") + @NotBlank(message = "应用密钥不能为空") + @Length(max = 255, message = "应用密钥长度不能超过 {max} 个字符") + private String appKey; + + /** + * 应用状态 + */ + @Schema(description = "应用状态") + @NotBlank(message = "应用状态不能为空") + @Length(max = 255, message = "应用状态长度不能超过 {max} 个字符") + private String status; + + /** + * 失效时间 + */ + @Schema(description = "失效时间") + @NotNull(message = "失效时间不能为空") + private LocalDateTime expirationTime; + + /** + * 应用描述 + */ + @Schema(description = "应用描述") + @Length(max = 255, message = "应用描述长度不能超过 {max} 个字符") + private String appDesc; +} \ No newline at end of file diff --git a/continew-admin-open/src/main/java/top/continew/admin/open/model/resp/AppDetailResp.java b/continew-admin-open/src/main/java/top/continew/admin/open/model/resp/AppDetailResp.java new file mode 100644 index 00000000..7a3342d8 --- /dev/null +++ b/continew-admin-open/src/main/java/top/continew/admin/open/model/resp/AppDetailResp.java @@ -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.open.model.resp; + +import java.io.Serial; +import java.time.*; + +import lombok.Data; + +import io.swagger.v3.oas.annotations.media.Schema; + +import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; +import com.alibaba.excel.annotation.ExcelProperty; + +import top.continew.starter.extension.crud.model.resp.BaseDetailResp; + +/** + * 应用详情信息 + * + * @author chengzi + * @since 2024/10/17 16:03 + */ +@Data +@ExcelIgnoreUnannotated +@Schema(description = "应用详情信息") +public class AppDetailResp extends BaseDetailResp { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 应用名称 + */ + @Schema(description = "应用名称") + @ExcelProperty(value = "应用名称") + private String name; + + /** + * 应用密钥 + */ + @Schema(description = "应用密钥") + @ExcelProperty(value = "应用密钥") + private String appKey; + + /** + * 应用状态 + */ + @Schema(description = "应用状态") + @ExcelProperty(value = "应用状态") + private String status; + + /** + * 失效时间 + */ + @Schema(description = "失效时间") + @ExcelProperty(value = "失效时间") + private LocalDateTime expirationTime; + + /** + * 应用描述 + */ + @Schema(description = "应用描述") + @ExcelProperty(value = "应用描述") + private String appDesc; +} \ No newline at end of file diff --git a/continew-admin-open/src/main/java/top/continew/admin/open/model/resp/AppResp.java b/continew-admin-open/src/main/java/top/continew/admin/open/model/resp/AppResp.java new file mode 100644 index 00000000..8457ef90 --- /dev/null +++ b/continew-admin-open/src/main/java/top/continew/admin/open/model/resp/AppResp.java @@ -0,0 +1,71 @@ +/* + * 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.open.model.resp; + +import java.io.Serial; +import java.time.*; + +import lombok.Data; + +import io.swagger.v3.oas.annotations.media.Schema; + +import top.continew.starter.extension.crud.model.resp.BaseResp; + +/** + * 应用信息 + * + * @author chengzi + * @since 2024/10/17 16:03 + */ +@Data +@Schema(description = "应用信息") +public class AppResp extends BaseResp { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 应用名称 + */ + @Schema(description = "应用名称") + private String name; + + /** + * APPKEY + */ + @Schema(description = "应用密钥") + private String appKey; + + /** + * 应用状态 + */ + @Schema(description = "应用状态") + private String status; + + /** + * 失效时间 + */ + @Schema(description = "失效时间") + private LocalDateTime expirationTime; + + /** + * 应用描述 + */ + @Schema(description = "应用描述") + private String appDesc; + +} \ No newline at end of file diff --git a/continew-admin-open/src/main/java/top/continew/admin/open/model/resp/AppSecretGetResp.java b/continew-admin-open/src/main/java/top/continew/admin/open/model/resp/AppSecretGetResp.java new file mode 100644 index 00000000..cb04f07d --- /dev/null +++ b/continew-admin-open/src/main/java/top/continew/admin/open/model/resp/AppSecretGetResp.java @@ -0,0 +1,47 @@ +/* + * 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.open.model.resp; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serial; + +/** + * 应用密钥/密码信息 + * + * @author chengzi + * @since 2024/10/17 16:03 + */ +@Data +@Schema(description = "应用密钥/密码信息") +public class AppSecretGetResp { + @Serial + private static final long serialVersionUID = 1L; + + /** + * 应用密钥 + */ + @Schema(description = "应用密钥") + private String appKey; + + /** + * 应用密码 + */ + @Schema(description = "应用密码") + private String appSecret; +} diff --git a/continew-admin-open/src/main/java/top/continew/admin/open/service/AppService.java b/continew-admin-open/src/main/java/top/continew/admin/open/service/AppService.java new file mode 100644 index 00000000..7ef07137 --- /dev/null +++ b/continew-admin-open/src/main/java/top/continew/admin/open/service/AppService.java @@ -0,0 +1,86 @@ +/* + * 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.open.service; + +import top.continew.admin.open.model.resp.AppSecretGetResp; +import top.continew.starter.extension.crud.service.BaseService; +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; + +/** + * 应用业务接口 + * + * @author chengzi + * @since 2024/10/17 16:03 + */ +public interface AppService extends BaseService { + /** + * 根据ID查询应用密码 + * + * @param id ID + * @return 应用密码 + */ + AppSecretGetResp getAppSecretById(Long id); + + /** + * 根据ID重置应用密码查看状态 + * + * @param id ID + */ + void resetAppSecretStatusById(Long id, String status); + + /** + * 根据应用密钥重置应用密码查看状态 + * + * @param appKey 应用密钥 + */ + void resetAppSecretStatusByAppkey(String appKey, String status); + + /** + * 根据ID刷新应用密码 + * + * @param id ID + */ + void refreshAppSecretByID(Long id); + + /** + * 根据应用密钥获取应用密码 + * + * @param appKey 应用密钥 + * @return 应用密码 + */ + String getAppSecretByAppKey(String appKey); + + /** + * 判断应用密钥是否存在 + * + * @param appKey 应用密钥 + * @return 是否存在(true:存在;false:不存在) + */ + boolean isExistAppKey(String appKey); + + /** + * 判断应用密钥是否过期 + * + * @param appKey 应用密钥 + * @return 是否过期(true:已过期;false:未过期) + */ + boolean isExpireAppKey(String appKey); + +} \ No newline at end of file diff --git a/continew-admin-open/src/main/java/top/continew/admin/open/service/impl/AppServiceImpl.java b/continew-admin-open/src/main/java/top/continew/admin/open/service/impl/AppServiceImpl.java new file mode 100644 index 00000000..1a25b16e --- /dev/null +++ b/continew-admin-open/src/main/java/top/continew/admin/open/service/impl/AppServiceImpl.java @@ -0,0 +1,102 @@ +/* + * 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.open.service.impl; + +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.date.LocalDateTimeUtil; +import cn.hutool.core.util.IdUtil; +import lombok.RequiredArgsConstructor; + +import org.springframework.stereotype.Service; + +import top.continew.admin.open.model.resp.AppSecretGetResp; +import top.continew.starter.extension.crud.service.impl.BaseServiceImpl; +import top.continew.admin.open.mapper.AppMapper; +import top.continew.admin.open.model.entity.AppDO; +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 java.time.LocalDateTime; + +/** + * 应用业务实现 + * + * @author chengzi + * @since 2024/10/17 16:03 + */ +@Service +@RequiredArgsConstructor +public class AppServiceImpl extends BaseServiceImpl implements AppService { + + // 已激活 + private final static String APP_ENABLED_KEY = "1"; + // 未激活 + private final static String APP_DISABLED_KEY = "0"; + + @Override + public AppSecretGetResp getAppSecretById(Long id) { + AppDO app = baseMapper.lambdaQuery().eq(AppDO::getId, id).one(); + String appSecret = "********"; + if (app.getSecretStatus().equals(APP_DISABLED_KEY)) { + appSecret = app.getAppSecret(); + this.resetAppSecretStatusById(id, APP_ENABLED_KEY); + } + AppSecretGetResp appSecretGetResp = new AppSecretGetResp(); + appSecretGetResp.setAppKey(app.getAppKey()); + appSecretGetResp.setAppSecret(appSecret); + return appSecretGetResp; + } + + @Override + public void resetAppSecretStatusById(Long id, String status) { + baseMapper.lambdaUpdate().set(AppDO::getSecretStatus, status).eq(AppDO::getId, id).update(); + } + + @Override + public void resetAppSecretStatusByAppkey(String appKey, String status) { + baseMapper.lambdaUpdate().set(AppDO::getSecretStatus, status).eq(AppDO::getAppKey, appKey).update(); + } + + @Override + public void refreshAppSecretByID(Long id) { + baseMapper.lambdaUpdate().set(AppDO::getAppSecret, IdUtil.simpleUUID()).eq(AppDO::getId, id).update(); + this.resetAppSecretStatusById(id, APP_DISABLED_KEY); + } + + @Override + public String getAppSecretByAppKey(String appKey) { + return baseMapper.lambdaQuery().select(AppDO::getAppSecret).eq(AppDO::getAppKey, appKey).one().getAppSecret(); + } + + @Override + public boolean isExistAppKey(String appKey) { + return baseMapper.lambdaQuery().eq(AppDO::getAppKey, appKey).exists(); + } + + @Override + public boolean isExpireAppKey(String appKey) { + LocalDateTime expirationTime = baseMapper.lambdaQuery() + .select(AppDO::getExpirationTime) + .eq(AppDO::getAppKey, appKey) + .one() + .getExpirationTime(); + return expirationTime.isBefore(LocalDateTimeUtil.of(DateUtil.date())); + } +} \ No newline at end of file diff --git a/continew-admin-open/src/main/java/top/continew/admin/open/sign/OpenSignTemplate.java b/continew-admin-open/src/main/java/top/continew/admin/open/sign/OpenSignTemplate.java new file mode 100644 index 00000000..ad6984dd --- /dev/null +++ b/continew-admin-open/src/main/java/top/continew/admin/open/sign/OpenSignTemplate.java @@ -0,0 +1,92 @@ +/* + * 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.open.sign; + +import cn.dev33.satoken.error.SaErrorCode; +import cn.dev33.satoken.exception.SaSignException; +import cn.dev33.satoken.sign.SaSignTemplate; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import top.continew.admin.open.service.AppService; + +import java.util.Map; +import java.util.TreeMap; + +import static cn.dev33.satoken.SaManager.log; + +@Component +@RequiredArgsConstructor +public class OpenSignTemplate extends SaSignTemplate { + + private final AppService appService; + public static String appKey = "appkey"; + + @Override + public void checkParamMap(Map paramMap) { + // 获取必须的参数 + String timestampValue = paramMap.get(timestamp); + String nonceValue = paramMap.get(nonce); + String signValue = paramMap.get(sign); + String appKeyValue = paramMap.get(appKey); + + // 参数非空校验 + SaSignException.notEmpty(timestampValue, "缺少 timestamp 字段"); + SaSignException.notEmpty(nonceValue, "缺少 nonce 字段"); + SaSignException.notEmpty(signValue, "缺少 sign 字段"); + SaSignException.notEmpty(appKeyValue, "缺少 appkey 字段"); + + // 应用存在性校验 + SaSignException.notTrue(!appService.isExistAppKey(appKeyValue), "应用不存在"); + + // 应用是否过期校验 + SaSignException.notTrue(appService.isExpireAppKey(appKeyValue), "应用已过期"); + + // 依次校验三个参数 + checkTimestamp(Long.parseLong(timestampValue)); + checkNonce(nonceValue); + checkSign(paramMap, signValue); + + // 通过 √ + } + + @Override + public String createSign(Map paramsMap) { + // 根据应用密钥获取对应的应用密码 + String appKey = (String)((Map)paramsMap).get("appkey"); + String secretKey = this.appService.getAppSecretByAppKey(appKey); + SaSignException.notEmpty(secretKey, "参与参数签名的秘钥不可为空", SaErrorCode.CODE_12201); + + // 如果调用者不小心传入了 sign 参数,则此处需要将 sign 参数排除在外 + if (paramsMap.containsKey(sign)) { + // 为了保证不影响原有的 paramsMap,此处需要再复制一份 + paramsMap = new TreeMap<>(paramsMap); + paramsMap.remove(sign); + } + + // 计算签名 + String paramsStr = joinParamsDictSort(paramsMap); + String fullStr = paramsStr + "&" + key + "=" + secretKey; + String signStr = abstractStr(fullStr); + + // 输入日志,方便调试 + log.debug("fullStr:{}", fullStr); + log.debug("signStr:{}", signStr); + + // 返回 + return signStr; + } +} diff --git a/continew-admin-open/src/main/java/top/continew/admin/open/util/ApiSignCheckUtils.java b/continew-admin-open/src/main/java/top/continew/admin/open/util/ApiSignCheckUtils.java new file mode 100644 index 00000000..0f43e2fa --- /dev/null +++ b/continew-admin-open/src/main/java/top/continew/admin/open/util/ApiSignCheckUtils.java @@ -0,0 +1,46 @@ +/* + * 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.open.util; + +import cn.dev33.satoken.context.SaHolder; +import cn.dev33.satoken.context.model.SaRequest; + +import java.util.List; + +/** + * API签名验证工具类 + * + * @author chengzi + * @since 2024/10/25 15:31 + */ +public class ApiSignCheckUtils { + + private ApiSignCheckUtils() { + } + + /** + * 判断请求是否包含sign参数 + * + * @return 是否包含sign参数(true:包含;false:不包含) + */ + public static boolean isExistSignParam() { + SaRequest saRequest = SaHolder.getRequest(); + List paramNames = saRequest.getParamNames(); + return paramNames.stream().anyMatch(paramName -> paramName.equals("sign")); + } + +} diff --git a/continew-admin-webapi/pom.xml b/continew-admin-webapi/pom.xml index a0473bd8..01b837db 100644 --- a/continew-admin-webapi/pom.xml +++ b/continew-admin-webapi/pom.xml @@ -50,6 +50,12 @@ continew-admin-generator + + + top.continew + continew-admin-open + + org.liquibase diff --git a/continew-admin-webapi/src/main/java/top/continew/admin/config/satoken/SaTokenConfiguration.java b/continew-admin-webapi/src/main/java/top/continew/admin/config/satoken/SaTokenConfiguration.java index 71ac153b..2695e109 100644 --- a/continew-admin-webapi/src/main/java/top/continew/admin/config/satoken/SaTokenConfiguration.java +++ b/continew-admin-webapi/src/main/java/top/continew/admin/config/satoken/SaTokenConfiguration.java @@ -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 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(), "密码已过期,请修改密码"); })); } } diff --git a/continew-admin-webapi/src/main/java/top/continew/admin/controller/open/AppController.java b/continew-admin-webapi/src/main/java/top/continew/admin/controller/open/AppController.java new file mode 100644 index 00000000..de858d97 --- /dev/null +++ b/continew-admin-webapi/src/main/java/top/continew/admin/controller/open/AppController.java @@ -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 { + + 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 add(AppReq req) { + BaseIdResp baseIdResp = super.add(req); + Long appId = baseIdResp.getId(); + appService.refreshAppSecretByID(appId); + appService.resetAppSecretStatusById(appId, APP_DISABLED_KEY); + return baseIdResp; + } +} \ No newline at end of file diff --git a/continew-admin-webapi/src/main/java/top/continew/starter/extension/crud/controller/BaseController.java b/continew-admin-webapi/src/main/java/top/continew/starter/extension/crud/controller/BaseController.java new file mode 100644 index 00000000..21f31513 --- /dev/null +++ b/continew-admin-webapi/src/main/java/top/continew/starter/extension/crud/controller/BaseController.java @@ -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 业务接口 + * @param 列表类型 + * @param 详情类型 + * @param 查询条件 + * @param 创建或修改类型 + * @author Charles7c + * @since 1.0.0 + */ +public abstract class BaseController, L, D, Q, C extends BaseReq> { + + @Autowired + protected S baseService; + + /** + * 分页查询列表 + * + * @param query 查询条件 + * @param pageQuery 分页查询条件 + * @return 分页信息 + */ + @Operation(summary = "分页查询列表", description = "分页查询列表") + @ResponseBody + @GetMapping + public PageResp 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(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 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 add(@Validated(ValidateGroup.Crud.Add.class) @RequestBody C req) { + this.checkPermission(Api.ADD); + return BaseIdResp.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 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())); + } + } +} diff --git a/continew-admin-webapi/src/main/resources/db/changelog/mysql/continew-admin_change_v3.4.0.sql b/continew-admin-webapi/src/main/resources/db/changelog/mysql/continew-admin_change_v3.4.0.sql index 96903ff4..4a991bc2 100644 --- a/continew-admin-webapi/src/main/resources/db/changelog/mysql/continew-admin_change_v3.4.0.sql +++ b/continew-admin-webapi/src/main/resources/db/changelog/mysql/continew-admin_change_v3.4.0.sql @@ -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 Admin(Continue 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); \ No newline at end of file diff --git a/pom.xml b/pom.xml index fdba62cc..68e7b2cd 100644 --- a/pom.xml +++ b/pom.xml @@ -29,6 +29,7 @@ continew-admin-system continew-admin-common continew-admin-extension + continew-admin-open @@ -73,6 +74,13 @@ continew-admin-generator ${revision} + + + + top.continew + continew-admin-open + ${revision} +