Commit 97db4586a82d2ce42e2a4ff0ecd47202ccb70212

Authored by YunaiV
1 parent 1f36af8e

完成 OAuth2 的客户端模块

Showing 33 changed files with 498 additions and 91 deletions
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/json/JsonUtils.java
... ... @@ -113,8 +113,7 @@ public class JsonUtils {
113 113 }
114 114 }
115 115  
116   - // TODO @Li:和上面的风格保持一致哈。parseTree
117   - public static JsonNode readTree(String text) {
  116 + public static JsonNode parseTree(String text) {
118 117 try {
119 118 return objectMapper.readTree(text);
120 119 } catch (IOException e) {
... ... @@ -123,7 +122,7 @@ public class JsonUtils {
123 122 }
124 123 }
125 124  
126   - public static JsonNode readTree(byte[] text) {
  125 + public static JsonNode parseTree(byte[] text) {
127 126 try {
128 127 return objectMapper.readTree(text);
129 128 } catch (IOException e) {
... ... @@ -132,4 +131,8 @@ public class JsonUtils {
132 131 }
133 132 }
134 133  
  134 + public static boolean isJson(String text) {
  135 + return JSONUtil.isJson(text);
  136 + }
  137 +
135 138 }
... ...
yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/auth/MemberAuthServiceImpl.java
... ... @@ -18,7 +18,7 @@ import cn.iocoder.yudao.module.system.api.logger.LoginLogApi;
18 18 import cn.iocoder.yudao.module.system.api.logger.dto.LoginLogCreateReqDTO;
19 19 import cn.iocoder.yudao.module.system.api.sms.SmsCodeApi;
20 20 import cn.iocoder.yudao.module.system.api.social.SocialUserApi;
21   -import cn.iocoder.yudao.module.system.enums.auth.OAuth2ClientIdEnum;
  21 +import cn.iocoder.yudao.module.system.enums.auth.OAuth2ClientConstants;
22 22 import cn.iocoder.yudao.module.system.enums.logger.LoginLogTypeEnum;
23 23 import cn.iocoder.yudao.module.system.enums.logger.LoginResultEnum;
24 24 import cn.iocoder.yudao.module.system.enums.sms.SmsSceneEnum;
... ... @@ -120,7 +120,7 @@ public class MemberAuthServiceImpl implements MemberAuthService {
120 120 createLoginLog(user.getId(), mobile, logType, LoginResultEnum.SUCCESS);
121 121 // 创建 Token 令牌
122 122 OAuth2AccessTokenRespDTO accessTokenRespDTO = oauth2TokenApi.createAccessToken(new OAuth2AccessTokenCreateReqDTO()
123   - .setUserId(user.getId()).setUserType(getUserType().getValue()).setClientId(OAuth2ClientIdEnum.DEFAULT.getId()));
  123 + .setUserId(user.getId()).setUserType(getUserType().getValue()).setClientId(OAuth2ClientConstants.CLIENT_ID_DEFAULT));
124 124 // 构建返回结果
125 125 return AuthConvert.INSTANCE.convert(accessTokenRespDTO);
126 126 }
... ... @@ -212,7 +212,7 @@ public class MemberAuthServiceImpl implements MemberAuthService {
212 212  
213 213 @Override
214 214 public AppAuthLoginRespVO refreshToken(String refreshToken) {
215   - OAuth2AccessTokenRespDTO accessTokenDO = oauth2TokenApi.refreshAccessToken(refreshToken, OAuth2ClientIdEnum.DEFAULT.getId());
  215 + OAuth2AccessTokenRespDTO accessTokenDO = oauth2TokenApi.refreshAccessToken(refreshToken, OAuth2ClientConstants.CLIENT_ID_DEFAULT);
216 216 return AuthConvert.INSTANCE.convert(accessTokenDO);
217 217 }
218 218  
... ...
yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/auth/OAuth2TokenApi.java
... ... @@ -44,6 +44,6 @@ public interface OAuth2TokenApi {
44 44 * @param clientId 客户端编号
45 45 * @return 访问令牌的信息
46 46 */
47   - OAuth2AccessTokenRespDTO refreshAccessToken(String refreshToken, Long clientId);
  47 + OAuth2AccessTokenRespDTO refreshAccessToken(String refreshToken, String clientId);
48 48  
49 49 }
... ...
yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/auth/dto/OAuth2AccessTokenCreateReqDTO.java
... ... @@ -30,6 +30,6 @@ public class OAuth2AccessTokenCreateReqDTO implements Serializable {
30 30 * 客户端编号
31 31 */
32 32 @NotNull(message = "客户端编号不能为空")
33   - private Long clientId;
  33 + private String clientId;
34 34  
35 35 }
... ...
yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/ErrorCodeConstants.java
... ... @@ -125,5 +125,6 @@ public interface ErrorCodeConstants {
125 125  
126 126 // ========== 系统敏感词 1002020000 =========
127 127 ErrorCode OAUTH2_CLIENT_NOT_EXISTS = new ErrorCode(1002020000, "OAuth2 客户端不存在");
  128 + ErrorCode OAUTH2_CLIENT_EXISTS = new ErrorCode(1002020001, "OAuth2 客户端编号已存在");
128 129  
129 130 }
... ...
yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/auth/OAuth2ClientConstants.java 0 → 100644
  1 +package cn.iocoder.yudao.module.system.enums.auth;
  2 +
  3 +/**
  4 + * OAuth2.0 客户端的通用枚举
  5 + *
  6 + * @author 芋道源码
  7 + */
  8 +public interface OAuth2ClientConstants {
  9 +
  10 + String CLIENT_ID_DEFAULT = "default";
  11 +
  12 +}
... ...
yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/auth/OAuth2ClientIdEnum.java renamed to yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/auth/OAuth2GrantTypeEnum.java
... ... @@ -4,14 +4,21 @@ import lombok.AllArgsConstructor;
4 4 import lombok.Getter;
5 5  
6 6 /**
7   - * OAuth2.0 客户端的编号枚举
  7 + * OAuth2 授权类型(模式)的枚举
  8 + *
  9 + * @author 芋道源码
8 10 */
9 11 @AllArgsConstructor
10 12 @Getter
11   -public enum OAuth2ClientIdEnum {
  13 +public enum OAuth2GrantTypeEnum {
12 14  
13   - DEFAULT(1L); // 系统默认
  15 + PASSWORD("password"), // 密码模式
  16 + AUTHORIZATION_CODE("authorization_code"), // 授权码模式
  17 + IMPLICIT("implicit"), // 简化模式
  18 + CLIENT_CREDENTIALS("client_credentials"), // 客户端模式
  19 + REFRESH_TOKEN("refresh_token"), // 刷新模式
  20 + ;
14 21  
15   - private final Long id;
  22 + private final String grantType;
16 23  
17 24 }
... ...
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/api/auth/OAuth2TokenApiImpl.java
... ... @@ -40,7 +40,7 @@ public class OAuth2TokenApiImpl implements OAuth2TokenApi {
40 40 }
41 41  
42 42 @Override
43   - public OAuth2AccessTokenRespDTO refreshAccessToken(String refreshToken, Long clientId) {
  43 + public OAuth2AccessTokenRespDTO refreshAccessToken(String refreshToken, String clientId) {
44 44 OAuth2AccessTokenDO accessTokenDO = oauth2TokenService.refreshAccessToken(refreshToken, clientId);
45 45 return OAuth2TokenConvert.INSTANCE.convert2(accessTokenDO);
46 46 }
... ...
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/OAuth2ClientController.http 0 → 100644
  1 +### 请求 /login 接口 => 成功
  2 +POST {{baseUrl}}/system/oauth2-client/create
  3 +Content-Type: application/json
  4 +Authorization: Bearer {{token}}
  5 +tenant-id: {{adminTenentId}}
  6 +
  7 +{
  8 + "id": "1",
  9 + "secret": "admin123",
  10 + "name": "芋道源码",
  11 + "logo": "https://www.iocoder.cn/images/favicon.ico",
  12 + "description": "我是描述",
  13 + "status": 0,
  14 + "accessTokenValiditySeconds": 180,
  15 + "refreshTokenValiditySeconds": 8640,
  16 + "redirectUris": ["https://www.iocoder.cn"],
  17 + "autoApprove": true,
  18 + "authorizedGrantTypes": ["password"],
  19 + "scopes": ["user_info"],
  20 + "authorities": ["system:user:query"],
  21 + "resource_ids": ["1024"],
  22 + "additionalInformation": "{}"
  23 +}
... ...
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/vo/client/OAuth2ClientBaseVO.java
1 1 package cn.iocoder.yudao.module.system.controller.admin.auth.vo.client;
2 2  
  3 +import cn.hutool.core.util.StrUtil;
  4 +import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
3 5 import io.swagger.annotations.ApiModelProperty;
4 6 import lombok.Data;
  7 +import org.hibernate.validator.constraints.URL;
5 8  
  9 +import javax.validation.constraints.AssertTrue;
  10 +import javax.validation.constraints.NotEmpty;
6 11 import javax.validation.constraints.NotNull;
7 12 import java.util.List;
8 13  
... ... @@ -15,7 +20,7 @@ public class OAuth2ClientBaseVO {
15 20  
16 21 @ApiModelProperty(value = "客户端编号", required = true)
17 22 @NotNull(message = "客户端编号不能为空")
18   - private Long id;
  23 + private String clientId;
19 24  
20 25 @ApiModelProperty(value = "客户端密钥", required = true)
21 26 @NotNull(message = "客户端密钥不能为空")
... ... @@ -27,6 +32,7 @@ public class OAuth2ClientBaseVO {
27 32  
28 33 @ApiModelProperty(value = "应用图标", required = true)
29 34 @NotNull(message = "应用图标不能为空")
  35 + @URL(message = "应用图标的地址不正确")
30 36 private String logo;
31 37  
32 38 @ApiModelProperty(value = "应用描述")
... ... @@ -46,6 +52,32 @@ public class OAuth2ClientBaseVO {
46 52  
47 53 @ApiModelProperty(value = "可重定向的 URI 地址", required = true)
48 54 @NotNull(message = "可重定向的 URI 地址不能为空")
49   - private List<String> redirectUris;
  55 + private List<@NotEmpty(message = "重定向的 URI 不能为空")
  56 + @URL(message = "重定向的 URI 格式不正确") String> redirectUris;
  57 +
  58 + @ApiModelProperty(value = "是否自动授权", required = true, example = "true")
  59 + @NotNull(message = "是否自动授权不能为空")
  60 + private Boolean autoApprove;
  61 +
  62 + @ApiModelProperty(value = "授权类型", required = true, example = "password", notes = "参见 OAuth2GrantTypeEnum 枚举")
  63 + @NotNull(message = "授权类型不能为空")
  64 + private List<String> authorizedGrantTypes;
  65 +
  66 + @ApiModelProperty(value = "授权范围", example = "user_info")
  67 + private List<String> scopes;
  68 +
  69 + @ApiModelProperty(value = "权限", example = "system:user:query")
  70 + private List<String> authorities;
  71 +
  72 + @ApiModelProperty(value = "资源", example = "1024")
  73 + private List<String> resourceIds;
  74 +
  75 + @ApiModelProperty(value = "附加信息", example = "{yunai: true}")
  76 + private String additionalInformation;
  77 +
  78 + @AssertTrue(message = "附加信息必须是 JSON 格式")
  79 + public boolean isAdditionalInformationJson() {
  80 + return StrUtil.isEmpty(additionalInformation) || JsonUtils.isJson(additionalInformation);
  81 + }
50 82  
51 83 }
... ...
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/vo/client/OAuth2ClientRespVO.java
... ... @@ -14,6 +14,9 @@ import java.util.Date;
14 14 @ToString(callSuper = true)
15 15 public class OAuth2ClientRespVO extends OAuth2ClientBaseVO {
16 16  
  17 + @ApiModelProperty(value = "编号", required = true)
  18 + private Long id;
  19 +
17 20 @ApiModelProperty(value = "创建时间", required = true)
18 21 private Date createTime;
19 22  
... ...
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/vo/client/OAuth2ClientUpdateReqVO.java
1 1 package cn.iocoder.yudao.module.system.controller.admin.auth.vo.client;
2 2  
3 3 import io.swagger.annotations.ApiModel;
  4 +import io.swagger.annotations.ApiModelProperty;
4 5 import lombok.Data;
5 6 import lombok.EqualsAndHashCode;
6 7 import lombok.ToString;
7 8  
  9 +import javax.validation.constraints.NotNull;
  10 +
8 11 @ApiModel("管理后台 - OAuth2 客户端更新 Request VO")
9 12 @Data
10 13 @EqualsAndHashCode(callSuper = true)
11 14 @ToString(callSuper = true)
12 15 public class OAuth2ClientUpdateReqVO extends OAuth2ClientBaseVO {
13 16  
  17 + @ApiModelProperty(value = "编号", required = true)
  18 + @NotNull(message = "编号不能为空")
  19 + private Long id;
  20 +
14 21 }
... ...
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/vo/token/OAuth2AccessTokenPageReqVO.java
... ... @@ -18,6 +18,6 @@ public class OAuth2AccessTokenPageReqVO extends PageParam {
18 18 private Integer userType;
19 19  
20 20 @ApiModelProperty(value = "客户端编号", required = true, example = "2")
21   - private Long clientId;
  21 + private String clientId;
22 22  
23 23 }
... ...
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/vo/token/OAuth2AccessTokenRespVO.java
... ... @@ -30,7 +30,7 @@ public class OAuth2AccessTokenRespVO {
30 30 private Integer userType;
31 31  
32 32 @ApiModelProperty(value = "客户端编号", required = true, example = "2")
33   - private Long clientId;
  33 + private String clientId;
34 34  
35 35 @ApiModelProperty(value = "创建时间", required = true)
36 36 private Date createTime;
... ...
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/auth/OAuth2AccessTokenDO.java
... ... @@ -52,7 +52,7 @@ public class OAuth2AccessTokenDO extends TenantBaseDO {
52 52 *
53 53 * 关联 {@link OAuth2ClientDO#getId()}
54 54 */
55   - private Long clientId;
  55 + private String clientId;
56 56 /**
57 57 * 过期时间
58 58 */
... ...
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/auth/OAuth2ClientDO.java
... ... @@ -2,39 +2,38 @@ package cn.iocoder.yudao.module.system.dal.dataobject.auth;
2 2  
3 3 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
4 4 import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
5   -import com.baomidou.mybatisplus.annotation.IdType;
  5 +import cn.iocoder.yudao.module.system.enums.auth.OAuth2GrantTypeEnum;
6 6 import com.baomidou.mybatisplus.annotation.TableField;
7 7 import com.baomidou.mybatisplus.annotation.TableId;
8 8 import com.baomidou.mybatisplus.annotation.TableName;
9 9 import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
10 10 import lombok.Data;
11 11 import lombok.EqualsAndHashCode;
12   -import lombok.experimental.Accessors;
13 12  
14 13 import java.util.List;
15 14  
16 15 /**
17 16 * OAuth2 客户端 DO
18 17 *
19   - * 如下字段,考虑到使用相对不是很高频,主要是一些开关,暂时不支持:
20   - * authorized_grant_types、authorities、additional_information、autoapprove、resource_ids、scope
21   - *
22 18 * @author 芋道源码
23 19 */
24 20 @TableName(value = "system_oauth2_client", autoResultMap = true)
25 21 @Data
26 22 @EqualsAndHashCode(callSuper = true)
27   -@Accessors(chain = true)
28 23 public class OAuth2ClientDO extends BaseDO {
29 24  
30 25 /**
31   - * 客户端编号
  26 + * 编号,数据库自增
32 27 *
33 28 * 由于 SQL Server 在存储 String 主键有点问题,所以暂时使用 Long 类型
34 29 */
35   - @TableId(type = IdType.INPUT)
  30 + @TableId
36 31 private Long id;
37 32 /**
  33 + * 客户端编号
  34 + */
  35 + private String clientId;
  36 + /**
38 37 * 客户端密钥
39 38 */
40 39 private String secret;
... ... @@ -69,5 +68,35 @@ public class OAuth2ClientDO extends BaseDO {
69 68 */
70 69 @TableField(typeHandler = JacksonTypeHandler.class)
71 70 private List<String> redirectUris;
  71 + /**
  72 + * 是否自动授权
  73 + */
  74 + private Boolean autoApprove;
  75 + /**
  76 + * 授权类型(模式)
  77 + *
  78 + * 枚举 {@link OAuth2GrantTypeEnum}
  79 + */
  80 + @TableField(typeHandler = JacksonTypeHandler.class)
  81 + private List<String> authorizedGrantTypes;
  82 + /**
  83 + * 授权范围
  84 + */
  85 + @TableField(typeHandler = JacksonTypeHandler.class)
  86 + private List<String> scopes;
  87 + /**
  88 + * 权限
  89 + */
  90 + @TableField(typeHandler = JacksonTypeHandler.class)
  91 + private List<String> authorities;
  92 + /**
  93 + * 资源
  94 + */
  95 + @TableField(typeHandler = JacksonTypeHandler.class)
  96 + private List<String> resourceIds;
  97 + /**
  98 + * 附加信息,JSON 格式
  99 + */
  100 + private String additionalInformation;
72 101  
73 102 }
... ...
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/auth/OAuth2CodeDO.java
... ... @@ -5,7 +5,6 @@ import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
5 5 import com.baomidou.mybatisplus.annotation.TableName;
6 6 import lombok.Data;
7 7 import lombok.EqualsAndHashCode;
8   -import lombok.experimental.Accessors;
9 8  
10 9 import java.util.Date;
11 10  
... ... @@ -17,7 +16,6 @@ import java.util.Date;
17 16 @TableName("system_oauth2_code")
18 17 @Data
19 18 @EqualsAndHashCode(callSuper = true)
20   -@Accessors(chain = true)
21 19 public class OAuth2CodeDO extends BaseDO {
22 20  
23 21 /**
... ... @@ -43,7 +41,7 @@ public class OAuth2CodeDO extends BaseDO {
43 41 *
44 42 * 关联 {@link OAuth2ClientDO#getId()}
45 43 */
46   - private Long clientId;
  44 + private String clientId;
47 45 /**
48 46 * 刷新令牌
49 47 *
... ...
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/auth/OAuth2RefreshTokenDO.java
... ... @@ -43,7 +43,7 @@ public class OAuth2RefreshTokenDO extends BaseDO {
43 43 *
44 44 * 关联 {@link OAuth2ClientDO#getId()}
45 45 */
46   - private Long clientId;
  46 + private String clientId;
47 47 /**
48 48 * 过期时间
49 49 */
... ...
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/auth/OAuth2ClientMapper.java
... ... @@ -6,6 +6,9 @@ import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
6 6 import cn.iocoder.yudao.module.system.controller.admin.auth.vo.client.OAuth2ClientPageReqVO;
7 7 import cn.iocoder.yudao.module.system.dal.dataobject.auth.OAuth2ClientDO;
8 8 import org.apache.ibatis.annotations.Mapper;
  9 +import org.apache.ibatis.annotations.Select;
  10 +
  11 +import java.util.Date;
9 12  
10 13 /**
11 14 * OAuth2 客户端 Mapper
... ... @@ -22,4 +25,11 @@ public interface OAuth2ClientMapper extends BaseMapperX&lt;OAuth2ClientDO&gt; {
22 25 .orderByDesc(OAuth2ClientDO::getId));
23 26 }
24 27  
  28 + default OAuth2ClientDO selectByClientId(String clientId) {
  29 + return selectOne(OAuth2ClientDO::getClientId, clientId);
  30 + }
  31 +
  32 + @Select("SELECT COUNT(*) FROM system_oauth2_client WHERE update_time > #{maxUpdateTime}")
  33 + int selectCountByUpdateTimeGt(Date maxUpdateTime);
  34 +
25 35 }
... ...
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/consumer/auth/OAuth2ClientRefreshConsumer.java 0 → 100644
  1 +package cn.iocoder.yudao.module.system.mq.consumer.auth;
  2 +
  3 +import cn.iocoder.yudao.framework.mq.core.pubsub.AbstractChannelMessageListener;
  4 +import cn.iocoder.yudao.module.system.mq.message.auth.OAuth2ClientRefreshMessage;
  5 +import cn.iocoder.yudao.module.system.service.auth.OAuth2ClientService;
  6 +import lombok.extern.slf4j.Slf4j;
  7 +import org.springframework.stereotype.Component;
  8 +
  9 +import javax.annotation.Resource;
  10 +
  11 +/**
  12 + * 针对 {@link OAuth2ClientRefreshMessage} 的消费者
  13 + *
  14 + * @author 芋道源码
  15 + */
  16 +@Component
  17 +@Slf4j
  18 +public class OAuth2ClientRefreshConsumer extends AbstractChannelMessageListener<OAuth2ClientRefreshMessage> {
  19 +
  20 + @Resource
  21 + private OAuth2ClientService oauth2ClientService;
  22 +
  23 + @Override
  24 + public void onMessage(OAuth2ClientRefreshMessage message) {
  25 + log.info("[onMessage][收到 OAuth2Client 刷新消息]");
  26 + oauth2ClientService.initLocalCache();
  27 + }
  28 +
  29 +}
... ...
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/message/auth/OAuth2ClientRefreshMessage.java 0 → 100644
  1 +package cn.iocoder.yudao.module.system.mq.message.auth;
  2 +
  3 +import cn.iocoder.yudao.framework.mq.core.pubsub.AbstractChannelMessage;
  4 +import lombok.Data;
  5 +import lombok.EqualsAndHashCode;
  6 +
  7 +/**
  8 + * OAuth 2.0 客户端的数据刷新 Message
  9 + *
  10 + * @author 芋道源码
  11 + */
  12 +@Data
  13 +@EqualsAndHashCode(callSuper = true)
  14 +public class OAuth2ClientRefreshMessage extends AbstractChannelMessage {
  15 +
  16 + @Override
  17 + public String getChannel() {
  18 + return "system.oauth2-client.refresh";
  19 + }
  20 +
  21 +}
... ...
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/producer/auth/OAuth2ClientProducer.java 0 → 100644
  1 +package cn.iocoder.yudao.module.system.mq.producer.auth;
  2 +
  3 +import cn.iocoder.yudao.framework.mq.core.RedisMQTemplate;
  4 +import cn.iocoder.yudao.module.system.mq.message.auth.OAuth2ClientRefreshMessage;
  5 +import org.springframework.stereotype.Component;
  6 +
  7 +import javax.annotation.Resource;
  8 +
  9 +/**
  10 + * OAuth 2.0 客户端相关消息的 Producer
  11 + */
  12 +@Component
  13 +public class OAuth2ClientProducer {
  14 +
  15 + @Resource
  16 + private RedisMQTemplate redisMQTemplate;
  17 +
  18 + /**
  19 + * 发送 {@link OAuth2ClientRefreshMessage} 消息
  20 + */
  21 + public void sendOAuth2ClientRefreshMessage() {
  22 + OAuth2ClientRefreshMessage message = new OAuth2ClientRefreshMessage();
  23 + redisMQTemplate.send(message);
  24 + }
  25 +
  26 +}
... ...
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/auth/AdminAuthServiceImpl.java
... ... @@ -12,7 +12,7 @@ import cn.iocoder.yudao.module.system.controller.admin.auth.vo.auth.*;
12 12 import cn.iocoder.yudao.module.system.convert.auth.AuthConvert;
13 13 import cn.iocoder.yudao.module.system.dal.dataobject.auth.OAuth2AccessTokenDO;
14 14 import cn.iocoder.yudao.module.system.dal.dataobject.user.AdminUserDO;
15   -import cn.iocoder.yudao.module.system.enums.auth.OAuth2ClientIdEnum;
  15 +import cn.iocoder.yudao.module.system.enums.auth.OAuth2ClientConstants;
16 16 import cn.iocoder.yudao.module.system.enums.logger.LoginLogTypeEnum;
17 17 import cn.iocoder.yudao.module.system.enums.logger.LoginResultEnum;
18 18 import cn.iocoder.yudao.module.system.enums.sms.SmsSceneEnum;
... ... @@ -197,7 +197,7 @@ public class AdminAuthServiceImpl implements AdminAuthService {
197 197  
198 198 @Override
199 199 public AuthLoginRespVO refreshToken(String refreshToken) {
200   - OAuth2AccessTokenDO accessTokenDO = oauth2TokenService.refreshAccessToken(refreshToken, OAuth2ClientIdEnum.DEFAULT.getId());
  200 + OAuth2AccessTokenDO accessTokenDO = oauth2TokenService.refreshAccessToken(refreshToken, OAuth2ClientConstants.CLIENT_ID_DEFAULT);
201 201 return AuthConvert.INSTANCE.convert(accessTokenDO);
202 202 }
203 203  
... ... @@ -206,7 +206,7 @@ public class AdminAuthServiceImpl implements AdminAuthService {
206 206 createLoginLog(userId, username, logType, LoginResultEnum.SUCCESS);
207 207 // 创建访问令牌
208 208 OAuth2AccessTokenDO accessTokenDO = oauth2TokenService.createAccessToken(userId, getUserType().getValue(),
209   - OAuth2ClientIdEnum.DEFAULT.getId());
  209 + OAuth2ClientConstants.CLIENT_ID_DEFAULT);
210 210 // 构建返回结果
211 211 return AuthConvert.INSTANCE.convert(accessTokenDO);
212 212 }
... ...
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/auth/OAuth2ClientService.java
... ... @@ -18,7 +18,12 @@ import javax.validation.Valid;
18 18 public interface OAuth2ClientService {
19 19  
20 20 /**
21   - * 创建OAuth2 客户端
  21 + * 初始化 OAuth2Client 的本地缓存
  22 + */
  23 + void initLocalCache();
  24 +
  25 + /**
  26 + * 创建 OAuth2 客户端
22 27 *
23 28 * @param createReqVO 创建信息
24 29 * @return 编号
... ... @@ -26,21 +31,21 @@ public interface OAuth2ClientService {
26 31 Long createOAuth2Client(@Valid OAuth2ClientCreateReqVO createReqVO);
27 32  
28 33 /**
29   - * 更新OAuth2 客户端
  34 + * 更新 OAuth2 客户端
30 35 *
31 36 * @param updateReqVO 更新信息
32 37 */
33 38 void updateOAuth2Client(@Valid OAuth2ClientUpdateReqVO updateReqVO);
34 39  
35 40 /**
36   - * 删除OAuth2 客户端
  41 + * 删除 OAuth2 客户端
37 42 *
38 43 * @param id 编号
39 44 */
40 45 void deleteOAuth2Client(Long id);
41 46  
42 47 /**
43   - * 获得OAuth2 客户端
  48 + * 获得 OAuth2 客户端
44 49 *
45 50 * @param id 编号
46 51 * @return OAuth2 客户端
... ... @@ -48,7 +53,7 @@ public interface OAuth2ClientService {
48 53 OAuth2ClientDO getOAuth2Client(Long id);
49 54  
50 55 /**
51   - * 获得OAuth2 客户端分页
  56 + * 获得 OAuth2 客户端分页
52 57 *
53 58 * @param pageReqVO 分页查询
54 59 * @return OAuth2 客户端分页
... ... @@ -58,9 +63,9 @@ public interface OAuth2ClientService {
58 63 /**
59 64 * 从缓存中,校验客户端是否合法
60 65 *
61   - * @param id 客户端编号
  66 + * @param clientId 客户端编号
62 67 * @return 客户端
63 68 */
64   - OAuth2ClientDO validOAuthClientFromCache(Long id);
  69 + OAuth2ClientDO validOAuthClientFromCache(String clientId);
65 70  
66 71 }
... ...
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/auth/OAuth2ClientServiceImpl.java
1 1 package cn.iocoder.yudao.module.system.service.auth;
2 2  
  3 +import cn.hutool.core.collection.CollUtil;
3 4 import cn.iocoder.yudao.framework.common.pojo.PageResult;
4 5 import cn.iocoder.yudao.module.system.controller.admin.auth.vo.client.OAuth2ClientCreateReqVO;
5 6 import cn.iocoder.yudao.module.system.controller.admin.auth.vo.client.OAuth2ClientPageReqVO;
... ... @@ -7,11 +8,24 @@ import cn.iocoder.yudao.module.system.controller.admin.auth.vo.client.OAuth2Clie
7 8 import cn.iocoder.yudao.module.system.convert.auth.OAuth2ClientConvert;
8 9 import cn.iocoder.yudao.module.system.dal.dataobject.auth.OAuth2ClientDO;
9 10 import cn.iocoder.yudao.module.system.dal.mysql.auth.OAuth2ClientMapper;
  11 +import cn.iocoder.yudao.module.system.mq.producer.auth.OAuth2ClientProducer;
  12 +import com.google.common.annotations.VisibleForTesting;
  13 +import lombok.Getter;
  14 +import lombok.extern.slf4j.Slf4j;
  15 +import org.springframework.scheduling.annotation.Scheduled;
10 16 import org.springframework.stereotype.Service;
  17 +import org.springframework.validation.annotation.Validated;
11 18  
  19 +import javax.annotation.PostConstruct;
12 20 import javax.annotation.Resource;
  21 +import java.util.Date;
  22 +import java.util.List;
  23 +import java.util.Map;
13 24  
14 25 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
  26 +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap;
  27 +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.getMaxValue;
  28 +import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.OAUTH2_CLIENT_EXISTS;
15 29 import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.OAUTH2_CLIENT_NOT_EXISTS;
16 30  
17 31 /**
... ... @@ -20,35 +34,113 @@ import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.OAUTH2_CLI
20 34 * @author 芋道源码
21 35 */
22 36 @Service
  37 +@Validated
  38 +@Slf4j
23 39 public class OAuth2ClientServiceImpl implements OAuth2ClientService {
24 40  
  41 + /**
  42 + * 定时执行 {@link #schedulePeriodicRefresh()} 的周期
  43 + * 因为已经通过 Redis Pub/Sub 机制,所以频率不需要高
  44 + */
  45 + private static final long SCHEDULER_PERIOD = 5 * 60 * 1000L;
  46 +
  47 + /**
  48 + * 客户端缓存
  49 + * key:客户端编号 {@link OAuth2ClientDO#getClientId()} ()}
  50 + *
  51 + * 这里声明 volatile 修饰的原因是,每次刷新时,直接修改指向
  52 + */
  53 + @Getter
  54 + private volatile Map<String, OAuth2ClientDO> clientCache;
  55 + /**
  56 + * 缓存角色的最大更新时间,用于后续的增量轮询,判断是否有更新
  57 + */
  58 + @Getter
  59 + private volatile Date maxUpdateTime;
  60 +
25 61 @Resource
26 62 private OAuth2ClientMapper oauth2ClientMapper;
27 63  
  64 + @Resource
  65 + private OAuth2ClientProducer oauth2ClientProducer;
  66 +
  67 + /**
  68 + * 初始化 {@link #clientCache} 缓存
  69 + */
  70 + @Override
  71 + @PostConstruct
  72 + public void initLocalCache() {
  73 + // 获取客户端列表,如果有更新
  74 + List<OAuth2ClientDO> tenantList = loadOAuth2ClientIfUpdate(maxUpdateTime);
  75 + if (CollUtil.isEmpty(tenantList)) {
  76 + return;
  77 + }
  78 +
  79 + // 写入缓存
  80 + clientCache = convertMap(tenantList, OAuth2ClientDO::getClientId);
  81 + maxUpdateTime = getMaxValue(tenantList, OAuth2ClientDO::getUpdateTime);
  82 + log.info("[initLocalCache][初始化 OAuth2Client 数量为 {}]", tenantList.size());
  83 + }
  84 +
  85 + @Scheduled(fixedDelay = SCHEDULER_PERIOD, initialDelay = SCHEDULER_PERIOD)
  86 + public void schedulePeriodicRefresh() {
  87 + initLocalCache();
  88 + }
  89 +
  90 + /**
  91 + * 如果客户端发生变化,从数据库中获取最新的全量客户端。
  92 + * 如果未发生变化,则返回空
  93 + *
  94 + * @param maxUpdateTime 当前客户端的最大更新时间
  95 + * @return 客户端列表
  96 + */
  97 + private List<OAuth2ClientDO> loadOAuth2ClientIfUpdate(Date maxUpdateTime) {
  98 + // 第一步,判断是否要更新。
  99 + if (maxUpdateTime == null) { // 如果更新时间为空,说明 DB 一定有新数据
  100 + log.info("[loadOAuth2ClientIfUpdate][首次加载全量客户端]");
  101 + } else { // 判断数据库中是否有更新的客户端
  102 + if (oauth2ClientMapper.selectCountByUpdateTimeGt(maxUpdateTime) == 0) {
  103 + return null;
  104 + }
  105 + log.info("[loadOAuth2ClientIfUpdate][增量加载全量客户端]");
  106 + }
  107 + // 第二步,如果有更新,则从数据库加载所有客户端
  108 + return oauth2ClientMapper.selectList();
  109 + }
  110 +
28 111 @Override
29 112 public Long createOAuth2Client(OAuth2ClientCreateReqVO createReqVO) {
  113 + validateClientIdExists(null, createReqVO.getClientId());
30 114 // 插入
31   - OAuth2ClientDO oAuth2Client = OAuth2ClientConvert.INSTANCE.convert(createReqVO);
32   - oauth2ClientMapper.insert(oAuth2Client);
33   - // 返回
34   - return oAuth2Client.getId();
  115 + OAuth2ClientDO oauth2Client = OAuth2ClientConvert.INSTANCE.convert(createReqVO);
  116 + oauth2ClientMapper.insert(oauth2Client);
  117 + // 发送刷新消息
  118 + oauth2ClientProducer.sendOAuth2ClientRefreshMessage();
  119 + return oauth2Client.getId();
35 120 }
36 121  
37 122 @Override
38 123 public void updateOAuth2Client(OAuth2ClientUpdateReqVO updateReqVO) {
39 124 // 校验存在
40   - this.validateOAuth2ClientExists(updateReqVO.getId());
  125 + validateOAuth2ClientExists(updateReqVO.getId());
  126 + // 校验 Client 未被占用
  127 + validateClientIdExists(updateReqVO.getId(), updateReqVO.getClientId());
  128 +
41 129 // 更新
42 130 OAuth2ClientDO updateObj = OAuth2ClientConvert.INSTANCE.convert(updateReqVO);
43 131 oauth2ClientMapper.updateById(updateObj);
  132 + // 发送刷新消息
  133 + oauth2ClientProducer.sendOAuth2ClientRefreshMessage();
44 134 }
45 135  
46 136 @Override
47 137 public void deleteOAuth2Client(Long id) {
48 138 // 校验存在
49   - this.validateOAuth2ClientExists(id);
  139 + validateOAuth2ClientExists(id);
50 140 // 删除
51 141 oauth2ClientMapper.deleteById(id);
  142 + // 发送刷新消息
  143 + oauth2ClientProducer.sendOAuth2ClientRefreshMessage();
52 144 }
53 145  
54 146 private void validateOAuth2ClientExists(Long id) {
... ... @@ -57,6 +149,21 @@ public class OAuth2ClientServiceImpl implements OAuth2ClientService {
57 149 }
58 150 }
59 151  
  152 + @VisibleForTesting
  153 + public void validateClientIdExists(Long id, String clientId) {
  154 + OAuth2ClientDO client = oauth2ClientMapper.selectByClientId(clientId);
  155 + if (client == null) {
  156 + return;
  157 + }
  158 + // 如果 id 为空,说明不用比较是否为相同 id 的客户端
  159 + if (id == null) {
  160 + throw exception(OAUTH2_CLIENT_EXISTS);
  161 + }
  162 + if (!client.getClientId().equals(clientId)) {
  163 + throw exception(OAUTH2_CLIENT_EXISTS);
  164 + }
  165 + }
  166 +
60 167 @Override
61 168 public OAuth2ClientDO getOAuth2Client(Long id) {
62 169 return oauth2ClientMapper.selectById(id);
... ... @@ -68,10 +175,8 @@ public class OAuth2ClientServiceImpl implements OAuth2ClientService {
68 175 }
69 176  
70 177 @Override
71   - public OAuth2ClientDO validOAuthClientFromCache(Long id) {
72   - return new OAuth2ClientDO().setId(id)
73   - .setAccessTokenValiditySeconds(60 * 30)
74   - .setRefreshTokenValiditySeconds(60 * 60 * 24 * 30);
  178 + public OAuth2ClientDO validOAuthClientFromCache(String clientId) {
  179 + return clientCache.get(clientId);
75 180 }
76 181  
77 182 }
... ...
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/auth/OAuth2TokenService.java
... ... @@ -24,7 +24,7 @@ public interface OAuth2TokenService {
24 24 * @param clientId 客户端编号
25 25 * @return 访问令牌的信息
26 26 */
27   - OAuth2AccessTokenDO createAccessToken(Long userId, Integer userType, Long clientId);
  27 + OAuth2AccessTokenDO createAccessToken(Long userId, Integer userType, String clientId);
28 28  
29 29 /**
30 30 * 刷新访问令牌
... ... @@ -35,7 +35,7 @@ public interface OAuth2TokenService {
35 35 * @param clientId 客户端编号
36 36 * @return 访问令牌的信息
37 37 */
38   - OAuth2AccessTokenDO refreshAccessToken(String refreshToken, Long clientId);
  38 + OAuth2AccessTokenDO refreshAccessToken(String refreshToken, String clientId);
39 39  
40 40 /**
41 41 * 获得访问令牌
... ...
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/auth/OAuth2TokenServiceImpl.java
... ... @@ -45,7 +45,7 @@ public class OAuth2TokenServiceImpl implements OAuth2TokenService {
45 45  
46 46 @Override
47 47 @Transactional
48   - public OAuth2AccessTokenDO createAccessToken(Long userId, Integer userType, Long clientId) {
  48 + public OAuth2AccessTokenDO createAccessToken(Long userId, Integer userType, String clientId) {
49 49 OAuth2ClientDO clientDO = oauth2ClientService.validOAuthClientFromCache(clientId);
50 50 // 创建刷新令牌
51 51 OAuth2RefreshTokenDO refreshTokenDO = createOAuth2RefreshToken(userId, userType, clientDO);
... ... @@ -54,7 +54,7 @@ public class OAuth2TokenServiceImpl implements OAuth2TokenService {
54 54 }
55 55  
56 56 @Override
57   - public OAuth2AccessTokenDO refreshAccessToken(String refreshToken, Long clientId) {
  57 + public OAuth2AccessTokenDO refreshAccessToken(String refreshToken, String clientId) {
58 58 // 查询访问令牌
59 59 OAuth2RefreshTokenDO refreshTokenDO = oauth2RefreshTokenMapper.selectByRefreshToken(refreshToken);
60 60 if (refreshTokenDO == null) {
... ... @@ -134,7 +134,7 @@ public class OAuth2TokenServiceImpl implements OAuth2TokenService {
134 134  
135 135 private OAuth2AccessTokenDO createOAuth2AccessToken(OAuth2RefreshTokenDO refreshTokenDO, OAuth2ClientDO clientDO) {
136 136 OAuth2AccessTokenDO accessTokenDO = new OAuth2AccessTokenDO().setAccessToken(generateAccessToken())
137   - .setUserId(refreshTokenDO.getUserId()).setUserType(refreshTokenDO.getUserType()).setClientId(clientDO.getId())
  137 + .setUserId(refreshTokenDO.getUserId()).setUserType(refreshTokenDO.getUserType()).setClientId(clientDO.getClientId())
138 138 .setRefreshToken(refreshTokenDO.getRefreshToken())
139 139 .setExpiresTime(DateUtils.addDate(Calendar.SECOND, clientDO.getAccessTokenValiditySeconds()));
140 140 accessTokenDO.setTenantId(TenantContextHolder.getTenantId()); // 手动设置租户编号,避免缓存到 Redis 的时候,无对应的租户编号
... ... @@ -146,7 +146,7 @@ public class OAuth2TokenServiceImpl implements OAuth2TokenService {
146 146  
147 147 private OAuth2RefreshTokenDO createOAuth2RefreshToken(Long userId, Integer userType, OAuth2ClientDO clientDO) {
148 148 OAuth2RefreshTokenDO refreshToken = new OAuth2RefreshTokenDO().setRefreshToken(generateRefreshToken())
149   - .setUserId(userId).setUserType(userType).setClientId(clientDO.getId())
  149 + .setUserId(userId).setUserType(userType).setClientId(clientDO.getClientId())
150 150 .setExpiresTime(DateUtils.addDate(Calendar.SECOND, clientDO.getRefreshTokenValiditySeconds()));
151 151 oauth2RefreshTokenMapper.insert(refreshToken);
152 152 return refreshToken;
... ...
yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/auth/AuthServiceImplTest.java
... ... @@ -197,7 +197,7 @@ public class AuthServiceImplTest extends BaseDbUnitTest {
197 197 // mock 缓存登录用户到 Redis
198 198 OAuth2AccessTokenDO accessTokenDO = randomPojo(OAuth2AccessTokenDO.class, o -> o.setUserId(1L)
199 199 .setUserType(UserTypeEnum.ADMIN.getValue()));
200   - when(oauth2TokenService.createAccessToken(eq(1L), eq(UserTypeEnum.ADMIN.getValue()), eq(1L)))
  200 + when(oauth2TokenService.createAccessToken(eq(1L), eq(UserTypeEnum.ADMIN.getValue()), eq("default")))
201 201 .thenReturn(accessTokenDO);
202 202  
203 203 // 调用, 并断言异常
... ...
yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/auth/OAuth2ClientServiceImplTest.java
... ... @@ -8,19 +8,23 @@ import cn.iocoder.yudao.module.system.controller.admin.auth.vo.client.OAuth2Clie
8 8 import cn.iocoder.yudao.module.system.controller.admin.auth.vo.client.OAuth2ClientUpdateReqVO;
9 9 import cn.iocoder.yudao.module.system.dal.dataobject.auth.OAuth2ClientDO;
10 10 import cn.iocoder.yudao.module.system.dal.mysql.auth.OAuth2ClientMapper;
  11 +import cn.iocoder.yudao.module.system.mq.producer.auth.OAuth2ClientProducer;
11 12 import org.junit.jupiter.api.Disabled;
12 13 import org.junit.jupiter.api.Test;
  14 +import org.springframework.boot.test.mock.mockito.MockBean;
13 15 import org.springframework.context.annotation.Import;
14 16  
15 17 import javax.annotation.Resource;
  18 +import java.util.Map;
16 19  
17 20 import static cn.iocoder.yudao.framework.common.util.object.ObjectUtils.cloneIgnoreId;
  21 +import static cn.iocoder.yudao.framework.common.util.object.ObjectUtils.max;
18 22 import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertPojoEquals;
19 23 import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException;
20   -import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId;
21   -import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
  24 +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*;
22 25 import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.OAUTH2_CLIENT_NOT_EXISTS;
23 26 import static org.junit.jupiter.api.Assertions.*;
  27 +import static org.mockito.Mockito.verify;
24 28  
25 29 /**
26 30 * {@link OAuth2ClientServiceImpl} 的单元测试类
... ... @@ -31,40 +35,66 @@ import static org.junit.jupiter.api.Assertions.*;
31 35 public class OAuth2ClientServiceImplTest extends BaseDbUnitTest {
32 36  
33 37 @Resource
34   - private OAuth2ClientServiceImpl oAuth2ClientService;
  38 + private OAuth2ClientServiceImpl oauth2ClientService;
35 39  
36 40 @Resource
37   - private OAuth2ClientMapper oAuth2ClientMapper;
  41 + private OAuth2ClientMapper oauth2ClientMapper;
  42 +
  43 + @MockBean
  44 + private OAuth2ClientProducer oauth2ClientProducer;
  45 +
  46 + @Test
  47 + public void testInitLocalCache() {
  48 + // mock 数据
  49 + OAuth2ClientDO clientDO1 = randomPojo(OAuth2ClientDO.class);
  50 + oauth2ClientMapper.insert(clientDO1);
  51 + OAuth2ClientDO clientDO2 = randomPojo(OAuth2ClientDO.class);
  52 + oauth2ClientMapper.insert(clientDO2);
  53 +
  54 + // 调用
  55 + oauth2ClientService.initLocalCache();
  56 + // 断言 clientCache 缓存
  57 + Map<String, OAuth2ClientDO> clientCache = oauth2ClientService.getClientCache();
  58 + assertEquals(2, clientCache.size());
  59 + assertPojoEquals(clientDO1, clientCache.get(clientDO1.getClientId()));
  60 + assertPojoEquals(clientDO2, clientCache.get(clientDO2.getClientId()));
  61 + // 断言 maxUpdateTime 缓存
  62 + assertEquals(max(clientDO1.getUpdateTime(), clientDO2.getUpdateTime()), oauth2ClientService.getMaxUpdateTime());
  63 + }
38 64  
39 65 @Test
40 66 public void testCreateOAuth2Client_success() {
41 67 // 准备参数
42   - OAuth2ClientCreateReqVO reqVO = randomPojo(OAuth2ClientCreateReqVO.class);
  68 + OAuth2ClientCreateReqVO reqVO = randomPojo(OAuth2ClientCreateReqVO.class,
  69 + o -> o.setLogo(randomString()));
43 70  
44 71 // 调用
45   - Long oauth2ClientId = oAuth2ClientService.createOAuth2Client(reqVO);
  72 + Long oauth2ClientId = oauth2ClientService.createOAuth2Client(reqVO);
46 73 // 断言
47 74 assertNotNull(oauth2ClientId);
48 75 // 校验记录的属性是否正确
49   - OAuth2ClientDO oAuth2Client = oAuth2ClientMapper.selectById(oauth2ClientId);
  76 + OAuth2ClientDO oAuth2Client = oauth2ClientMapper.selectById(oauth2ClientId);
50 77 assertPojoEquals(reqVO, oAuth2Client);
  78 + verify(oauth2ClientProducer).sendOAuth2ClientRefreshMessage();
51 79 }
52 80  
53 81 @Test
54 82 public void testUpdateOAuth2Client_success() {
55 83 // mock 数据
56 84 OAuth2ClientDO dbOAuth2Client = randomPojo(OAuth2ClientDO.class);
57   - oAuth2ClientMapper.insert(dbOAuth2Client);// @Sql: 先插入出一条存在的数据
  85 + oauth2ClientMapper.insert(dbOAuth2Client);// @Sql: 先插入出一条存在的数据
58 86 // 准备参数
59 87 OAuth2ClientUpdateReqVO reqVO = randomPojo(OAuth2ClientUpdateReqVO.class, o -> {
60 88 o.setId(dbOAuth2Client.getId()); // 设置更新的 ID
  89 + o.setLogo(randomString());
61 90 });
62 91  
63 92 // 调用
64   - oAuth2ClientService.updateOAuth2Client(reqVO);
  93 + oauth2ClientService.updateOAuth2Client(reqVO);
65 94 // 校验是否更新正确
66   - OAuth2ClientDO oAuth2Client = oAuth2ClientMapper.selectById(reqVO.getId()); // 获取最新的
  95 + OAuth2ClientDO oAuth2Client = oauth2ClientMapper.selectById(reqVO.getId()); // 获取最新的
67 96 assertPojoEquals(reqVO, oAuth2Client);
  97 + verify(oauth2ClientProducer).sendOAuth2ClientRefreshMessage();
68 98 }
69 99  
70 100 @Test
... ... @@ -73,21 +103,22 @@ public class OAuth2ClientServiceImplTest extends BaseDbUnitTest {
73 103 OAuth2ClientUpdateReqVO reqVO = randomPojo(OAuth2ClientUpdateReqVO.class);
74 104  
75 105 // 调用, 并断言异常
76   - assertServiceException(() -> oAuth2ClientService.updateOAuth2Client(reqVO), OAUTH2_CLIENT_NOT_EXISTS);
  106 + assertServiceException(() -> oauth2ClientService.updateOAuth2Client(reqVO), OAUTH2_CLIENT_NOT_EXISTS);
77 107 }
78 108  
79 109 @Test
80 110 public void testDeleteOAuth2Client_success() {
81 111 // mock 数据
82 112 OAuth2ClientDO dbOAuth2Client = randomPojo(OAuth2ClientDO.class);
83   - oAuth2ClientMapper.insert(dbOAuth2Client);// @Sql: 先插入出一条存在的数据
  113 + oauth2ClientMapper.insert(dbOAuth2Client);// @Sql: 先插入出一条存在的数据
84 114 // 准备参数
85 115 Long id = dbOAuth2Client.getId();
86 116  
87 117 // 调用
88   - oAuth2ClientService.deleteOAuth2Client(id);
89   - // 校验数据不存在了
90   - assertNull(oAuth2ClientMapper.selectById(id));
  118 + oauth2ClientService.deleteOAuth2Client(id);
  119 + // 校验数据不存在了
  120 + assertNull(oauth2ClientMapper.selectById(id));
  121 + verify(oauth2ClientProducer).sendOAuth2ClientRefreshMessage();
91 122 }
92 123  
93 124 @Test
... ... @@ -96,7 +127,7 @@ public class OAuth2ClientServiceImplTest extends BaseDbUnitTest {
96 127 Long id = randomLongId();
97 128  
98 129 // 调用, 并断言异常
99   - assertServiceException(() -> oAuth2ClientService.deleteOAuth2Client(id), OAUTH2_CLIENT_NOT_EXISTS);
  130 + assertServiceException(() -> oauth2ClientService.deleteOAuth2Client(id), OAUTH2_CLIENT_NOT_EXISTS);
100 131 }
101 132  
102 133 @Test
... ... @@ -107,18 +138,18 @@ public class OAuth2ClientServiceImplTest extends BaseDbUnitTest {
107 138 o.setName("潜龙");
108 139 o.setStatus(CommonStatusEnum.ENABLE.getStatus());
109 140 });
110   - oAuth2ClientMapper.insert(dbOAuth2Client);
  141 + oauth2ClientMapper.insert(dbOAuth2Client);
111 142 // 测试 name 不匹配
112   - oAuth2ClientMapper.insert(cloneIgnoreId(dbOAuth2Client, o -> o.setName("凤凰")));
  143 + oauth2ClientMapper.insert(cloneIgnoreId(dbOAuth2Client, o -> o.setName("凤凰")));
113 144 // 测试 status 不匹配
114   - oAuth2ClientMapper.insert(cloneIgnoreId(dbOAuth2Client, o -> o.setStatus(CommonStatusEnum.ENABLE.getStatus())));
  145 + oauth2ClientMapper.insert(cloneIgnoreId(dbOAuth2Client, o -> o.setStatus(CommonStatusEnum.ENABLE.getStatus())));
115 146 // 准备参数
116 147 OAuth2ClientPageReqVO reqVO = new OAuth2ClientPageReqVO();
117 148 reqVO.setName("long");
118 149 reqVO.setStatus(CommonStatusEnum.ENABLE.getStatus());
119 150  
120 151 // 调用
121   - PageResult<OAuth2ClientDO> pageResult = oAuth2ClientService.getOAuth2ClientPage(reqVO);
  152 + PageResult<OAuth2ClientDO> pageResult = oauth2ClientService.getOAuth2ClientPage(reqVO);
122 153 // 断言
123 154 assertEquals(1, pageResult.getTotal());
124 155 assertEquals(1, pageResult.getList().size());
... ...
yudao-module-system/yudao-module-system-biz/src/test/resources/sql/create_tables.sql
... ... @@ -473,6 +473,7 @@ CREATE TABLE IF NOT EXISTS &quot;system_sensitive_word&quot; (
473 473  
474 474 CREATE TABLE IF NOT EXISTS "system_oauth2_client" (
475 475 "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
  476 + "client_id" varchar NOT NULL,
476 477 "secret" varchar NOT NULL,
477 478 "name" varchar NOT NULL,
478 479 "logo" varchar NOT NULL,
... ... @@ -481,6 +482,12 @@ CREATE TABLE IF NOT EXISTS &quot;system_oauth2_client&quot; (
481 482 "access_token_validity_seconds" int NOT NULL,
482 483 "refresh_token_validity_seconds" int NOT NULL,
483 484 "redirect_uris" varchar NOT NULL,
  485 + "auto_approve" bit NOT NULL DEFAULT FALSE,
  486 + "authorized_grant_types" varchar NOT NULL,
  487 + "scopes" varchar NOT NULL DEFAULT '',
  488 + "authorities" varchar NOT NULL DEFAULT '',
  489 + "resource_ids" varchar NOT NULL DEFAULT '',
  490 + "additional_information" varchar NOT NULL DEFAULT '',
484 491 "creator" varchar DEFAULT '',
485 492 "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
486 493 "updater" varchar DEFAULT '',
... ...
yudao-ui-admin/src/components/generator/config.js
... ... @@ -499,7 +499,7 @@ export const selectComponents = [
499 499 __slot__: {
500 500 'list-type': true
501 501 },
502   - action: 'https://jsonplaceholder.typicode.com/posts/',
  502 + action: process.env.VUE_APP_BASE_API + "/admin-api/infra/file/upload", // 请求地址
503 503 disabled: false,
504 504 accept: '',
505 505 name: 'file',
... ...
yudao-ui-admin/src/utils/dict.js
... ... @@ -23,6 +23,7 @@ export const DICT_TYPE = {
23 23 SYSTEM_SMS_SEND_STATUS: 'system_sms_send_status',
24 24 SYSTEM_SMS_RECEIVE_STATUS: 'system_sms_receive_status',
25 25 SYSTEM_ERROR_CODE_TYPE: 'system_error_code_type',
  26 + SYSTEM_OAUTH2_GRANT_TYPE: 'system_oauth2_grant_type',
26 27  
27 28 // ========== INFRA 模块 ==========
28 29 INFRA_BOOLEAN_STRING: 'infra_boolean_string',
... ...
yudao-ui-admin/src/views/system/oauth2/client/index.vue
... ... @@ -29,19 +29,32 @@
29 29  
30 30 <!-- 列表 -->
31 31 <el-table v-loading="loading" :data="list">
32   - <el-table-column label="客户端编号" align="center" prop="id" />
  32 + <el-table-column label="客户端编号" align="center" prop="clientId" />
33 33 <el-table-column label="客户端密钥" align="center" prop="secret" />
34 34 <el-table-column label="应用名" align="center" prop="name" />
35   - <el-table-column label="应用图标" align="center" prop="logo" />
36   - <el-table-column label="应用描述" align="center" prop="description" />
  35 + <el-table-column label="应用图标" align="center" prop="logo">
  36 + <template slot-scope="scope">
  37 + <img width="40px" height="40px" :src="scope.row.logo">
  38 + </template>
  39 + </el-table-column>
37 40 <el-table-column label="状态" align="center" prop="status">
38 41 <template slot-scope="scope">
39 42 <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
40 43 </template>
41 44 </el-table-column>
42   - <el-table-column label="访问令牌的有效期" align="center" prop="accessTokenValiditySeconds" />
43   - <el-table-column label="刷新令牌的有效期" align="center" prop="refreshTokenValiditySeconds" />
44   - <el-table-column label="可重定向的 URI 地址" align="center" prop="redirectUris" />
  45 + <el-table-column label="访问令牌的有效期" align="center" prop="accessTokenValiditySeconds">
  46 + <template slot-scope="scope">{{ scope.row.accessTokenValiditySeconds }} 秒</template>
  47 + </el-table-column>
  48 + <el-table-column label="刷新令牌的有效期" align="center" prop="refreshTokenValiditySeconds">
  49 + <template slot-scope="scope">{{ scope.row.refreshTokenValiditySeconds }} 秒</template>
  50 + </el-table-column>
  51 + <el-table-column label="授权类型" align="center" prop="authorizedGrantTypes">
  52 + <template slot-scope="scope">
  53 + <el-tag :disable-transitions="true" v-for="(authorizedGrantType, index) in scope.row.authorizedGrantTypes" :index="index">
  54 + {{ authorizedGrantType }}
  55 + </el-tag>
  56 + </template>
  57 + </el-table-column>
45 58 <el-table-column label="创建时间" align="center" prop="createTime" width="180">
46 59 <template slot-scope="scope">
47 60 <span>{{ parseTime(scope.row.createTime) }}</span>
... ... @@ -63,6 +76,9 @@
63 76 <!-- 对话框(添加 / 修改) -->
64 77 <el-dialog :title="title" :visible.sync="open" width="700px" append-to-body>
65 78 <el-form ref="form" :model="form" :rules="rules" label-width="160px">
  79 + <el-form-item label="客户端编号" prop="secret">
  80 + <el-input v-model="form.clientId" placeholder="请输入客户端编号" />
  81 + </el-form-item>
66 82 <el-form-item label="客户端密钥" prop="secret">
67 83 <el-input v-model="form.secret" placeholder="请输入客户端密钥" />
68 84 </el-form-item>
... ... @@ -70,8 +86,7 @@
70 86 <el-input v-model="form.name" placeholder="请输入应用名" />
71 87 </el-form-item>
72 88 <el-form-item label="应用图标">
73   -<!-- <imageUpload v-model="form.logo" :limit="1"/>-->
74   - <file-upload v-model="form.logo" :limit="1"/>
  89 + <imageUpload v-model="form.logo" :limit="1"/>
75 90 </el-form-item>
76 91 <el-form-item label="应用描述">
77 92 <el-input type="textarea" v-model="form.description" placeholder="请输入应用名" />
... ... @@ -89,7 +104,39 @@
89 104 <el-input-number v-model="form.refreshTokenValiditySeconds" placeholder="单位:秒" />
90 105 </el-form-item>
91 106 <el-form-item label="可重定向的 URI 地址" prop="redirectUris">
92   - <el-input v-model="form.redirectUris" placeholder="请输入可重定向的 URI 地址" />
  107 + <el-select v-model="form.redirectUris" multiple filterable allow-create placeholder="请输入可重定向的 URI 地址" style="width: 500px" >
  108 + <el-option v-for="redirectUri in form.redirectUris" :key="redirectUri" :label="redirectUri" :value="redirectUri"/>
  109 + </el-select>
  110 + </el-form-item>
  111 + <el-form-item label="是否自动授权" prop="autoApprove">
  112 + <el-radio-group v-model="form.autoApprove">
  113 + <el-radio :key="true" :label="true">自动登录</el-radio>
  114 + <el-radio :key="false" :label="false">手动登录</el-radio>
  115 + </el-radio-group>
  116 + </el-form-item>
  117 + <el-form-item label="授权类型" prop="authorizedGrantTypes">
  118 + <el-select v-model="form.authorizedGrantTypes" multiple filterable placeholder="请输入授权类型" style="width: 500px" >
  119 + <el-option v-for="dict in this.getDictDatas(DICT_TYPE.SYSTEM_OAUTH2_GRANT_TYPE)"
  120 + :key="dict.value" :label="dict.label" :value="dict.value"/>
  121 + </el-select>
  122 + </el-form-item>
  123 + <el-form-item label="授权范围" prop="scopes">
  124 + <el-select v-model="form.scopes" multiple filterable allow-create placeholder="请输入授权范围" style="width: 500px" >
  125 + <el-option v-for="scope in form.scopes" :key="scope" :label="scope" :value="scope"/>
  126 + </el-select>
  127 + </el-form-item>
  128 + <el-form-item label="权限" prop="authorities">
  129 + <el-select v-model="form.authorities" multiple filterable allow-create placeholder="请输入权限" style="width: 500px" >
  130 + <el-option v-for="authority in form.authorities" :key="authority" :label="authority" :value="authority"/>
  131 + </el-select>
  132 + </el-form-item>
  133 + <el-form-item label="资源" prop="resourceIds">
  134 + <el-select v-model="form.resourceIds" multiple filterable allow-create placeholder="请输入资源" style="width: 500px" >
  135 + <el-option v-for="resourceId in form.resourceIds" :key="resourceId" :label="resourceId" :value="resourceId"/>
  136 + </el-select>
  137 + </el-form-item>
  138 + <el-form-item label="附加信息" prop="additionalInformation">
  139 + <el-input type="textarea" v-model="form.additionalInformation" placeholder="请输入附加信息,JSON 格式数据" />
93 140 </el-form-item>
94 141 </el-form>
95 142 <div slot="footer" class="dialog-footer">
... ... @@ -141,6 +188,7 @@ export default {
141 188 form: {},
142 189 // 表单校验
143 190 rules: {
  191 + clientId: [{ required: true, message: "客户端编号不能为空", trigger: "blur" }],
144 192 secret: [{ required: true, message: "客户端密钥不能为空", trigger: "blur" }],
145 193 name: [{ required: true, message: "应用名不能为空", trigger: "blur" }],
146 194 logo: [{ required: true, message: "应用图标不能为空", trigger: "blur" }],
... ... @@ -148,6 +196,8 @@ export default {
148 196 accessTokenValiditySeconds: [{ required: true, message: "访问令牌的有效期不能为空", trigger: "blur" }],
149 197 refreshTokenValiditySeconds: [{ required: true, message: "刷新令牌的有效期不能为空", trigger: "blur" }],
150 198 redirectUris: [{ required: true, message: "可重定向的 URI 地址不能为空", trigger: "blur" }],
  199 + autoApprove: [{ required: true, message: "是否自动授权不能为空", trigger: "blur" }],
  200 + authorizedGrantTypes: [{ required: true, message: "授权类型不能为空", trigger: "blur" }],
151 201 }
152 202 };
153 203 },
... ... @@ -176,14 +226,21 @@ export default {
176 226 reset() {
177 227 this.form = {
178 228 id: undefined,
  229 + clientId: undefined,
179 230 secret: undefined,
180 231 name: undefined,
181 232 logo: undefined,
182 233 description: undefined,
183 234 status: CommonStatusEnum.ENABLE,
184   - accessTokenValiditySeconds: undefined,
185   - refreshTokenValiditySeconds: undefined,
186   - redirectUris: undefined,
  235 + accessTokenValiditySeconds: 30 * 60,
  236 + refreshTokenValiditySeconds: 30 * 24 * 60,
  237 + redirectUris: [],
  238 + autoApprove: true,
  239 + authorizedGrantTypes: [],
  240 + scopes: [],
  241 + authorities: [],
  242 + resourceIds: [],
  243 + additionalInformation: undefined,
187 244 };
188 245 this.resetForm("form");
189 246 },
... ... @@ -239,7 +296,7 @@ export default {
239 296 /** 删除按钮操作 */
240 297 handleDelete(row) {
241 298 const id = row.id;
242   - this.$modal.confirm('是否确认删除 OAuth2 客户端编号为"' + id + '"的数据项?').then(function() {
  299 + this.$modal.confirm('是否确认删除客户端编号为"' + row.clientId + '"的数据项?').then(function() {
243 300 return deleteOAuth2Client(id);
244 301 }).then(() => {
245 302 this.getList();
... ...