完成 OAuth2 的客户端模块

This commit is contained in:
YunaiV 2022-05-12 01:09:16 +08:00
parent 1f36af8e6a
commit 97db4586a8
34 changed files with 511 additions and 104 deletions

View File

@ -113,8 +113,7 @@ public class JsonUtils {
} }
} }
// TODO @Li和上面的风格保持一致哈parseTree public static JsonNode parseTree(String text) {
public static JsonNode readTree(String text) {
try { try {
return objectMapper.readTree(text); return objectMapper.readTree(text);
} catch (IOException e) { } catch (IOException e) {
@ -123,7 +122,7 @@ public class JsonUtils {
} }
} }
public static JsonNode readTree(byte[] text) { public static JsonNode parseTree(byte[] text) {
try { try {
return objectMapper.readTree(text); return objectMapper.readTree(text);
} catch (IOException e) { } catch (IOException e) {
@ -132,4 +131,8 @@ public class JsonUtils {
} }
} }
public static boolean isJson(String text) {
return JSONUtil.isJson(text);
}
} }

View File

@ -18,7 +18,7 @@ import cn.iocoder.yudao.module.system.api.logger.LoginLogApi;
import cn.iocoder.yudao.module.system.api.logger.dto.LoginLogCreateReqDTO; import cn.iocoder.yudao.module.system.api.logger.dto.LoginLogCreateReqDTO;
import cn.iocoder.yudao.module.system.api.sms.SmsCodeApi; import cn.iocoder.yudao.module.system.api.sms.SmsCodeApi;
import cn.iocoder.yudao.module.system.api.social.SocialUserApi; import cn.iocoder.yudao.module.system.api.social.SocialUserApi;
import cn.iocoder.yudao.module.system.enums.auth.OAuth2ClientIdEnum; import cn.iocoder.yudao.module.system.enums.auth.OAuth2ClientConstants;
import cn.iocoder.yudao.module.system.enums.logger.LoginLogTypeEnum; import cn.iocoder.yudao.module.system.enums.logger.LoginLogTypeEnum;
import cn.iocoder.yudao.module.system.enums.logger.LoginResultEnum; import cn.iocoder.yudao.module.system.enums.logger.LoginResultEnum;
import cn.iocoder.yudao.module.system.enums.sms.SmsSceneEnum; import cn.iocoder.yudao.module.system.enums.sms.SmsSceneEnum;
@ -120,7 +120,7 @@ public class MemberAuthServiceImpl implements MemberAuthService {
createLoginLog(user.getId(), mobile, logType, LoginResultEnum.SUCCESS); createLoginLog(user.getId(), mobile, logType, LoginResultEnum.SUCCESS);
// 创建 Token 令牌 // 创建 Token 令牌
OAuth2AccessTokenRespDTO accessTokenRespDTO = oauth2TokenApi.createAccessToken(new OAuth2AccessTokenCreateReqDTO() OAuth2AccessTokenRespDTO accessTokenRespDTO = oauth2TokenApi.createAccessToken(new OAuth2AccessTokenCreateReqDTO()
.setUserId(user.getId()).setUserType(getUserType().getValue()).setClientId(OAuth2ClientIdEnum.DEFAULT.getId())); .setUserId(user.getId()).setUserType(getUserType().getValue()).setClientId(OAuth2ClientConstants.CLIENT_ID_DEFAULT));
// 构建返回结果 // 构建返回结果
return AuthConvert.INSTANCE.convert(accessTokenRespDTO); return AuthConvert.INSTANCE.convert(accessTokenRespDTO);
} }
@ -212,7 +212,7 @@ public class MemberAuthServiceImpl implements MemberAuthService {
@Override @Override
public AppAuthLoginRespVO refreshToken(String refreshToken) { public AppAuthLoginRespVO refreshToken(String refreshToken) {
OAuth2AccessTokenRespDTO accessTokenDO = oauth2TokenApi.refreshAccessToken(refreshToken, OAuth2ClientIdEnum.DEFAULT.getId()); OAuth2AccessTokenRespDTO accessTokenDO = oauth2TokenApi.refreshAccessToken(refreshToken, OAuth2ClientConstants.CLIENT_ID_DEFAULT);
return AuthConvert.INSTANCE.convert(accessTokenDO); return AuthConvert.INSTANCE.convert(accessTokenDO);
} }

View File

@ -44,6 +44,6 @@ public interface OAuth2TokenApi {
* @param clientId 客户端编号 * @param clientId 客户端编号
* @return 访问令牌的信息 * @return 访问令牌的信息
*/ */
OAuth2AccessTokenRespDTO refreshAccessToken(String refreshToken, Long clientId); OAuth2AccessTokenRespDTO refreshAccessToken(String refreshToken, String clientId);
} }

View File

@ -30,6 +30,6 @@ public class OAuth2AccessTokenCreateReqDTO implements Serializable {
* 客户端编号 * 客户端编号
*/ */
@NotNull(message = "客户端编号不能为空") @NotNull(message = "客户端编号不能为空")
private Long clientId; private String clientId;
} }

View File

@ -125,5 +125,6 @@ public interface ErrorCodeConstants {
// ========== 系统敏感词 1002020000 ========= // ========== 系统敏感词 1002020000 =========
ErrorCode OAUTH2_CLIENT_NOT_EXISTS = new ErrorCode(1002020000, "OAuth2 客户端不存在"); ErrorCode OAUTH2_CLIENT_NOT_EXISTS = new ErrorCode(1002020000, "OAuth2 客户端不存在");
ErrorCode OAUTH2_CLIENT_EXISTS = new ErrorCode(1002020001, "OAuth2 客户端编号已存在");
} }

View File

@ -0,0 +1,12 @@
package cn.iocoder.yudao.module.system.enums.auth;
/**
* OAuth2.0 客户端的通用枚举
*
* @author 芋道源码
*/
public interface OAuth2ClientConstants {
String CLIENT_ID_DEFAULT = "default";
}

View File

@ -1,17 +0,0 @@
package cn.iocoder.yudao.module.system.enums.auth;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* OAuth2.0 客户端的编号枚举
*/
@AllArgsConstructor
@Getter
public enum OAuth2ClientIdEnum {
DEFAULT(1L); // 系统默认
private final Long id;
}

View File

@ -0,0 +1,24 @@
package cn.iocoder.yudao.module.system.enums.auth;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* OAuth2 授权类型模式的枚举
*
* @author 芋道源码
*/
@AllArgsConstructor
@Getter
public enum OAuth2GrantTypeEnum {
PASSWORD("password"), // 密码模式
AUTHORIZATION_CODE("authorization_code"), // 授权码模式
IMPLICIT("implicit"), // 简化模式
CLIENT_CREDENTIALS("client_credentials"), // 客户端模式
REFRESH_TOKEN("refresh_token"), // 刷新模式
;
private final String grantType;
}

View File

@ -40,7 +40,7 @@ public class OAuth2TokenApiImpl implements OAuth2TokenApi {
} }
@Override @Override
public OAuth2AccessTokenRespDTO refreshAccessToken(String refreshToken, Long clientId) { public OAuth2AccessTokenRespDTO refreshAccessToken(String refreshToken, String clientId) {
OAuth2AccessTokenDO accessTokenDO = oauth2TokenService.refreshAccessToken(refreshToken, clientId); OAuth2AccessTokenDO accessTokenDO = oauth2TokenService.refreshAccessToken(refreshToken, clientId);
return OAuth2TokenConvert.INSTANCE.convert2(accessTokenDO); return OAuth2TokenConvert.INSTANCE.convert2(accessTokenDO);
} }

View File

