From da95ddf81045b2811c864d39962fa5c0e22857f7 Mon Sep 17 00:00:00 2001 From: chy Date: Mon, 9 Feb 2026 23:44:07 +0800 Subject: [PATCH] =?UTF-8?q?=E5=90=88=E5=B9=B6=E8=91=A3=E6=BD=98=E7=A5=A5?= =?UTF-8?q?=E5=86=B2=E7=AA=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/resources/application-local.yaml | 31 +--- .../service/system/entity/OAuth2ClientDO.java | 121 ++++++++++++++ .../service/impl/OAuth2ClientServiceImpl.java | 157 ++++++++++++++++++ 3 files changed, 287 insertions(+), 22 deletions(-) create mode 100644 lidee-service/lidee-service-system-biz/src/main/java/com/lideeyunji/service/system/entity/OAuth2ClientDO.java create mode 100644 lidee-service/lidee-service-system-biz/src/main/java/com/lideeyunji/service/system/service/impl/OAuth2ClientServiceImpl.java diff --git a/lidee-admin/src/main/resources/application-local.yaml b/lidee-admin/src/main/resources/application-local.yaml index 03adecd..0f0fe68 100644 --- a/lidee-admin/src/main/resources/application-local.yaml +++ b/lidee-admin/src/main/resources/application-local.yaml @@ -8,42 +8,29 @@ spring: primary: master datasource: master: -<<<<<<< HEAD - # MYSQL数据库 主库,业务库 - url: jdbc:mysql://127.0.0.1:3306/demo?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true # MySQL Connector/J 8.X 连接的示例 -======= # MYSQL数据库 主库,业务库 - url: jdbc:mysql://127.0.0.1:3306/gr_repoort001?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true # MySQL Connector/J 8.X 连接的示例 ->>>>>>> 20260204 + url: jdbc:mysql://127.0.0.1:3306/demo?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true # MySQL Connector/J 8.X 连接的示例 username: root - password: root + password: qihua lideeyunji: # 从库,框架库 -<<<<<<< HEAD - url: jdbc:mysql://127.0.0.1:3306/demo?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true # MySQL Connector/J 8.X 连接的示例 -======= - url: jdbc:mysql://127.0.0.1:3306/gr_repoort001?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true # MySQL Connector/J 8.X 连接的示例 ->>>>>>> 20260204 + url: jdbc:mysql://10.9.0.16:3307/gr_repoort?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&lowerCaseTableNames=1 # MySQL Connector/J 8.X 连接的示例 username: root - password: root + password: qihua slave: # 日志库单独 lazy: true # 开启懒加载,保证启动速度 -<<<<<<< HEAD - url: jdbc:mysql://127.0.0.1:3306/demo?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true # MySQL Connector/J 8.X 连接的示例 + url: jdbc:mysql://10.9.0.16:3307/gr_repoort?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&lowerCaseTableNames=1 # MySQL Connector/J 8.X 连接的示例 username: root - password: root + password: qihua bidb: # 日志库单独 lazy: true # 开启懒加载,保证启动速度 url: jdbc:mysql://127.0.0.1:3306/erp_bi_data?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true # MySQL Connector/J 8.X 连接的示例 -======= - url: jdbc:mysql://127.0.0.1:3306/gr_repoort001?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true # MySQL Connector/J 8.X 连接的示例 ->>>>>>> 20260204 username: root - password: root + password: qihua redis: - host: 127.0.0.1 # 地址 + host: 10.9.0.116 # 地址 port: 6379 # 端口 database: 2 # 数据库索引 - password: lidee@123 # 密码,建议生产环境开启 + password: 123456 # 密码,建议生产环境开启 --- #################### 地代码平台相关配置 #################### diff --git a/lidee-service/lidee-service-system-biz/src/main/java/com/lideeyunji/service/system/entity/OAuth2ClientDO.java b/lidee-service/lidee-service-system-biz/src/main/java/com/lideeyunji/service/system/entity/OAuth2ClientDO.java new file mode 100644 index 0000000..ce0d638 --- /dev/null +++ b/lidee-service/lidee-service-system-biz/src/main/java/com/lideeyunji/service/system/entity/OAuth2ClientDO.java @@ -0,0 +1,121 @@ +package com.lideeyunji.service.system.entity; + +import com.lideeyunji.tool.framework.common.enums.CommonStatusEnum; +import com.lideeyunji.tool.framework.mybatis.core.dataobject.BaseDO; +import com.lideeyunji.service.system.enums.OAuth2GrantTypeEnum; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.List; + +/** + * OAuth2 客户端 DO + * + * @author 金灯剑客 + */ +@TableName(value = "system_oauth2_client", autoResultMap = true) +@KeySequence("system_oauth2_client_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +public class OAuth2ClientDO extends BaseDO { + + /** + * 编号,数据库自增 + * + * 由于 SQL Server 在存储 String 主键有点问题,所以暂时使用 Long 类型 + */ + @TableId + private Long id; + /** + * 客户端编号 + */ + private String clientId; + /** + * 客户端密钥 + */ + private String secret; + /** + * 应用名 + */ + private String name; + /** + * 应用分类 + */ + private String category; + + /** + * 应用图标 + */ + private String logo; + /** + * 应用描述 + */ + private String description; + /** + * 状态 + * + * 枚举 {@link CommonStatusEnum} + */ + private Integer status; + /** + * 访问令牌的有效期 + */ + private Integer accessTokenValiditySeconds; + /** + * 刷新令牌的有效期 + */ + private Integer refreshTokenValiditySeconds; +// /** +// * 可重定向的 URI 地址 +// */ +// @TableField(typeHandler = JacksonTypeHandler.class) +// private List redirectUris; + + private String redirectUris; + + /** + * 授权类型(模式) + * + * 枚举 {@link OAuth2GrantTypeEnum} + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private List authorizedGrantTypes; + /** + * 授权范围 + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private List scopes; + /** + * 自动授权的 Scope + * + * code 授权时,如果 scope 在这个范围内,则自动通过 + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private List autoApproveScopes; + /** + * 权限 + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private List authorities; + /** + * 资源 + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private List resourceIds; + + /** + * 回调URI地址 + */ + private String callbackUris; + + /** + * 附加信息,JSON 格式 + */ + private String additionalInformation; + +} diff --git a/lidee-service/lidee-service-system-biz/src/main/java/com/lideeyunji/service/system/service/impl/OAuth2ClientServiceImpl.java b/lidee-service/lidee-service-system-biz/src/main/java/com/lideeyunji/service/system/service/impl/OAuth2ClientServiceImpl.java new file mode 100644 index 0000000..7135cfe --- /dev/null +++ b/lidee-service/lidee-service-system-biz/src/main/java/com/lideeyunji/service/system/service/impl/OAuth2ClientServiceImpl.java @@ -0,0 +1,157 @@ +package com.lideeyunji.service.system.service.impl; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.spring.SpringUtil; +import com.lideeyunji.tool.framework.common.enums.CommonStatusEnum; +import com.lideeyunji.tool.framework.common.pojo.PageResult; +import com.lideeyunji.tool.framework.common.util.object.BeanUtils; +import com.lideeyunji.tool.framework.common.util.string.StrUtils; +import com.lideeyunji.service.system.controller.vo.oauth2.client.OAuth2ClientPageReqVO; +import com.lideeyunji.service.system.controller.vo.oauth2.client.OAuth2ClientSaveReqVO; +import com.lideeyunji.service.system.entity.OAuth2ClientDO; +import com.lideeyunji.service.system.mapper.OAuth2ClientMapper; +import com.lideeyunji.service.system.config.redis.RedisKeyConstants; +import com.lideeyunji.service.system.service.IOAuth2ClientService; +import com.google.common.annotations.VisibleForTesting; +import lombok.extern.slf4j.Slf4j; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import java.util.Collection; + +import static com.lideeyunji.tool.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.lideeyunji.service.system.constant.ErrorCodeConstants.*; + +/** + * OAuth2.0 Client Service 实现类 + * + * @author 金灯剑客 + */ +@Service +@Validated +@Slf4j +public class OAuth2ClientServiceImpl implements IOAuth2ClientService { + + @Resource + private OAuth2ClientMapper oauth2ClientMapper; + + @Override + public Long createOAuth2Client(OAuth2ClientSaveReqVO createReqVO) { + validateClientIdExists(null, createReqVO.getClientId()); + // 插入 + OAuth2ClientDO client = BeanUtils.toBean(createReqVO, OAuth2ClientDO.class); + oauth2ClientMapper.insert(client); + return client.getId(); + } + + @Override + @CacheEvict(cacheNames = RedisKeyConstants.OAUTH_CLIENT, + allEntries = true) // allEntries 清空所有缓存,因为可能修改到 clientId 字段,不好清理 + public void updateOAuth2Client(OAuth2ClientSaveReqVO updateReqVO) { + // 校验存在 + validateOAuth2ClientExists(updateReqVO.getId()); + // 校验 Client 未被占用 + validateClientIdExists(updateReqVO.getId(), updateReqVO.getClientId()); + + // 更新 + OAuth2ClientDO updateObj = BeanUtils.toBean(updateReqVO, OAuth2ClientDO.class); + oauth2ClientMapper.updateById(updateObj); + } + + @Override + @CacheEvict(cacheNames = RedisKeyConstants.OAUTH_CLIENT, + allEntries = true) // allEntries 清空所有缓存,因为 id 不是直接的缓存 key,不好清理 + public void deleteOAuth2Client(Long id) { + // 校验存在 + validateOAuth2ClientExists(id); + // 删除 + oauth2ClientMapper.deleteById(id); + } + + private void validateOAuth2ClientExists(Long id) { + if (oauth2ClientMapper.selectById(id) == null) { + throw exception(OAUTH2_CLIENT_NOT_EXISTS); + } + } + + @VisibleForTesting + void validateClientIdExists(Long id, String clientId) { + OAuth2ClientDO client = oauth2ClientMapper.selectByClientId(clientId); + if (client == null) { + return; + } + // 如果 id 为空,说明不用比较是否为相同 id 的客户端 + if (id == null) { + throw exception(OAUTH2_CLIENT_EXISTS); + } + if (!client.getId().equals(id)) { + throw exception(OAUTH2_CLIENT_EXISTS); + } + } + + @Override + public OAuth2ClientDO getOAuth2Client(Long id) { + return oauth2ClientMapper.selectById(id); + } + + @Override + @Cacheable(cacheNames = RedisKeyConstants.OAUTH_CLIENT, key = "#clientId", + unless = "#result == null") + public OAuth2ClientDO getOAuth2ClientFromCache(String clientId) { + return oauth2ClientMapper.selectByClientId(clientId); + } + + @Override + public PageResult getOAuth2ClientPage(OAuth2ClientPageReqVO pageReqVO) { + return oauth2ClientMapper.selectPage(pageReqVO); + } + + @Override + public OAuth2ClientDO validOAuthClientFromCache(String clientId, String clientSecret, String authorizedGrantType, + Collection scopes, String redirectUri) { + // 校验客户端存在、且开启 + OAuth2ClientDO client = getSelf().getOAuth2ClientFromCache(clientId); + if (client == null) { + throw exception(OAUTH2_CLIENT_NOT_EXISTS); + } + if (CommonStatusEnum.isDisable(client.getStatus())) { + throw exception(OAUTH2_CLIENT_DISABLE); + } + + // 校验客户端密钥 + if (StrUtil.isNotEmpty(clientSecret) && ObjectUtil.notEqual(client.getSecret(), clientSecret)) { + throw exception(OAUTH2_CLIENT_CLIENT_SECRET_ERROR); + } + // 校验授权方式 + if (StrUtil.isNotEmpty(authorizedGrantType) && !CollUtil.contains(client.getAuthorizedGrantTypes(), authorizedGrantType)) { + throw exception(OAUTH2_CLIENT_AUTHORIZED_GRANT_TYPE_NOT_EXISTS); + } + // 校验授权范围 + if (CollUtil.isNotEmpty(scopes) && !CollUtil.containsAll(client.getScopes(), scopes)) { + throw exception(OAUTH2_CLIENT_SCOPE_OVER); + } + // 校验回调地址 +// if (StrUtil.isNotEmpty(redirectUri) && !StrUtils.startWithAny(redirectUri, client.getRedirectUris())) { +// throw exception(OAUTH2_CLIENT_REDIRECT_URI_NOT_MATCH, redirectUri); +// } + if (StrUtil.isNotEmpty(redirectUri) && ObjectUtil.notEqual(client.getRedirectUris(), redirectUri)) { + throw exception(OAUTH2_CLIENT_REDIRECT_URI_NOT_MATCH, redirectUri); + } + return client; + } + + /** + * 获得自身的代理对象,解决 AOP 生效问题 + * + * @return 自己 + */ + private OAuth2ClientServiceImpl getSelf() { + return SpringUtil.getBean(getClass()); + } + +}