mirror of
https://gitee.com/hhyykk/ipms-sjy.git
synced 2025-07-24 07:55:06 +08:00
完成 OAuth2 的客户端模块
This commit is contained in:
@ -40,7 +40,7 @@ public class OAuth2TokenApiImpl implements OAuth2TokenApi {
|
||||
}
|
||||
|
||||
@Override
|
||||
public OAuth2AccessTokenRespDTO refreshAccessToken(String refreshToken, Long clientId) {
|
||||
public OAuth2AccessTokenRespDTO refreshAccessToken(String refreshToken, String clientId) {
|
||||
OAuth2AccessTokenDO accessTokenDO = oauth2TokenService.refreshAccessToken(refreshToken, clientId);
|
||||
return OAuth2TokenConvert.INSTANCE.convert2(accessTokenDO);
|
||||
}
|
||||
|
@ -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": "{}"
|
||||
}
|
@ -1,8 +1,13 @@
|
||||
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 lombok.Data;
|
||||
import org.hibernate.validator.constraints.URL;
|
||||
|
||||
import javax.validation.constraints.AssertTrue;
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import java.util.List;
|
||||
|
||||
@ -15,7 +20,7 @@ public class OAuth2ClientBaseVO {
|
||||
|
||||
@ApiModelProperty(value = "客户端编号", required = true)
|
||||
@NotNull(message = "客户端编号不能为空")
|
||||
private Long id;
|
||||
private String clientId;
|
||||
|
||||
@ApiModelProperty(value = "客户端密钥", required = true)
|
||||
@NotNull(message = "客户端密钥不能为空")
|
||||
@ -27,6 +32,7 @@ public class OAuth2ClientBaseVO {
|
||||
|
||||
@ApiModelProperty(value = "应用图标", required = true)
|
||||
@NotNull(message = "应用图标不能为空")
|
||||
@URL(message = "应用图标的地址不正确")
|
||||
private String logo;
|
||||
|
||||
@ApiModelProperty(value = "应用描述")
|
||||
@ -46,6 +52,32 @@ public class OAuth2ClientBaseVO {
|
||||
|
||||
@ApiModelProperty(value = "可重定向的 URI 地址", required = true)
|
||||
@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);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -14,6 +14,9 @@ import java.util.Date;
|
||||
@ToString(callSuper = true)
|
||||
public class OAuth2ClientRespVO extends OAuth2ClientBaseVO {
|
||||
|
||||
@ApiModelProperty(value = "编号", required = true)
|
||||
private Long id;
|
||||
|
||||
@ApiModelProperty(value = "创建时间", required = true)
|
||||
private Date createTime;
|
||||
|
||||
|
@ -1,14 +1,21 @@
|
||||
package cn.iocoder.yudao.module.system.controller.admin.auth.vo.client;
|
||||
|
||||
import io.swagger.annotations.ApiModel;
|
||||
import io.swagger.annotations.ApiModelProperty;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.ToString;
|
||||
|
||||
import javax.validation.constraints.NotNull;
|
||||
|
||||
@ApiModel("管理后台 - OAuth2 客户端更新 Request VO")
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@ToString(callSuper = true)
|
||||
public class OAuth2ClientUpdateReqVO extends OAuth2ClientBaseVO {
|
||||
|
||||
@ApiModelProperty(value = "编号", required = true)
|
||||
@NotNull(message = "编号不能为空")
|
||||
private Long id;
|
||||
|
||||
}
|
||||
|
@ -18,6 +18,6 @@ public class OAuth2AccessTokenPageReqVO extends PageParam {
|
||||
private Integer userType;
|
||||
|
||||
@ApiModelProperty(value = "客户端编号", required = true, example = "2")
|
||||
private Long clientId;
|
||||
private String clientId;
|
||||
|
||||
}
|
||||
|
@ -30,7 +30,7 @@ public class OAuth2AccessTokenRespVO {
|
||||
private Integer userType;
|
||||
|
||||
@ApiModelProperty(value = "客户端编号", required = true, example = "2")
|
||||
private Long clientId;
|
||||
private String clientId;
|
||||
|
||||
@ApiModelProperty(value = "创建时间", required = true)
|
||||
private Date createTime;
|
||||
|
@ -52,7 +52,7 @@ public class OAuth2AccessTokenDO extends TenantBaseDO {
|
||||
*
|
||||
* 关联 {@link OAuth2ClientDO#getId()}
|
||||
*/
|
||||
private Long clientId;
|
||||
private String clientId;
|
||||
/**
|
||||
* 过期时间
|
||||
*/
|
||||
|
@ -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.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.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* OAuth2 客户端 DO
|
||||
*
|
||||
* 如下字段,考虑到使用相对不是很高频,主要是一些开关,暂时不支持:
|
||||
* authorized_grant_types、authorities、additional_information、autoapprove、resource_ids、scope
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@TableName(value = "system_oauth2_client", autoResultMap = true)
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Accessors(chain = true)
|
||||
public class OAuth2ClientDO extends BaseDO {
|
||||
|
||||
/**
|
||||
* 客户端编号
|
||||
* 编号,数据库自增
|
||||
*
|
||||
* 由于 SQL Server 在存储 String 主键有点问题,所以暂时使用 Long 类型
|
||||
*/
|
||||
@TableId(type = IdType.INPUT)
|
||||
@TableId
|
||||
private Long id;
|
||||
/**
|
||||
* 客户端编号
|
||||
*/
|
||||
private String clientId;
|
||||
/**
|
||||
* 客户端密钥
|
||||
*/
|
||||
@ -69,5 +68,35 @@ public class OAuth2ClientDO extends BaseDO {
|
||||
*/
|
||||
@TableField(typeHandler = JacksonTypeHandler.class)
|
||||
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;
|
||||
|
||||
}
|
||||
|
@ -5,7 +5,6 @@ import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
@ -17,7 +16,6 @@ import java.util.Date;
|
||||
@TableName("system_oauth2_code")
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Accessors(chain = true)
|
||||
public class OAuth2CodeDO extends BaseDO {
|
||||
|
||||
/**
|
||||
@ -43,7 +41,7 @@ public class OAuth2CodeDO extends BaseDO {
|
||||
*
|
||||
* 关联 {@link OAuth2ClientDO#getId()}
|
||||
*/
|
||||
private Long clientId;
|
||||
private String clientId;
|
||||
/**
|
||||
* 刷新令牌
|
||||
*
|
||||
|
@ -43,7 +43,7 @@ public class OAuth2RefreshTokenDO extends BaseDO {
|
||||
*
|
||||
* 关联 {@link OAuth2ClientDO#getId()}
|
||||
*/
|
||||
private Long clientId;
|
||||
private String clientId;
|
||||
/**
|
||||
* 过期时间
|
||||
*/
|
||||
|
@ -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.dal.dataobject.auth.OAuth2ClientDO;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* OAuth2 客户端 Mapper
|
||||
@ -22,4 +25,11 @@ public interface OAuth2ClientMapper extends BaseMapperX<OAuth2ClientDO> {
|
||||
.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);
|
||||
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
@ -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";
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
@ -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.dal.dataobject.auth.OAuth2AccessTokenDO;
|
||||
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.LoginResultEnum;
|
||||
import cn.iocoder.yudao.module.system.enums.sms.SmsSceneEnum;
|
||||
@ -197,7 +197,7 @@ public class AdminAuthServiceImpl implements AdminAuthService {
|
||||
|
||||
@Override
|
||||
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);
|
||||
}
|
||||
|
||||
@ -206,7 +206,7 @@ public class AdminAuthServiceImpl implements AdminAuthService {
|
||||
createLoginLog(userId, username, logType, LoginResultEnum.SUCCESS);
|
||||
// 创建访问令牌
|
||||
OAuth2AccessTokenDO accessTokenDO = oauth2TokenService.createAccessToken(userId, getUserType().getValue(),
|
||||
OAuth2ClientIdEnum.DEFAULT.getId());
|
||||
OAuth2ClientConstants.CLIENT_ID_DEFAULT);
|
||||
// 构建返回结果
|
||||
return AuthConvert.INSTANCE.convert(accessTokenDO);
|
||||
}
|
||||
|
@ -18,7 +18,12 @@ import javax.validation.Valid;
|
||||
public interface OAuth2ClientService {
|
||||
|
||||
/**
|
||||
* 创建OAuth2 客户端
|
||||
* 初始化 OAuth2Client 的本地缓存
|
||||
*/
|
||||
void initLocalCache();
|
||||
|
||||
/**
|
||||
* 创建 OAuth2 客户端
|
||||
*
|
||||
* @param createReqVO 创建信息
|
||||
* @return 编号
|
||||
@ -26,21 +31,21 @@ public interface OAuth2ClientService {
|
||||
Long createOAuth2Client(@Valid OAuth2ClientCreateReqVO createReqVO);
|
||||
|
||||
/**
|
||||
* 更新OAuth2 客户端
|
||||
* 更新 OAuth2 客户端
|
||||
*
|
||||
* @param updateReqVO 更新信息
|
||||
*/
|
||||
void updateOAuth2Client(@Valid OAuth2ClientUpdateReqVO updateReqVO);
|
||||
|
||||
/**
|
||||
* 删除OAuth2 客户端
|
||||
* 删除 OAuth2 客户端
|
||||
*
|
||||
* @param id 编号
|
||||
*/
|
||||
void deleteOAuth2Client(Long id);
|
||||
|
||||
/**
|
||||
* 获得OAuth2 客户端
|
||||
* 获得 OAuth2 客户端
|
||||
*
|
||||
* @param id 编号
|
||||
* @return OAuth2 客户端
|
||||
@ -48,7 +53,7 @@ public interface OAuth2ClientService {
|
||||
OAuth2ClientDO getOAuth2Client(Long id);
|
||||
|
||||
/**
|
||||
* 获得OAuth2 客户端分页
|
||||
* 获得 OAuth2 客户端分页
|
||||
*
|
||||
* @param pageReqVO 分页查询
|
||||
* @return OAuth2 客户端分页
|
||||
@ -58,9 +63,9 @@ public interface OAuth2ClientService {
|
||||
/**
|
||||
* 从缓存中,校验客户端是否合法
|
||||
*
|
||||
* @param id 客户端编号
|
||||
* @param clientId 客户端编号
|
||||
* @return 客户端
|
||||
*/
|
||||
OAuth2ClientDO validOAuthClientFromCache(Long id);
|
||||
OAuth2ClientDO validOAuthClientFromCache(String clientId);
|
||||
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
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.module.system.controller.admin.auth.vo.client.OAuth2ClientCreateReqVO;
|
||||
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.dal.dataobject.auth.OAuth2ClientDO;
|
||||
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.validation.annotation.Validated;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
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.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;
|
||||
|
||||
/**
|
||||
@ -20,35 +34,113 @@ import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.OAUTH2_CLI
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Service
|
||||
@Validated
|
||||
@Slf4j
|
||||
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
|
||||
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
|
||||
public Long createOAuth2Client(OAuth2ClientCreateReqVO createReqVO) {
|
||||
validateClientIdExists(null, createReqVO.getClientId());
|
||||
// 插入
|
||||
OAuth2ClientDO oAuth2Client = OAuth2ClientConvert.INSTANCE.convert(createReqVO);
|
||||
oauth2ClientMapper.insert(oAuth2Client);
|
||||
// 返回
|
||||
return oAuth2Client.getId();
|
||||
OAuth2ClientDO oauth2Client = OAuth2ClientConvert.INSTANCE.convert(createReqVO);
|
||||
oauth2ClientMapper.insert(oauth2Client);
|
||||
// 发送刷新消息
|
||||
oauth2ClientProducer.sendOAuth2ClientRefreshMessage();
|
||||
return oauth2Client.getId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateOAuth2Client(OAuth2ClientUpdateReqVO updateReqVO) {
|
||||
// 校验存在
|
||||
this.validateOAuth2ClientExists(updateReqVO.getId());
|
||||
validateOAuth2ClientExists(updateReqVO.getId());
|
||||
// 校验 Client 未被占用
|
||||
validateClientIdExists(updateReqVO.getId(), updateReqVO.getClientId());
|
||||
|
||||
// 更新
|
||||
OAuth2ClientDO updateObj = OAuth2ClientConvert.INSTANCE.convert(updateReqVO);
|
||||
oauth2ClientMapper.updateById(updateObj);
|
||||
// 发送刷新消息
|
||||
oauth2ClientProducer.sendOAuth2ClientRefreshMessage();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteOAuth2Client(Long id) {
|
||||
// 校验存在
|
||||
this.validateOAuth2ClientExists(id);
|
||||
validateOAuth2ClientExists(id);
|
||||
// 删除
|
||||
oauth2ClientMapper.deleteById(id);
|
||||
// 发送刷新消息
|
||||
oauth2ClientProducer.sendOAuth2ClientRefreshMessage();
|
||||
}
|
||||
|
||||
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
|
||||
public OAuth2ClientDO getOAuth2Client(Long id) {
|
||||
return oauth2ClientMapper.selectById(id);
|
||||
@ -68,10 +175,8 @@ public class OAuth2ClientServiceImpl implements OAuth2ClientService {
|
||||
}
|
||||
|
||||
@Override
|
||||
public OAuth2ClientDO validOAuthClientFromCache(Long id) {
|
||||
return new OAuth2ClientDO().setId(id)
|
||||
.setAccessTokenValiditySeconds(60 * 30)
|
||||
.setRefreshTokenValiditySeconds(60 * 60 * 24 * 30);
|
||||
public OAuth2ClientDO validOAuthClientFromCache(String clientId) {
|
||||
return clientCache.get(clientId);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -24,7 +24,7 @@ public interface OAuth2TokenService {
|
||||
* @param clientId 客户端编号
|
||||
* @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 客户端编号
|
||||
* @return 访问令牌的信息
|
||||
*/
|
||||
OAuth2AccessTokenDO refreshAccessToken(String refreshToken, Long clientId);
|
||||
OAuth2AccessTokenDO refreshAccessToken(String refreshToken, String clientId);
|
||||
|
||||
/**
|
||||
* 获得访问令牌
|
||||
|
@ -45,7 +45,7 @@ public class OAuth2TokenServiceImpl implements OAuth2TokenService {
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public OAuth2AccessTokenDO createAccessToken(Long userId, Integer userType, Long clientId) {
|
||||
public OAuth2AccessTokenDO createAccessToken(Long userId, Integer userType, String clientId) {
|
||||
OAuth2ClientDO clientDO = oauth2ClientService.validOAuthClientFromCache(clientId);
|
||||
// 创建刷新令牌
|
||||
OAuth2RefreshTokenDO refreshTokenDO = createOAuth2RefreshToken(userId, userType, clientDO);
|
||||
@ -54,7 +54,7 @@ public class OAuth2TokenServiceImpl implements OAuth2TokenService {
|
||||
}
|
||||
|
||||
@Override
|
||||
public OAuth2AccessTokenDO refreshAccessToken(String refreshToken, Long clientId) {
|
||||
public OAuth2AccessTokenDO refreshAccessToken(String refreshToken, String clientId) {
|
||||
// 查询访问令牌
|
||||
OAuth2RefreshTokenDO refreshTokenDO = oauth2RefreshTokenMapper.selectByRefreshToken(refreshToken);
|
||||
if (refreshTokenDO == null) {
|
||||
@ -134,7 +134,7 @@ public class OAuth2TokenServiceImpl implements OAuth2TokenService {
|
||||
|
||||
private OAuth2AccessTokenDO createOAuth2AccessToken(OAuth2RefreshTokenDO refreshTokenDO, OAuth2ClientDO clientDO) {
|
||||
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())
|
||||
.setExpiresTime(DateUtils.addDate(Calendar.SECOND, clientDO.getAccessTokenValiditySeconds()));
|
||||
accessTokenDO.setTenantId(TenantContextHolder.getTenantId()); // 手动设置租户编号,避免缓存到 Redis 的时候,无对应的租户编号
|
||||
@ -146,7 +146,7 @@ public class OAuth2TokenServiceImpl implements OAuth2TokenService {
|
||||
|
||||
private OAuth2RefreshTokenDO createOAuth2RefreshToken(Long userId, Integer userType, OAuth2ClientDO clientDO) {
|
||||
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()));
|
||||
oauth2RefreshTokenMapper.insert(refreshToken);
|
||||
return refreshToken;
|
||||
|
@ -197,7 +197,7 @@ public class AuthServiceImplTest extends BaseDbUnitTest {
|
||||
// mock 缓存登录用户到 Redis
|
||||
OAuth2AccessTokenDO accessTokenDO = randomPojo(OAuth2AccessTokenDO.class, o -> o.setUserId(1L)
|
||||
.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);
|
||||
|
||||
// 调用, 并断言异常
|
||||
|
@ -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.dal.dataobject.auth.OAuth2ClientDO;
|
||||
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.Test;
|
||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||
import org.springframework.context.annotation.Import;
|
||||
|
||||
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.max;
|
||||
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.RandomUtils.randomLongId;
|
||||
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
|
||||
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*;
|
||||
import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.OAUTH2_CLIENT_NOT_EXISTS;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
/**
|
||||
* {@link OAuth2ClientServiceImpl} 的单元测试类
|
||||
@ -31,40 +35,66 @@ import static org.junit.jupiter.api.Assertions.*;
|
||||
public class OAuth2ClientServiceImplTest extends BaseDbUnitTest {
|
||||
|
||||
@Resource
|
||||
private OAuth2ClientServiceImpl oAuth2ClientService;
|
||||
private OAuth2ClientServiceImpl oauth2ClientService;
|
||||
|
||||
@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
|
||||
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);
|
||||
// 校验记录的属性是否正确
|
||||
OAuth2ClientDO oAuth2Client = oAuth2ClientMapper.selectById(oauth2ClientId);
|
||||
OAuth2ClientDO oAuth2Client = oauth2ClientMapper.selectById(oauth2ClientId);
|
||||
assertPojoEquals(reqVO, oAuth2Client);
|
||||
verify(oauth2ClientProducer).sendOAuth2ClientRefreshMessage();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUpdateOAuth2Client_success() {
|
||||
// mock 数据
|
||||
OAuth2ClientDO dbOAuth2Client = randomPojo(OAuth2ClientDO.class);
|
||||
oAuth2ClientMapper.insert(dbOAuth2Client);// @Sql: 先插入出一条存在的数据
|
||||
oauth2ClientMapper.insert(dbOAuth2Client);// @Sql: 先插入出一条存在的数据
|
||||
// 准备参数
|
||||
OAuth2ClientUpdateReqVO reqVO = randomPojo(OAuth2ClientUpdateReqVO.class, o -> {
|
||||
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);
|
||||
verify(oauth2ClientProducer).sendOAuth2ClientRefreshMessage();
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -73,21 +103,22 @@ public class OAuth2ClientServiceImplTest extends BaseDbUnitTest {
|
||||
OAuth2ClientUpdateReqVO reqVO = randomPojo(OAuth2ClientUpdateReqVO.class);
|
||||
|
||||
// 调用, 并断言异常
|
||||
assertServiceException(() -> oAuth2ClientService.updateOAuth2Client(reqVO), OAUTH2_CLIENT_NOT_EXISTS);
|
||||
assertServiceException(() -> oauth2ClientService.updateOAuth2Client(reqVO), OAUTH2_CLIENT_NOT_EXISTS);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDeleteOAuth2Client_success() {
|
||||
// mock 数据
|
||||
OAuth2ClientDO dbOAuth2Client = randomPojo(OAuth2ClientDO.class);
|
||||
oAuth2ClientMapper.insert(dbOAuth2Client);// @Sql: 先插入出一条存在的数据
|
||||
oauth2ClientMapper.insert(dbOAuth2Client);// @Sql: 先插入出一条存在的数据
|
||||
// 准备参数
|
||||
Long id = dbOAuth2Client.getId();
|
||||
|
||||
// 调用
|
||||
oAuth2ClientService.deleteOAuth2Client(id);
|
||||
// 校验数据不存在了
|
||||
assertNull(oAuth2ClientMapper.selectById(id));
|
||||
oauth2ClientService.deleteOAuth2Client(id);
|
||||
// 校验数据不存在了
|
||||
assertNull(oauth2ClientMapper.selectById(id));
|
||||
verify(oauth2ClientProducer).sendOAuth2ClientRefreshMessage();
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -96,7 +127,7 @@ public class OAuth2ClientServiceImplTest extends BaseDbUnitTest {
|
||||
Long id = randomLongId();
|
||||
|
||||
// 调用, 并断言异常
|
||||
assertServiceException(() -> oAuth2ClientService.deleteOAuth2Client(id), OAUTH2_CLIENT_NOT_EXISTS);
|
||||
assertServiceException(() -> oauth2ClientService.deleteOAuth2Client(id), OAUTH2_CLIENT_NOT_EXISTS);
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -107,18 +138,18 @@ public class OAuth2ClientServiceImplTest extends BaseDbUnitTest {
|
||||
o.setName("潜龙");
|
||||
o.setStatus(CommonStatusEnum.ENABLE.getStatus());
|
||||
});
|
||||
oAuth2ClientMapper.insert(dbOAuth2Client);
|
||||
oauth2ClientMapper.insert(dbOAuth2Client);
|
||||
// 测试 name 不匹配
|
||||
oAuth2ClientMapper.insert(cloneIgnoreId(dbOAuth2Client, o -> o.setName("凤凰")));
|
||||
oauth2ClientMapper.insert(cloneIgnoreId(dbOAuth2Client, o -> o.setName("凤凰")));
|
||||
// 测试 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();
|
||||
reqVO.setName("long");
|
||||
reqVO.setStatus(CommonStatusEnum.ENABLE.getStatus());
|
||||
|
||||
// 调用
|
||||
PageResult<OAuth2ClientDO> pageResult = oAuth2ClientService.getOAuth2ClientPage(reqVO);
|
||||
PageResult<OAuth2ClientDO> pageResult = oauth2ClientService.getOAuth2ClientPage(reqVO);
|
||||
// 断言
|
||||
assertEquals(1, pageResult.getTotal());
|
||||
assertEquals(1, pageResult.getList().size());
|
||||
|
@ -473,6 +473,7 @@ CREATE TABLE IF NOT EXISTS "system_sensitive_word" (
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "system_oauth2_client" (
|
||||
"id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
|
||||
"client_id" varchar NOT NULL,
|
||||
"secret" varchar NOT NULL,
|
||||
"name" 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,
|
||||
"refresh_token_validity_seconds" int 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 '',
|
||||
"create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updater" varchar DEFAULT '',
|
||||
|
Reference in New Issue
Block a user