@ -0,0 +1,23 @@
### 请求 /login 接口 => 成功
POST {{baseUrl}}/system/oauth2-client/create
Content-Type: application/json
Authorization: Bearer {{token}}
tenant-id: {{adminTenentId}}
{
"id": "1",
"secret": "admin123",
"name": "芋道源码",
"logo": "https://www.iocoder.cn/images/favicon.ico",
"description": "我是描述",
"status": 0,
"accessTokenValiditySeconds": 180,
"refreshTokenValiditySeconds": 8640,
"redirectUris": ["https://www.iocoder.cn"],
"autoApprove": true,
"authorizedGrantTypes": ["password"],
"scopes": ["user_info"],
"authorities": ["system:user:query"],
"resource_ids": ["1024"],
"additionalInformation": "{}"
}

View File

@ -1,8 +1,13 @@
package cn.iocoder.yudao.module.system.controller.admin.auth.vo.client; package cn.iocoder.yudao.module.system.controller.admin.auth.vo.client;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import io.swagger.annotations.ApiModelProperty; import io.swagger.annotations.ApiModelProperty;
import lombok.Data; import lombok.Data;
import org.hibernate.validator.constraints.URL;
import javax.validation.constraints.AssertTrue;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull; import javax.validation.constraints.NotNull;
import java.util.List; import java.util.List;
@ -15,7 +20,7 @@ public class OAuth2ClientBaseVO {
@ApiModelProperty(value = "客户端编号", required = true) @ApiModelProperty(value = "客户端编号", required = true)
@NotNull(message = "客户端编号不能为空") @NotNull(message = "客户端编号不能为空")
private Long id; private String clientId;
@ApiModelProperty(value = "客户端密钥", required = true) @ApiModelProperty(value = "客户端密钥", required = true)
@NotNull(message = "客户端密钥不能为空") @NotNull(message = "客户端密钥不能为空")
@ -27,6 +32,7 @@ public class OAuth2ClientBaseVO {
@ApiModelProperty(value = "应用图标", required = true) @ApiModelProperty(value = "应用图标", required = true)
@NotNull(message = "应用图标不能为空") @NotNull(message = "应用图标不能为空")
@URL(message = "应用图标的地址不正确")
private String logo; private String logo;
@ApiModelProperty(value = "应用描述") @ApiModelProperty(value = "应用描述")
@ -46,6 +52,32 @@ public class OAuth2ClientBaseVO {
@ApiModelProperty(value = "可重定向的 URI 地址", required = true) @ApiModelProperty(value = "可重定向的 URI 地址", required = true)
@NotNull(message = "可重定向的 URI 地址不能为空") @NotNull(message = "可重定向的 URI 地址不能为空")
private List<String> redirectUris; private List<@NotEmpty(message = "重定向的 URI 不能为空")
@URL(message = "重定向的 URI 格式不正确") String> redirectUris;
@ApiModelProperty(value = "是否自动授权", required = true, example = "true")
@NotNull(message = "是否自动授权不能为空")
private Boolean autoApprove;
@ApiModelProperty(value = "授权类型", required = true, example = "password", notes = "参见 OAuth2GrantTypeEnum 枚举")
@NotNull(message = "授权类型不能为空")
private List<String> authorizedGrantTypes;
@ApiModelProperty(value = "授权范围", example = "user_info")
private List<String> scopes;
@ApiModelProperty(value = "权限", example = "system:user:query")
private List<String> authorities;
@ApiModelProperty(value = "资源", example = "1024")
private List<String> resourceIds;
@ApiModelProperty(value = "附加信息", example = "{yunai: true}")
private String additionalInformation;
@AssertTrue(message = "附加信息必须是 JSON 格式")
public boolean isAdditionalInformationJson() {
return StrUtil.isEmpty(additionalInformation) || JsonUtils.isJson(additionalInformation);
}
} }

View File

@ -14,6 +14,9 @@ import java.util.Date;
@ToString(callSuper = true) @ToString(callSuper = true)
public class OAuth2ClientRespVO extends OAuth2ClientBaseVO { public class OAuth2ClientRespVO extends OAuth2ClientBaseVO {
@ApiModelProperty(value = "编号", required = true)
private Long id;
@ApiModelProperty(value = "创建时间", required = true) @ApiModelProperty(value = "创建时间", required = true)
private Date createTime; private Date createTime;

View File

@ -1,14 +1,21 @@
package cn.iocoder.yudao.module.system.controller.admin.auth.vo.client; package cn.iocoder.yudao.module.system.controller.admin.auth.vo.client;
import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import lombok.ToString; import lombok.ToString;
import javax.validation.constraints.NotNull;
@ApiModel("管理后台 - OAuth2 客户端更新 Request VO") @ApiModel("管理后台 - OAuth2 客户端更新 Request VO")
@Data @Data
@EqualsAndHashCode(callSuper = true) @EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true) @ToString(callSuper = true)
public class OAuth2ClientUpdateReqVO extends OAuth2ClientBaseVO { public class OAuth2ClientUpdateReqVO extends OAuth2ClientBaseVO {
@ApiModelProperty(value = "编号", required = true)
@NotNull(message = "编号不能为空")
private Long id;
} }

View File

@ -18,6 +18,6 @@ public class OAuth2AccessTokenPageReqVO extends PageParam {
private Integer userType; private Integer userType;
@ApiModelProperty(value = "客户端编号", required = true, example = "2") @ApiModelProperty(value = "客户端编号", required = true, example = "2")
private Long clientId; private String clientId;
} }

View File

@ -30,7 +30,7 @@ public class OAuth2AccessTokenRespVO {
private Integer userType; private Integer userType;
@ApiModelProperty(value = "客户端编号", required = true, example = "2") @ApiModelProperty(value = "客户端编号", required = true, example = "2")
private Long clientId; private String clientId;
@ApiModelProperty(value = "创建时间", required = true) @ApiModelProperty(value = "创建时间", required = true)
private Date createTime; private Date createTime;

View File

@ -52,7 +52,7 @@ public class OAuth2AccessTokenDO extends TenantBaseDO {
* *
* 关联 {@link OAuth2ClientDO#getId()} * 关联 {@link OAuth2ClientDO#getId()}
*/ */
private Long clientId; private String clientId;
/** /**
* 过期时间 * 过期时间
*/ */

View File

@ -2,38 +2,37 @@ package cn.iocoder.yudao.module.system.dal.dataobject.auth;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.IdType; import cn.iocoder.yudao.module.system.enums.auth.OAuth2GrantTypeEnum;
import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName; import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import java.util.List; import java.util.List;
/** /**
* OAuth2 客户端 DO * OAuth2 客户端 DO
* *
* 如下字段考虑到使用相对不是很高频主要是一些开关暂时不支持
* authorized_grant_typesauthoritiesadditional_informationautoapproveresource_idsscope
*
* @author 芋道源码 * @author 芋道源码
*/ */
@TableName(value = "system_oauth2_client", autoResultMap = true) @TableName(value = "system_oauth2_client", autoResultMap = true)
@Data @Data
@EqualsAndHashCode(callSuper = true) @EqualsAndHashCode(callSuper = true)
@Accessors(chain = true)
public class OAuth2ClientDO extends BaseDO { public class OAuth2ClientDO extends BaseDO {
/** /**
* 客户端编号 * 编号数据库自增
* *
* 由于 SQL Server 在存储 String 主键有点问题所以暂时使用 Long 类型 * 由于 SQL Server 在存储 String 主键有点问题所以暂时使用 Long 类型
*/ */
@TableId(type = IdType.INPUT) @TableId
private Long id; private Long id;
/**
* 客户端编号
*/
private String clientId;
/** /**
* 客户端密钥 * 客户端密钥
*/ */
@ -69,5 +68,35 @@ public class OAuth2ClientDO extends BaseDO {
*/ */
@TableField(typeHandler = JacksonTypeHandler.class) @TableField(typeHandler = JacksonTypeHandler.class)
private List<String> redirectUris; private List<String> redirectUris;
/**
* 是否自动授权
*/
private Boolean autoApprove;
/**
* 授权类型模式
*
* 枚举 {@link OAuth2GrantTypeEnum}
*/
@TableField(typeHandler = JacksonTypeHandler.class)
private List<String> authorizedGrantTypes;
/**
* 授权范围
*/
@TableField(typeHandler = JacksonTypeHandler.class)
private List<String> scopes;
/**
* 权限
*/
@TableField(typeHandler = JacksonTypeHandler.class)
private List<String> authorities;
/**
* 资源
*/
@TableField(typeHandler = JacksonTypeHandler.class)
private List<String> resourceIds;
/**
* 附加信息JSON 格式
*/
private String additionalInformation;
} }

View File

@ -5,7 +5,6 @@ import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.TableName; import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import java.util.Date; import java.util.Date;
@ -17,7 +16,6 @@ import java.util.Date;
@TableName("system_oauth2_code") @TableName("system_oauth2_code")
@Data @Data
@EqualsAndHashCode(callSuper = true) @EqualsAndHashCode(callSuper = true)
@Accessors(chain = true)
public class OAuth2CodeDO extends BaseDO { public class OAuth2CodeDO extends BaseDO {
/** /**
@ -43,7 +41,7 @@ public class OAuth2CodeDO extends BaseDO {
* *
* 关联 {@link OAuth2ClientDO#getId()} * 关联 {@link OAuth2ClientDO#getId()}
*/ */
private Long clientId; private String clientId;
/** /**
* 刷新令牌 * 刷新令牌
* *

View File

@ -43,7 +43,7 @@ public class OAuth2RefreshTokenDO extends BaseDO {
* *
* 关联 {@link OAuth2ClientDO#getId()} * 关联 {@link OAuth2ClientDO#getId()}
*/ */
private Long clientId; private String clientId;
/** /**
* 过期时间 * 过期时间
*/ */

View File

@ -6,6 +6,9 @@ import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.system.controller.admin.auth.vo.client.OAuth2ClientPageReqVO; import cn.iocoder.yudao.module.system.controller.admin.auth.vo.client.OAuth2ClientPageReqVO;
import cn.iocoder.yudao.module.system.dal.dataobject.auth.OAuth2ClientDO; import cn.iocoder.yudao.module.system.dal.dataobject.auth.OAuth2ClientDO;
import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import java.util.Date;
/** /**
* OAuth2 客户端 Mapper * OAuth2 客户端 Mapper
@ -22,4 +25,11 @@ public interface OAuth2ClientMapper extends BaseMapperX<OAuth2ClientDO> {
.orderByDesc(OAuth2ClientDO::getId)); .orderByDesc(OAuth2ClientDO::getId));
} }
default OAuth2ClientDO selectByClientId(String clientId) {
return selectOne(OAuth2ClientDO::getClientId, clientId);
}
@Select("SELECT COUNT(*) FROM system_oauth2_client WHERE update_time > #{maxUpdateTime}")
int selectCountByUpdateTimeGt(Date maxUpdateTime);
} }

View File

@ -0,0 +1,29 @@
package cn.iocoder.yudao.module.system.mq.consumer.auth;
import cn.iocoder.yudao.framework.mq.core.pubsub.AbstractChannelMessageListener;
import cn.iocoder.yudao.module.system.mq.message.auth.OAuth2ClientRefreshMessage;
import cn.iocoder.yudao.module.system.service.auth.OAuth2ClientService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
/**
* 针对 {@link OAuth2ClientRefreshMessage} 的消费者
*
* @author 芋道源码
*/
@Component
@Slf4j
public class OAuth2ClientRefreshConsumer extends AbstractChannelMessageListener<OAuth2ClientRefreshMessage> {
@Resource
private OAuth2ClientService oauth2ClientService;
@Override
public void onMessage(OAuth2ClientRefreshMessage message) {
log.info("[onMessage][收到 OAuth2Client 刷新消息]");
oauth2ClientService.initLocalCache();
}
}

View File

@ -0,0 +1,21 @@
package cn.iocoder.yudao.module.system.mq.message.auth;
import cn.iocoder.yudao.framework.mq.core.pubsub.AbstractChannelMessage;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* OAuth 2.0 客户端的数据刷新 Message
*
* @author 芋道源码
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class OAuth2ClientRefreshMessage extends AbstractChannelMessage {
@Override
public String getChannel() {
return "system.oauth2-client.refresh";
}
}

View File

@ -0,0 +1,26 @@
package cn.iocoder.yudao.module.system.mq.producer.auth;
import cn.iocoder.yudao.framework.mq.core.RedisMQTemplate;
import cn.iocoder.yudao.module.system.mq.message.auth.OAuth2ClientRefreshMessage;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
/**
* OAuth 2.0 客户端相关消息的 Producer
*/
@Component
public class OAuth2ClientProducer {
@Resource
private RedisMQTemplate redisMQTemplate;
/**
* 发送 {@link OAuth2ClientRefreshMessage} 消息
*/
public void sendOAuth2ClientRefreshMessage() {
OAuth2ClientRefreshMessage message = new OAuth2ClientRefreshMessage();
redisMQTemplate.send(message);
}
}

View File

@ -12,7 +12,7 @@ import cn.iocoder.yudao.module.system.controller.admin.auth.vo.auth.*;
import cn.iocoder.yudao.module.system.convert.auth.AuthConvert; import cn.iocoder.yudao.module.system.convert.auth.AuthConvert;
import cn.iocoder.yudao.module.system.dal.dataobject.auth.OAuth2AccessTokenDO; import cn.iocoder.yudao.module.system.dal.dataobject.auth.OAuth2AccessTokenDO;
import cn.iocoder.yudao.module.system.dal.dataobject.user.AdminUserDO; import cn.iocoder.yudao.module.system.dal.dataobject.user.AdminUserDO;
import cn.iocoder.yudao.module.system.enums.auth.OAuth2ClientIdEnum; import cn.iocoder.yudao.module.system.enums.auth.OAuth2ClientConstants;
import cn.iocoder.yudao.module.system.enums.logger.LoginLogTypeEnum; import cn.iocoder.yudao.module.system.enums.logger.LoginLogTypeEnum;
import cn.iocoder.yudao.module.system.enums.logger.LoginResultEnum; import cn.iocoder.yudao.module.system.enums.logger.LoginResultEnum;
import cn.iocoder.yudao.module.system.enums.sms.SmsSceneEnum; import cn.iocoder.yudao.module.system.enums.sms.SmsSceneEnum;
@ -197,7 +197,7 @@ public class AdminAuthServiceImpl implements AdminAuthService {
@Override @Override
public AuthLoginRespVO refreshToken(String refreshToken) { public AuthLoginRespVO refreshToken(String refreshToken) {
OAuth2AccessTokenDO accessTokenDO = oauth2TokenService.refreshAccessToken(refreshToken, OAuth2ClientIdEnum.DEFAULT.getId()); OAuth2AccessTokenDO accessTokenDO = oauth2TokenService.refreshAccessToken(refreshToken, OAuth2ClientConstants.CLIENT_ID_DEFAULT);
return AuthConvert.INSTANCE.convert(accessTokenDO); return AuthConvert.INSTANCE.convert(accessTokenDO);
} }
@ -206,7 +206,7 @@ public class AdminAuthServiceImpl implements AdminAuthService {
createLoginLog(userId, username, logType, LoginResultEnum.SUCCESS); createLoginLog(userId, username, logType, LoginResultEnum.SUCCESS);
// 创建访问令牌 // 创建访问令牌
OAuth2AccessTokenDO accessTokenDO = oauth2TokenService.createAccessToken(userId, getUserType().getValue(), OAuth2AccessTokenDO accessTokenDO = oauth2TokenService.createAccessToken(userId, getUserType().getValue(),
OAuth2ClientIdEnum.DEFAULT.getId()); OAuth2ClientConstants.CLIENT_ID_DEFAULT);
// 构建返回结果 // 构建返回结果
return AuthConvert.INSTANCE.convert(accessTokenDO); return AuthConvert.INSTANCE.convert(accessTokenDO);
} }

View File

@ -17,6 +17,11 @@ import javax.validation.Valid;
*/ */
public interface OAuth2ClientService { public interface OAuth2ClientService {
/**
* 初始化 OAuth2Client 的本地缓存
*/
void initLocalCache();
/** /**
* 创建 OAuth2 客户端 * 创建 OAuth2 客户端
* *
@ -58,9 +63,9 @@ public interface OAuth2ClientService {
/** /**
* 从缓存中校验客户端是否合法 * 从缓存中校验客户端是否合法
* *
* @param id 客户端编号 * @param clientId 客户端编号
* @return 客户端 * @return 客户端
*/ */
OAuth2ClientDO validOAuthClientFromCache(Long id); OAuth2ClientDO validOAuthClientFromCache(String clientId);
} }

View File

@ -1,5 +1,6 @@
package cn.iocoder.yudao.module.system.service.auth; package cn.iocoder.yudao.module.system.service.auth;
import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.system.controller.admin.auth.vo.client.OAuth2ClientCreateReqVO; import cn.iocoder.yudao.module.system.controller.admin.auth.vo.client.OAuth2ClientCreateReqVO;
import cn.iocoder.yudao.module.system.controller.admin.auth.vo.client.OAuth2ClientPageReqVO; 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
import cn.iocoder.yudao.module.system.convert.auth.OAuth2ClientConvert; import cn.iocoder.yudao.module.system.convert.auth.OAuth2ClientConvert;
import cn.iocoder.yudao.module.system.dal.dataobject.auth.OAuth2ClientDO; import cn.iocoder.yudao.module.system.dal.dataobject.auth.OAuth2ClientDO;
import cn.iocoder.yudao.module.system.dal.mysql.auth.OAuth2ClientMapper; import cn.iocoder.yudao.module.system.dal.mysql.auth.OAuth2ClientMapper;
import cn.iocoder.yudao.module.system.mq.producer.auth.OAuth2ClientProducer;
import com.google.common.annotations.VisibleForTesting;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import javax.annotation.PostConstruct;
import javax.annotation.Resource; import javax.annotation.Resource;
import java.util.Date;
import java.util.List;
import java.util.Map;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.getMaxValue;
import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.OAUTH2_CLIENT_EXISTS;
import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.OAUTH2_CLIENT_NOT_EXISTS; import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.OAUTH2_CLIENT_NOT_EXISTS;
/** /**
@ -20,35 +34,113 @@ import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.OAUTH2_CLI
* @author 芋道源码 * @author 芋道源码
*/ */
@Service @Service
@Validated
@Slf4j
public class OAuth2ClientServiceImpl implements OAuth2ClientService { public class OAuth2ClientServiceImpl implements OAuth2ClientService {
/**
* 定时执行 {@link #schedulePeriodicRefresh()} 的周期
* 因为已经通过 Redis Pub/Sub 机制所以频率不需要高
*/
private static final long SCHEDULER_PERIOD = 5 * 60 * 1000L;
/**
* 客户端缓存
* key客户端编号 {@link OAuth2ClientDO#getClientId()} ()}
*
* 这里声明 volatile 修饰的原因是每次刷新时直接修改指向
*/
@Getter
private volatile Map<String, OAuth2ClientDO> clientCache;
/**
* 缓存角色的最大更新时间用于后续的增量轮询判断是否有更新
*/
@Getter
private volatile Date maxUpdateTime;
@Resource @Resource
private OAuth2ClientMapper oauth2ClientMapper; private OAuth2ClientMapper oauth2ClientMapper;
@Resource
private OAuth2ClientProducer oauth2ClientProducer;
/**
* 初始化 {@link #clientCache} 缓存
*/
@Override
@PostConstruct
public void initLocalCache() {
// 获取客户端列表如果有更新
List<OAuth2ClientDO> tenantList = loadOAuth2ClientIfUpdate(maxUpdateTime);
if (CollUtil.isEmpty(tenantList)) {
return;
}
// 写入缓存
clientCache = convertMap(tenantList, OAuth2ClientDO::getClientId);
maxUpdateTime = getMaxValue(tenantList, OAuth2ClientDO::getUpdateTime);
log.info("[initLocalCache][初始化 OAuth2Client 数量为 {}]", tenantList.size());
}
@Scheduled(fixedDelay = SCHEDULER_PERIOD, initialDelay = SCHEDULER_PERIOD)
public void schedulePeriodicRefresh() {
initLocalCache();
}
/**
* 如果客户端发生变化从数据库中获取最新的全量客户端
* 如果未发生变化则返回空
*
* @param maxUpdateTime 当前客户端的最大更新时间
* @return 客户端列表
*/
private List<OAuth2ClientDO> loadOAuth2ClientIfUpdate(Date maxUpdateTime) {
// 第一步判断是否要更新
if (maxUpdateTime == null) { // 如果更新时间为空说明 DB 一定有新数据
log.info("[loadOAuth2ClientIfUpdate][首次加载全量客户端]");
} else { // 判断数据库中是否有更新的客户端
if (oauth2ClientMapper.selectCountByUpdateTimeGt(maxUpdateTime) == 0) {
return null;
}
log.info("[loadOAuth2ClientIfUpdate][增量加载全量客户端]");
}
// 第二步如果有更新则从数据库加载所有客户端
return oauth2ClientMapper.selectList();
}
@Override @Override
public Long createOAuth2Client(OAuth2ClientCreateReqVO createReqVO) { public Long createOAuth2Client(OAuth2ClientCreateReqVO createReqVO) {
validateClientIdExists(null, createReqVO.getClientId());
// 插入 // 插入
OAuth2ClientDO oAuth2Client = OAuth2ClientConvert.INSTANCE.convert(createReqVO); OAuth2ClientDO oauth2Client = OAuth2ClientConvert.INSTANCE.convert(createReqVO);
oauth2ClientMapper.insert(oAuth2Client); oauth2ClientMapper.insert(oauth2Client);
// 返回 // 发送刷新消息
return oAuth2Client.getId(); oauth2ClientProducer.sendOAuth2ClientRefreshMessage();
return oauth2Client.getId();
} }
@Override @Override
public void updateOAuth2Client(OAuth2ClientUpdateReqVO updateReqVO) { public void updateOAuth2Client(OAuth2ClientUpdateReqVO updateReqVO) {
// 校验存在 // 校验存在
this.validateOAuth2ClientExists(updateReqVO.getId()); validateOAuth2ClientExists(updateReqVO.getId());
// 校验 Client 未被占用
validateClientIdExists(updateReqVO.getId(), updateReqVO.getClientId());
// 更新 // 更新
OAuth2ClientDO updateObj = OAuth2ClientConvert.INSTANCE.convert(updateReqVO); OAuth2ClientDO updateObj = OAuth2ClientConvert.INSTANCE.convert(updateReqVO);
oauth2ClientMapper.updateById(updateObj); oauth2ClientMapper.updateById(updateObj);
// 发送刷新消息
oauth2ClientProducer.sendOAuth2ClientRefreshMessage();
} }
@Override @Override
public void deleteOAuth2Client(Long id) { public void deleteOAuth2Client(Long id) {
// 校验存在 // 校验存在
this.validateOAuth2ClientExists(id); validateOAuth2ClientExists(id);
// 删除 // 删除
oauth2ClientMapper.deleteById(id); oauth2ClientMapper.deleteById(id);
// 发送刷新消息
oauth2ClientProducer.sendOAuth2ClientRefreshMessage();
} }
private void validateOAuth2ClientExists(Long id) { private void validateOAuth2ClientExists(Long id) {
@ -57,6 +149,21 @@ public class OAuth2ClientServiceImpl implements OAuth2ClientService {
} }
} }
@VisibleForTesting
public 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.getClientId().equals(clientId)) {
throw exception(OAUTH2_CLIENT_EXISTS);
}
}
@Override @Override
public OAuth2ClientDO getOAuth2Client(Long id) { public OAuth2ClientDO getOAuth2Client(Long id) {
return oauth2ClientMapper.selectById(id); return oauth2ClientMapper.selectById(id);
@ -68,10 +175,8 @@ public class OAuth2ClientServiceImpl implements OAuth2ClientService {
} }
@Override @Override
public OAuth2ClientDO validOAuthClientFromCache(Long id) { public OAuth2ClientDO validOAuthClientFromCache(String clientId) {
return new OAuth2ClientDO().setId(id) return clientCache.get(clientId);
.setAccessTokenValiditySeconds(60 * 30)
.setRefreshTokenValiditySeconds(60 * 60 * 24 * 30);
} }
} }

View File

@ -24,7 +24,7 @@ public interface OAuth2TokenService {
* @param clientId 客户端编号 * @param clientId 客户端编号
* @return 访问令牌的信息 * @return 访问令牌的信息
*/ */
OAuth2AccessTokenDO createAccessToken(Long userId, Integer userType, Long clientId); OAuth2AccessTokenDO createAccessToken(Long userId, Integer userType, String clientId);
/** /**
* 刷新访问令牌 * 刷新访问令牌
@ -35,7 +35,7 @@ public interface OAuth2TokenService {
* @param clientId 客户端编号 * @param clientId 客户端编号
* @return 访问令牌的信息 * @return 访问令牌的信息
*/ */
OAuth2AccessTokenDO refreshAccessToken(String refreshToken, Long clientId); OAuth2AccessTokenDO refreshAccessToken(String refreshToken, String clientId);
/** /**
* 获得访问令牌 * 获得访问令牌

View File

@ -45,7 +45,7 @@ public class OAuth2TokenServiceImpl implements OAuth2TokenService {
@Override @Override
@Transactional @Transactional
public OAuth2AccessTokenDO createAccessToken(Long userId, Integer userType, Long clientId) { public OAuth2AccessTokenDO createAccessToken(Long userId, Integer userType, String clientId) {
OAuth2ClientDO clientDO = oauth2ClientService.validOAuthClientFromCache(clientId); OAuth2ClientDO clientDO = oauth2ClientService.validOAuthClientFromCache(clientId);
// 创建刷新令牌 // 创建刷新令牌
OAuth2RefreshTokenDO refreshTokenDO = createOAuth2RefreshToken(userId, userType, clientDO); OAuth2RefreshTokenDO refreshTokenDO = createOAuth2RefreshToken(userId, userType, clientDO);
@ -54,7 +54,7 @@ public class OAuth2TokenServiceImpl implements OAuth2TokenService {
} }
@Override @Override
public OAuth2AccessTokenDO refreshAccessToken(String refreshToken, Long clientId) { public OAuth2AccessTokenDO refreshAccessToken(String refreshToken, String clientId) {
// 查询访问令牌 // 查询访问令牌
OAuth2RefreshTokenDO refreshTokenDO = oauth2RefreshTokenMapper.selectByRefreshToken(refreshToken); OAuth2RefreshTokenDO refreshTokenDO = oauth2RefreshTokenMapper.selectByRefreshToken(refreshToken);
if (refreshTokenDO == null) { if (refreshTokenDO == null) {
@ -134,7 +134,7 @@ public class OAuth2TokenServiceImpl implements OAuth2TokenService {
private OAuth2AccessTokenDO createOAuth2AccessToken(OAuth2RefreshTokenDO refreshTokenDO, OAuth2ClientDO clientDO) { private OAuth2AccessTokenDO createOAuth2AccessToken(OAuth2RefreshTokenDO refreshTokenDO, OAuth2ClientDO clientDO) {
OAuth2AccessTokenDO accessTokenDO = new OAuth2AccessTokenDO().setAccessToken(generateAccessToken()) OAuth2AccessTokenDO accessTokenDO = new OAuth2AccessTokenDO().setAccessToken(generateAccessToken())
.setUserId(refreshTokenDO.getUserId()).setUserType(refreshTokenDO.getUserType()).setClientId(clientDO.getId()) .setUserId(refreshTokenDO.getUserId()).setUserType(refreshTokenDO.getUserType()).setClientId(clientDO.getClientId())
.setRefreshToken(refreshTokenDO.getRefreshToken()) .setRefreshToken(refreshTokenDO.getRefreshToken())
.setExpiresTime(DateUtils.addDate(Calendar.SECOND, clientDO.getAccessTokenValiditySeconds())); .setExpiresTime(DateUtils.addDate(Calendar.SECOND, clientDO.getAccessTokenValiditySeconds()));
accessTokenDO.setTenantId(TenantContextHolder.getTenantId()); // 手动设置租户编号避免缓存到 Redis 的时候无对应的租户编号 accessTokenDO.setTenantId(TenantContextHolder.getTenantId()); // 手动设置租户编号避免缓存到 Redis 的时候无对应的租户编号
@ -146,7 +146,7 @@ public class OAuth2TokenServiceImpl implements OAuth2TokenService {
private OAuth2RefreshTokenDO createOAuth2RefreshToken(Long userId, Integer userType, OAuth2ClientDO clientDO) { private OAuth2RefreshTokenDO createOAuth2RefreshToken(Long userId, Integer userType, OAuth2ClientDO clientDO) {
OAuth2RefreshTokenDO refreshToken = new OAuth2RefreshTokenDO().setRefreshToken(generateRefreshToken()) OAuth2RefreshTokenDO refreshToken = new OAuth2RefreshTokenDO().setRefreshToken(generateRefreshToken())
.setUserId(userId).setUserType(userType).setClientId(clientDO.getId()) .setUserId(userId).setUserType(userType).setClientId(clientDO.getClientId())
.setExpiresTime(DateUtils.addDate(Calendar.SECOND, clientDO.getRefreshTokenValiditySeconds())); .setExpiresTime(DateUtils.addDate(Calendar.SECOND, clientDO.getRefreshTokenValiditySeconds()));
oauth2RefreshTokenMapper.insert(refreshToken); oauth2RefreshTokenMapper.insert(refreshToken);
return refreshToken; return refreshToken;

View File

@ -197,7 +197,7 @@ public class AuthServiceImplTest extends BaseDbUnitTest {
// mock 缓存登录用户到 Redis // mock 缓存登录用户到 Redis
OAuth2AccessTokenDO accessTokenDO = randomPojo(OAuth2AccessTokenDO.class, o -> o.setUserId(1L) OAuth2AccessTokenDO accessTokenDO = randomPojo(OAuth2AccessTokenDO.class, o -> o.setUserId(1L)
.setUserType(UserTypeEnum.ADMIN.getValue())); .setUserType(UserTypeEnum.ADMIN.getValue()));
when(oauth2TokenService.createAccessToken(eq(1L), eq(UserTypeEnum.ADMIN.getValue()), eq(1L))) when(oauth2TokenService.createAccessToken(eq(1L), eq(UserTypeEnum.ADMIN.getValue()), eq("default")))
.thenReturn(accessTokenDO); .thenReturn(accessTokenDO);
// 调用, 并断言异常 // 调用, 并断言异常

View File

@ -8,19 +8,23 @@ import cn.iocoder.yudao.module.system.controller.admin.auth.vo.client.OAuth2Clie
import cn.iocoder.yudao.module.system.controller.admin.auth.vo.client.OAuth2ClientUpdateReqVO; import cn.iocoder.yudao.module.system.controller.admin.auth.vo.client.OAuth2ClientUpdateReqVO;
import cn.iocoder.yudao.module.system.dal.dataobject.auth.OAuth2ClientDO; import cn.iocoder.yudao.module.system.dal.dataobject.auth.OAuth2ClientDO;
import cn.iocoder.yudao.module.system.dal.mysql.auth.OAuth2ClientMapper; import cn.iocoder.yudao.module.system.dal.mysql.auth.OAuth2ClientMapper;
import cn.iocoder.yudao.module.system.mq.producer.auth.OAuth2ClientProducer;
import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.context.annotation.Import; import org.springframework.context.annotation.Import;
import javax.annotation.Resource; import javax.annotation.Resource;
import java.util.Map;
import static cn.iocoder.yudao.framework.common.util.object.ObjectUtils.cloneIgnoreId; import static cn.iocoder.yudao.framework.common.util.object.ObjectUtils.cloneIgnoreId;
import static cn.iocoder.yudao.framework.common.util.object.ObjectUtils.max;
import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertPojoEquals; import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertPojoEquals;
import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException; import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException;
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId; import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*;
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.OAUTH2_CLIENT_NOT_EXISTS; import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.OAUTH2_CLIENT_NOT_EXISTS;
import static org.junit.jupiter.api.Assertions.*; import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.verify;
/** /**
* {@link OAuth2ClientServiceImpl} 的单元测试类 * {@link OAuth2ClientServiceImpl} 的单元测试类
@ -31,40 +35,66 @@ import static org.junit.jupiter.api.Assertions.*;
public class OAuth2ClientServiceImplTest extends BaseDbUnitTest { public class OAuth2ClientServiceImplTest extends BaseDbUnitTest {
@Resource @Resource
private OAuth2ClientServiceImpl oAuth2ClientService; private OAuth2ClientServiceImpl oauth2ClientService;
@Resource @Resource
private OAuth2ClientMapper oAuth2ClientMapper; private OAuth2ClientMapper oauth2ClientMapper;
@MockBean
private OAuth2ClientProducer oauth2ClientProducer;
@Test
public void testInitLocalCache() {
// mock 数据
OAuth2ClientDO clientDO1 = randomPojo(OAuth2ClientDO.class);
oauth2ClientMapper.insert(clientDO1);
OAuth2ClientDO clientDO2 = randomPojo(OAuth2ClientDO.class);
oauth2ClientMapper.insert(clientDO2);
// 调用
oauth2ClientService.initLocalCache();
// 断言 clientCache 缓存
Map<String, OAuth2ClientDO> clientCache = oauth2ClientService.getClientCache();
assertEquals(2, clientCache.size());
assertPojoEquals(clientDO1, clientCache.get(clientDO1.getClientId()));
assertPojoEquals(clientDO2, clientCache.get(clientDO2.getClientId()));
// 断言 maxUpdateTime 缓存
assertEquals(max(clientDO1.getUpdateTime(), clientDO2.getUpdateTime()), oauth2ClientService.getMaxUpdateTime());
}
@Test @Test
public void testCreateOAuth2Client_success() { public void testCreateOAuth2Client_success() {
// 准备参数 // 准备参数
OAuth2ClientCreateReqVO reqVO = randomPojo(OAuth2ClientCreateReqVO.class); OAuth2ClientCreateReqVO reqVO = randomPojo(OAuth2ClientCreateReqVO.class,
o -> o.setLogo(randomString()));
// 调用 // 调用
Long oauth2ClientId = oAuth2ClientService.createOAuth2Client(reqVO); Long oauth2ClientId = oauth2ClientService.createOAuth2Client(reqVO);
// 断言 // 断言
assertNotNull(oauth2ClientId); assertNotNull(oauth2ClientId);
// 校验记录的属性是否正确 // 校验记录的属性是否正确
OAuth2ClientDO oAuth2Client = oAuth2ClientMapper.selectById(oauth2ClientId); OAuth2ClientDO oAuth2Client = oauth2ClientMapper.selectById(oauth2ClientId);
assertPojoEquals(reqVO, oAuth2Client); assertPojoEquals(reqVO, oAuth2Client);
verify(oauth2ClientProducer).sendOAuth2ClientRefreshMessage();
} }
@Test @Test
public void testUpdateOAuth2Client_success() { public void testUpdateOAuth2Client_success() {
// mock 数据 // mock 数据
OAuth2ClientDO dbOAuth2Client = randomPojo(OAuth2ClientDO.class); OAuth2ClientDO dbOAuth2Client = randomPojo(OAuth2ClientDO.class);
oAuth2ClientMapper.insert(dbOAuth2Client);// @Sql: 先插入出一条存在的数据 oauth2ClientMapper.insert(dbOAuth2Client);// @Sql: 先插入出一条存在的数据
// 准备参数 // 准备参数
OAuth2ClientUpdateReqVO reqVO = randomPojo(OAuth2ClientUpdateReqVO.class, o -> { OAuth2ClientUpdateReqVO reqVO = randomPojo(OAuth2ClientUpdateReqVO.class, o -> {
o.setId(dbOAuth2Client.getId()); // 设置更新的 ID o.setId(dbOAuth2Client.getId()); // 设置更新的 ID
o.setLogo(randomString());
}); });
// 调用 // 调用
oAuth2ClientService.updateOAuth2Client(reqVO); oauth2ClientService.updateOAuth2Client(reqVO);
// 校验是否更新正确 // 校验是否更新正确
OAuth2ClientDO oAuth2Client = oAuth2ClientMapper.selectById(reqVO.getId()); // 获取最新的 OAuth2ClientDO oAuth2Client = oauth2ClientMapper.selectById(reqVO.getId()); // 获取最新的
assertPojoEquals(reqVO, oAuth2Client); assertPojoEquals(reqVO, oAuth2Client);
verify(oauth2ClientProducer).sendOAuth2ClientRefreshMessage();
} }
@Test @Test
@ -73,21 +103,22 @@ public class OAuth2ClientServiceImplTest extends BaseDbUnitTest {
OAuth2ClientUpdateReqVO reqVO = randomPojo(OAuth2ClientUpdateReqVO.class); OAuth2ClientUpdateReqVO reqVO = randomPojo(OAuth2ClientUpdateReqVO.class);
// 调用, 并断言异常 // 调用, 并断言异常
assertServiceException(() -> oAuth2ClientService.updateOAuth2Client(reqVO), OAUTH2_CLIENT_NOT_EXISTS); assertServiceException(() -> oauth2ClientService.updateOAuth2Client(reqVO), OAUTH2_CLIENT_NOT_EXISTS);
} }
@Test @Test
public void testDeleteOAuth2Client_success() { public void testDeleteOAuth2Client_success() {
// mock 数据 // mock 数据
OAuth2ClientDO dbOAuth2Client = randomPojo(OAuth2ClientDO.class); OAuth2ClientDO dbOAuth2Client = randomPojo(OAuth2ClientDO.class);
oAuth2ClientMapper.insert(dbOAuth2Client);// @Sql: 先插入出一条存在的数据 oauth2ClientMapper.insert(dbOAuth2Client);// @Sql: 先插入出一条存在的数据
// 准备参数 // 准备参数
Long id = dbOAuth2Client.getId(); Long id = dbOAuth2Client.getId();
// 调用 // 调用
oAuth2ClientService.deleteOAuth2Client(id); oauth2ClientService.deleteOAuth2Client(id);
// 校验数据不存在了 // 校验数据不存在了
assertNull(oAuth2ClientMapper.selectById(id)); assertNull(oauth2ClientMapper.selectById(id));
verify(oauth2ClientProducer).sendOAuth2ClientRefreshMessage();
} }
@Test @Test
@ -96,7 +127,7 @@ public class OAuth2ClientServiceImplTest extends BaseDbUnitTest {
Long id = randomLongId(); Long id = randomLongId();
// 调用, 并断言异常 // 调用, 并断言异常
assertServiceException(() -> oAuth2ClientService.deleteOAuth2Client(id), OAUTH2_CLIENT_NOT_EXISTS); assertServiceException(() -> oauth2ClientService.deleteOAuth2Client(id), OAUTH2_CLIENT_NOT_EXISTS);
} }
@Test @Test
@ -107,18 +138,18 @@ public class OAuth2ClientServiceImplTest extends BaseDbUnitTest {
o.setName("潜龙"); o.setName("潜龙");
o.setStatus(CommonStatusEnum.ENABLE.getStatus()); o.setStatus(CommonStatusEnum.ENABLE.getStatus());
}); });
oAuth2ClientMapper.insert(dbOAuth2Client); oauth2ClientMapper.insert(dbOAuth2Client);
// 测试 name 不匹配 // 测试 name 不匹配
oAuth2ClientMapper.insert(cloneIgnoreId(dbOAuth2Client, o -> o.setName("凤凰"))); oauth2ClientMapper.insert(cloneIgnoreId(dbOAuth2Client, o -> o.setName("凤凰")));
// 测试 status 不匹配 // 测试 status 不匹配
oAuth2ClientMapper.insert(cloneIgnoreId(dbOAuth2Client, o -> o.setStatus(CommonStatusEnum.ENABLE.getStatus()))); oauth2ClientMapper.insert(cloneIgnoreId(dbOAuth2Client, o -> o.setStatus(CommonStatusEnum.ENABLE.getStatus())));
// 准备参数 // 准备参数
OAuth2ClientPageReqVO reqVO = new OAuth2ClientPageReqVO(); OAuth2ClientPageReqVO reqVO = new OAuth2ClientPageReqVO();
reqVO.setName("long"); reqVO.setName("long");
reqVO.setStatus(CommonStatusEnum.ENABLE.getStatus()); reqVO.setStatus(CommonStatusEnum.ENABLE.getStatus());
// 调用 // 调用
PageResult<OAuth2ClientDO> pageResult = oAuth2ClientService.getOAuth2ClientPage(reqVO); PageResult<OAuth2ClientDO> pageResult = oauth2ClientService.getOAuth2ClientPage(reqVO);
// 断言 // 断言
assertEquals(1, pageResult.getTotal()); assertEquals(1, pageResult.getTotal());
assertEquals(1, pageResult.getList().size()); assertEquals(1, pageResult.getList().size());

View File

@ -473,6 +473,7 @@ CREATE TABLE IF NOT EXISTS "system_sensitive_word" (
CREATE TABLE IF NOT EXISTS "system_oauth2_client" ( CREATE TABLE IF NOT EXISTS "system_oauth2_client" (
"id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
"client_id" varchar NOT NULL,
"secret" varchar NOT NULL, "secret" varchar NOT NULL,
"name" varchar NOT NULL, "name" varchar NOT NULL,
"logo" varchar NOT NULL, "logo" varchar NOT NULL,
@ -481,6 +482,12 @@ CREATE TABLE IF NOT EXISTS "system_oauth2_client" (
"access_token_validity_seconds" int NOT NULL, "access_token_validity_seconds" int NOT NULL,
"refresh_token_validity_seconds" int NOT NULL, "refresh_token_validity_seconds" int NOT NULL,
"redirect_uris" varchar NOT NULL, "redirect_uris" varchar NOT NULL,
"auto_approve" bit NOT NULL DEFAULT FALSE,
"authorized_grant_types" varchar NOT NULL,
"scopes" varchar NOT NULL DEFAULT '',
"authorities" varchar NOT NULL DEFAULT '',
"resource_ids" varchar NOT NULL DEFAULT '',
"additional_information" varchar NOT NULL DEFAULT '',
"creator" varchar DEFAULT '', "creator" varchar DEFAULT '',
"create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updater" varchar DEFAULT '', "updater" varchar DEFAULT '',

View File

@ -499,7 +499,7 @@ export const selectComponents = [
__slot__: { __slot__: {
'list-type': true 'list-type': true
}, },
action: 'https://jsonplaceholder.typicode.com/posts/', action: process.env.VUE_APP_BASE_API + "/admin-api/infra/file/upload", // 请求地址
disabled: false, disabled: false,
accept: '', accept: '',
name: 'file', name: 'file',

View File

@ -23,6 +23,7 @@ export const DICT_TYPE = {
SYSTEM_SMS_SEND_STATUS: 'system_sms_send_status', SYSTEM_SMS_SEND_STATUS: 'system_sms_send_status',
SYSTEM_SMS_RECEIVE_STATUS: 'system_sms_receive_status', SYSTEM_SMS_RECEIVE_STATUS: 'system_sms_receive_status',
SYSTEM_ERROR_CODE_TYPE: 'system_error_code_type', SYSTEM_ERROR_CODE_TYPE: 'system_error_code_type',
SYSTEM_OAUTH2_GRANT_TYPE: 'system_oauth2_grant_type',
// ========== INFRA 模块 ========== // ========== INFRA 模块 ==========
INFRA_BOOLEAN_STRING: 'infra_boolean_string', INFRA_BOOLEAN_STRING: 'infra_boolean_string',

View File

@ -29,19 +29,32 @@
<!-- 列表 --> <!-- 列表 -->
<el-table v-loading="loading" :data="list"> <el-table v-loading="loading" :data="list">
<el-table-column label="客户端编号" align="center" prop="id" /> <el-table-column label="客户端编号" align="center" prop="clientId" />
<el-table-column label="客户端密钥" align="center" prop="secret" /> <el-table-column label="客户端密钥" align="center" prop="secret" />
<el-table-column label="应用名" align="center" prop="name" /> <el-table-column label="应用名" align="center" prop="name" />
<el-table-column label="应用图标" align="center" prop="logo" /> <el-table-column label="应用图标" align="center" prop="logo">
<el-table-column label="应用描述" align="center" prop="description" /> <template slot-scope="scope">
<img width="40px" height="40px" :src="scope.row.logo">
</template>
</el-table-column>
<el-table-column label="状态" align="center" prop="status"> <el-table-column label="状态" align="center" prop="status">
<template slot-scope="scope"> <template slot-scope="scope">
<dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" /> <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="访问令牌的有效期" align="center" prop="accessTokenValiditySeconds" /> <el-table-column label="访问令牌的有效期" align="center" prop="accessTokenValiditySeconds">
<el-table-column label="刷新令牌的有效期" align="center" prop="refreshTokenValiditySeconds" /> <template slot-scope="scope">{{ scope.row.accessTokenValiditySeconds }} </template>
<el-table-column label="可重定向的 URI 地址" align="center" prop="redirectUris" /> </el-table-column>
<el-table-column label="刷新令牌的有效期" align="center" prop="refreshTokenValiditySeconds">
<template slot-scope="scope">{{ scope.row.refreshTokenValiditySeconds }} </template>
</el-table-column>
<el-table-column label="授权类型" align="center" prop="authorizedGrantTypes">
<template slot-scope="scope">
<el-tag :disable-transitions="true" v-for="(authorizedGrantType, index) in scope.row.authorizedGrantTypes" :index="index">
{{ authorizedGrantType }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="创建时间" align="center" prop="createTime" width="180"> <el-table-column label="创建时间" align="center" prop="createTime" width="180">
<template slot-scope="scope"> <template slot-scope="scope">
<span>{{ parseTime(scope.row.createTime) }}</span> <span>{{ parseTime(scope.row.createTime) }}</span>
@ -63,6 +76,9 @@
<!-- 对话框(添加 / 修改) --> <!-- 对话框(添加 / 修改) -->
<el-dialog :title="title" :visible.sync="open" width="700px" append-to-body> <el-dialog :title="title" :visible.sync="open" width="700px" append-to-body>
<el-form ref="form" :model="form" :rules="rules" label-width="160px"> <el-form ref="form" :model="form" :rules="rules" label-width="160px">
<el-form-item label="客户端编号" prop="secret">
<el-input v-model="form.clientId" placeholder="请输入客户端编号" />
</el-form-item>
<el-form-item label="客户端密钥" prop="secret"> <el-form-item label="客户端密钥" prop="secret">
<el-input v-model="form.secret" placeholder="请输入客户端密钥" /> <el-input v-model="form.secret" placeholder="请输入客户端密钥" />
</el-form-item> </el-form-item>
@ -70,8 +86,7 @@
<el-input v-model="form.name" placeholder="请输入应用名" /> <el-input v-model="form.name" placeholder="请输入应用名" />
</el-form-item> </el-form-item>
<el-form-item label="应用图标"> <el-form-item label="应用图标">
<!-- <imageUpload v-model="form.logo" :limit="1"/>--> <imageUpload v-model="form.logo" :limit="1"/>
<file-upload v-model="form.logo" :limit="1"/>
</el-form-item> </el-form-item>
<el-form-item label="应用描述"> <el-form-item label="应用描述">
<el-input type="textarea" v-model="form.description" placeholder="请输入应用名" /> <el-input type="textarea" v-model="form.description" placeholder="请输入应用名" />
@ -89,7 +104,39 @@
<el-input-number v-model="form.refreshTokenValiditySeconds" placeholder="单位:秒" /> <el-input-number v-model="form.refreshTokenValiditySeconds" placeholder="单位:秒" />
</el-form-item> </el-form-item>
<el-form-item label="可重定向的 URI 地址" prop="redirectUris"> <el-form-item label="可重定向的 URI 地址" prop="redirectUris">
<el-input v-model="form.redirectUris" placeholder="请输入可重定向的 URI 地址" /> <el-select v-model="form.redirectUris" multiple filterable allow-create placeholder="请输入可重定向的 URI 地址" style="width: 500px" >
<el-option v-for="redirectUri in form.redirectUris" :key="redirectUri" :label="redirectUri" :value="redirectUri"/>
</el-select>
</el-form-item>
<el-form-item label="是否自动授权" prop="autoApprove">
<el-radio-group v-model="form.autoApprove">
<el-radio :key="true" :label="true">自动登录</el-radio>
<el-radio :key="false" :label="false">手动登录</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="授权类型" prop="authorizedGrantTypes">
<el-select v-model="form.authorizedGrantTypes" multiple filterable placeholder="请输入授权类型" style="width: 500px" >
<el-option v-for="dict in this.getDictDatas(DICT_TYPE.SYSTEM_OAUTH2_GRANT_TYPE)"
:key="dict.value" :label="dict.label" :value="dict.value"/>
</el-select>
</el-form-item>
<el-form-item label="授权范围" prop="scopes">
<el-select v-model="form.scopes" multiple filterable allow-create placeholder="请输入授权范围" style="width: 500px" >
<el-option v-for="scope in form.scopes" :key="scope" :label="scope" :value="scope"/>
</el-select>
</el-form-item>
<el-form-item label="权限" prop="authorities">
<el-select v-model="form.authorities" multiple filterable allow-create placeholder="请输入权限" style="width: 500px" >
<el-option v-for="authority in form.authorities" :key="authority" :label="authority" :value="authority"/>
</el-select>
</el-form-item>
<el-form-item label="资源" prop="resourceIds">
<el-select v-model="form.resourceIds" multiple filterable allow-create placeholder="请输入资源" style="width: 500px" >
<el-option v-for="resourceId in form.resourceIds" :key="resourceId" :label="resourceId" :value="resourceId"/>
</el-select>
</el-form-item>
<el-form-item label="附加信息" prop="additionalInformation">
<el-input type="textarea" v-model="form.additionalInformation" placeholder="请输入附加信息JSON 格式数据" />
</el-form-item> </el-form-item>
</el-form> </el-form>
<div slot="footer" class="dialog-footer"> <div slot="footer" class="dialog-footer">
@ -141,6 +188,7 @@ export default {
form: {}, form: {},
// //
rules: { rules: {
clientId: [{ required: true, message: "客户端编号不能为空", trigger: "blur" }],
secret: [{ required: true, message: "客户端密钥不能为空", trigger: "blur" }], secret: [{ required: true, message: "客户端密钥不能为空", trigger: "blur" }],
name: [{ required: true, message: "应用名不能为空", trigger: "blur" }], name: [{ required: true, message: "应用名不能为空", trigger: "blur" }],
logo: [{ required: true, message: "应用图标不能为空", trigger: "blur" }], logo: [{ required: true, message: "应用图标不能为空", trigger: "blur" }],
@ -148,6 +196,8 @@ export default {
accessTokenValiditySeconds: [{ required: true, message: "访问令牌的有效期不能为空", trigger: "blur" }], accessTokenValiditySeconds: [{ required: true, message: "访问令牌的有效期不能为空", trigger: "blur" }],
refreshTokenValiditySeconds: [{ required: true, message: "刷新令牌的有效期不能为空", trigger: "blur" }], refreshTokenValiditySeconds: [{ required: true, message: "刷新令牌的有效期不能为空", trigger: "blur" }],
redirectUris: [{ required: true, message: "可重定向的 URI 地址不能为空", trigger: "blur" }], redirectUris: [{ required: true, message: "可重定向的 URI 地址不能为空", trigger: "blur" }],
autoApprove: [{ required: true, message: "是否自动授权不能为空", trigger: "blur" }],
authorizedGrantTypes: [{ required: true, message: "授权类型不能为空", trigger: "blur" }],
} }
}; };
}, },
@ -176,14 +226,21 @@ export default {
reset() { reset() {
this.form = { this.form = {
id: undefined, id: undefined,
clientId: undefined,
secret: undefined, secret: undefined,
name: undefined, name: undefined,
logo: undefined, logo: undefined,
description: undefined, description: undefined,
status: CommonStatusEnum.ENABLE, status: CommonStatusEnum.ENABLE,
accessTokenValiditySeconds: undefined, accessTokenValiditySeconds: 30 * 60,
refreshTokenValiditySeconds: undefined, refreshTokenValiditySeconds: 30 * 24 * 60,
redirectUris: undefined, redirectUris: [],
autoApprove: true,
authorizedGrantTypes: [],
scopes: [],
authorities: [],
resourceIds: [],
additionalInformation: undefined,
}; };
this.resetForm("form"); this.resetForm("form");
}, },
@ -239,7 +296,7 @@ export default {
/** 删除按钮操作 */ /** 删除按钮操作 */
handleDelete(row) { handleDelete(row) {
const id = row.id; const id = row.id;
this.$modal.confirm('是否确认删除 OAuth2 客户端编号为"' + id + '"的数据项?').then(function() { this.$modal.confirm('是否确认删除客户端编号为"' + row.clientId + '"的数据项?').then(function() {
return deleteOAuth2Client(id); return deleteOAuth2Client(id);
}).then(() => { }).then(() => {
this.getList(); this.getList();