diff --git a/yudao-dependencies/pom.xml b/yudao-dependencies/pom.xml index 5a7e152ae..37a2c8a1b 100644 --- a/yudao-dependencies/pom.xml +++ b/yudao-dependencies/pom.xml @@ -22,7 +22,6 @@ 1.5.22 2.5 - 5.1.46 1.2.8 3.4.3.4 3.5.2 @@ -76,12 +75,6 @@ ${spring.boot.version} pom import - - - mysql - mysql-connector-java - - @@ -179,11 +172,6 @@ ${revision} - - mysql - mysql-connector-java - ${mysql.version} - com.alibaba druid-spring-boot-starter diff --git a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/collection/CollectionUtils.java b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/collection/CollectionUtils.java index 1ea74ee29..a6466ab44 100644 --- a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/collection/CollectionUtils.java +++ b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/collection/CollectionUtils.java @@ -54,6 +54,13 @@ public class CollectionUtils { return from.stream().map(func).filter(Objects::nonNull).collect(Collectors.toList()); } + public static List convertList(Collection from, Function func, Predicate filter) { + if (CollUtil.isEmpty(from)) { + return new ArrayList<>(); + } + return from.stream().filter(filter).map(func).filter(Objects::nonNull).collect(Collectors.toList()); + } + public static Set convertSet(Collection from, Function func) { if (CollUtil.isEmpty(from)) { return new HashSet<>(); diff --git a/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/dept/rule/DeptDataPermissionRule.java b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/dept/rule/DeptDataPermissionRule.java index ed9168ba0..2a0c9aefa 100644 --- a/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/dept/rule/DeptDataPermissionRule.java +++ b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/dept/rule/DeptDataPermissionRule.java @@ -1,11 +1,13 @@ package cn.iocoder.yudao.framework.datapermission.core.dept.rule; import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.StrUtil; -import cn.iocoder.yudao.framework.datapermission.core.dept.service.DeptDataPermissionFrameworkService; -import cn.iocoder.yudao.framework.datapermission.core.dept.service.dto.DeptDataPermissionRespDTO; +import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import cn.iocoder.yudao.framework.datapermission.core.dept.service.DeptDataPermissionFrameworkService; +import cn.iocoder.yudao.framework.datapermission.core.dept.service.dto.DeptDataPermissionRespDTO; import cn.iocoder.yudao.framework.datapermission.core.rule.DataPermissionRule; import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; import cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils; @@ -13,7 +15,6 @@ import cn.iocoder.yudao.framework.security.core.LoginUser; import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils; import com.baomidou.mybatisplus.core.metadata.TableInfoHelper; import lombok.AllArgsConstructor; -import lombok.Getter; import lombok.extern.slf4j.Slf4j; import net.sf.jsqlparser.expression.Alias; import net.sf.jsqlparser.expression.Expression; @@ -24,10 +25,7 @@ import net.sf.jsqlparser.expression.operators.relational.EqualsTo; import net.sf.jsqlparser.expression.operators.relational.ExpressionList; import net.sf.jsqlparser.expression.operators.relational.InExpression; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; +import java.util.*; /** * 基于部门的 {@link DataPermissionRule} 数据权限规则实现 @@ -50,6 +48,11 @@ import java.util.Set; @Slf4j public class DeptDataPermissionRule implements DataPermissionRule { + /** + * LoginUser 的 Context 缓存 Key + */ + protected static final String CONTEXT_KEY = DeptDataPermissionRule.class.getSimpleName(); + private static final String DEPT_COLUMN_NAME = "dept_id"; private static final String USER_COLUMN_NAME = "user_id"; @@ -90,13 +93,23 @@ public class DeptDataPermissionRule implements DataPermissionRule { if (loginUser == null) { return null; } + // 只有管理员类型的用户,才进行数据权限的处理 + if (ObjectUtil.notEqual(loginUser.getUserType(), UserTypeEnum.ADMIN.getValue())) { + return null; + } // 获得数据权限 - DeptDataPermissionRespDTO deptDataPermission = deptDataPermissionService.getDeptDataPermission(loginUser); + DeptDataPermissionRespDTO deptDataPermission = loginUser.getContext(CONTEXT_KEY, DeptDataPermissionRespDTO.class); + // 从上下文中拿不到,则调用逻辑进行获取 if (deptDataPermission == null) { - log.error("[getExpression][LoginUser({}) 获取数据权限为 null]", JsonUtils.toJsonString(loginUser)); - throw new NullPointerException(String.format("LoginUser(%d) Table(%s/%s) 未返回数据权限", - loginUser.getId(), tableName, tableAlias.getName())); + deptDataPermission = deptDataPermissionService.getDeptDataPermission(loginUser.getId()); + if (deptDataPermission == null) { + log.error("[getExpression][LoginUser({}) 获取数据权限为 null]", JsonUtils.toJsonString(loginUser)); + throw new NullPointerException(String.format("LoginUser(%d) Table(%s/%s) 未返回数据权限", + loginUser.getId(), tableName, tableAlias.getName())); + } + // 添加到上下文中,避免重复计算 + loginUser.setContext(CONTEXT_KEY, deptDataPermission); } // 情况一,如果是 ALL 可查看全部,则无需拼接条件 @@ -111,8 +124,8 @@ public class DeptDataPermissionRule implements DataPermissionRule { } // 情况三,拼接 Dept 和 User 的条件,最后组合 - Expression deptExpression = this.buildDeptExpression(tableName,tableAlias, deptDataPermission.getDeptIds()); - Expression userExpression = this.buildUserExpression(tableName, tableAlias, deptDataPermission.getSelf(), loginUser.getId()); + Expression deptExpression = buildDeptExpression(tableName,tableAlias, deptDataPermission.getDeptIds()); + Expression userExpression = buildUserExpression(tableName, tableAlias, deptDataPermission.getSelf(), loginUser.getId()); if (deptExpression == null && userExpression == null) { // TODO 芋艿:获得不到条件的时候,暂时不抛出异常,而是不返回数据 log.warn("[getExpression][LoginUser({}) Table({}/{}) DeptDataPermission({}) 构建的条件为空]", diff --git a/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/dept/service/DeptDataPermissionFrameworkService.java b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/dept/service/DeptDataPermissionFrameworkService.java index 3ee616755..e491aa036 100644 --- a/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/dept/service/DeptDataPermissionFrameworkService.java +++ b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/dept/service/DeptDataPermissionFrameworkService.java @@ -1,7 +1,6 @@ package cn.iocoder.yudao.framework.datapermission.core.dept.service; import cn.iocoder.yudao.framework.datapermission.core.dept.service.dto.DeptDataPermissionRespDTO; -import cn.iocoder.yudao.framework.security.core.LoginUser; /** * 基于部门的数据权限 Framework Service 接口 @@ -14,9 +13,9 @@ public interface DeptDataPermissionFrameworkService { /** * 获得登陆用户的部门数据权限 * - * @param loginUser 登陆用户 + * @param userId 用户编号 * @return 部门数据权限 */ - DeptDataPermissionRespDTO getDeptDataPermission(LoginUser loginUser); + DeptDataPermissionRespDTO getDeptDataPermission(Long userId); } diff --git a/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/dept/rule/DeptDataPermissionRuleTest.java b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/dept/rule/DeptDataPermissionRuleTest.java index 7282c1816..ef5ff16ac 100644 --- a/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/dept/rule/DeptDataPermissionRuleTest.java +++ b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/dept/rule/DeptDataPermissionRuleTest.java @@ -2,6 +2,7 @@ package cn.iocoder.yudao.framework.datapermission.core.dept.rule; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.ReflectUtil; +import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; import cn.iocoder.yudao.framework.common.util.collection.SetUtils; import cn.iocoder.yudao.framework.datapermission.core.dept.service.DeptDataPermissionFrameworkService; import cn.iocoder.yudao.framework.datapermission.core.dept.service.dto.DeptDataPermissionRespDTO; @@ -69,7 +70,8 @@ class DeptDataPermissionRuleTest extends BaseMockitoUnitTest { String tableName = "t_user"; Alias tableAlias = new Alias("u"); // mock 方法 - LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setId(1L)); + LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setId(1L) + .setUserType(UserTypeEnum.ADMIN.getValue())); securityFrameworkUtilsMock.when(SecurityFrameworkUtils::getLoginUser).thenReturn(loginUser); // 调用 @@ -88,16 +90,18 @@ class DeptDataPermissionRuleTest extends BaseMockitoUnitTest { String tableName = "t_user"; Alias tableAlias = new Alias("u"); // mock 方法(LoginUser) - LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setId(1L)); + LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setId(1L) + .setUserType(UserTypeEnum.ADMIN.getValue())); securityFrameworkUtilsMock.when(SecurityFrameworkUtils::getLoginUser).thenReturn(loginUser); // mock 方法(DeptDataPermissionRespDTO) DeptDataPermissionRespDTO deptDataPermission = new DeptDataPermissionRespDTO().setAll(true); - when(deptDataPermissionFrameworkService.getDeptDataPermission(same(loginUser))).thenReturn(deptDataPermission); + when(deptDataPermissionFrameworkService.getDeptDataPermission(same(1L))).thenReturn(deptDataPermission); // 调用 Expression expression = rule.getExpression(tableName, tableAlias); // 断言 assertNull(expression); + assertSame(deptDataPermission, loginUser.getContext(DeptDataPermissionRule.CONTEXT_KEY, DeptDataPermissionRespDTO.class)); } } @@ -109,16 +113,18 @@ class DeptDataPermissionRuleTest extends BaseMockitoUnitTest { String tableName = "t_user"; Alias tableAlias = new Alias("u"); // mock 方法(LoginUser) - LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setId(1L)); + LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setId(1L) + .setUserType(UserTypeEnum.ADMIN.getValue())); securityFrameworkUtilsMock.when(SecurityFrameworkUtils::getLoginUser).thenReturn(loginUser); // mock 方法(DeptDataPermissionRespDTO) DeptDataPermissionRespDTO deptDataPermission = new DeptDataPermissionRespDTO(); - when(deptDataPermissionFrameworkService.getDeptDataPermission(same(loginUser))).thenReturn(deptDataPermission); + when(deptDataPermissionFrameworkService.getDeptDataPermission(same(1L))).thenReturn(deptDataPermission); // 调用 Expression expression = rule.getExpression(tableName, tableAlias); // 断言 assertEquals("null = null", expression.toString()); + assertSame(deptDataPermission, loginUser.getContext(DeptDataPermissionRule.CONTEXT_KEY, DeptDataPermissionRespDTO.class)); } } @@ -130,17 +136,19 @@ class DeptDataPermissionRuleTest extends BaseMockitoUnitTest { String tableName = "t_user"; Alias tableAlias = new Alias("u"); // mock 方法(LoginUser) - LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setId(1L)); + LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setId(1L) + .setUserType(UserTypeEnum.ADMIN.getValue())); securityFrameworkUtilsMock.when(SecurityFrameworkUtils::getLoginUser).thenReturn(loginUser); // mock 方法(DeptDataPermissionRespDTO) DeptDataPermissionRespDTO deptDataPermission = new DeptDataPermissionRespDTO() .setDeptIds(SetUtils.asSet(10L, 20L)).setSelf(true); - when(deptDataPermissionFrameworkService.getDeptDataPermission(same(loginUser))).thenReturn(deptDataPermission); + when(deptDataPermissionFrameworkService.getDeptDataPermission(same(1L))).thenReturn(deptDataPermission); // 调用 Expression expression = rule.getExpression(tableName, tableAlias); // 断言 assertSame(EXPRESSION_NULL, expression); + assertSame(deptDataPermission, loginUser.getContext(DeptDataPermissionRule.CONTEXT_KEY, DeptDataPermissionRespDTO.class)); } } @@ -152,12 +160,13 @@ class DeptDataPermissionRuleTest extends BaseMockitoUnitTest { String tableName = "t_user"; Alias tableAlias = new Alias("u"); // mock 方法(LoginUser) - LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setId(1L)); + LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setId(1L) + .setUserType(UserTypeEnum.ADMIN.getValue())); securityFrameworkUtilsMock.when(SecurityFrameworkUtils::getLoginUser).thenReturn(loginUser); // mock 方法(DeptDataPermissionRespDTO) DeptDataPermissionRespDTO deptDataPermission = new DeptDataPermissionRespDTO() .setSelf(true); - when(deptDataPermissionFrameworkService.getDeptDataPermission(same(loginUser))).thenReturn(deptDataPermission); + when(deptDataPermissionFrameworkService.getDeptDataPermission(same(1L))).thenReturn(deptDataPermission); // 添加 user 字段配置 rule.addUserColumn("t_user", "id"); @@ -165,6 +174,7 @@ class DeptDataPermissionRuleTest extends BaseMockitoUnitTest { Expression expression = rule.getExpression(tableName, tableAlias); // 断言 assertEquals("u.id = 1", expression.toString()); + assertSame(deptDataPermission, loginUser.getContext(DeptDataPermissionRule.CONTEXT_KEY, DeptDataPermissionRespDTO.class)); } } @@ -176,12 +186,13 @@ class DeptDataPermissionRuleTest extends BaseMockitoUnitTest { String tableName = "t_user"; Alias tableAlias = new Alias("u"); // mock 方法(LoginUser) - LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setId(1L)); + LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setId(1L) + .setUserType(UserTypeEnum.ADMIN.getValue())); securityFrameworkUtilsMock.when(SecurityFrameworkUtils::getLoginUser).thenReturn(loginUser); // mock 方法(DeptDataPermissionRespDTO) DeptDataPermissionRespDTO deptDataPermission = new DeptDataPermissionRespDTO() .setDeptIds(CollUtil.newLinkedHashSet(10L, 20L)); - when(deptDataPermissionFrameworkService.getDeptDataPermission(same(loginUser))).thenReturn(deptDataPermission); + when(deptDataPermissionFrameworkService.getDeptDataPermission(same(1L))).thenReturn(deptDataPermission); // 添加 dept 字段配置 rule.addDeptColumn("t_user", "dept_id"); @@ -189,6 +200,7 @@ class DeptDataPermissionRuleTest extends BaseMockitoUnitTest { Expression expression = rule.getExpression(tableName, tableAlias); // 断言 assertEquals("u.dept_id IN (10, 20)", expression.toString()); + assertSame(deptDataPermission, loginUser.getContext(DeptDataPermissionRule.CONTEXT_KEY, DeptDataPermissionRespDTO.class)); } } @@ -200,12 +212,13 @@ class DeptDataPermissionRuleTest extends BaseMockitoUnitTest { String tableName = "t_user"; Alias tableAlias = new Alias("u"); // mock 方法(LoginUser) - LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setId(1L)); + LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setId(1L) + .setUserType(UserTypeEnum.ADMIN.getValue())); securityFrameworkUtilsMock.when(SecurityFrameworkUtils::getLoginUser).thenReturn(loginUser); // mock 方法(DeptDataPermissionRespDTO) DeptDataPermissionRespDTO deptDataPermission = new DeptDataPermissionRespDTO() .setDeptIds(CollUtil.newLinkedHashSet(10L, 20L)).setSelf(true); - when(deptDataPermissionFrameworkService.getDeptDataPermission(same(loginUser))).thenReturn(deptDataPermission); + when(deptDataPermissionFrameworkService.getDeptDataPermission(same(1L))).thenReturn(deptDataPermission); // 添加 user 字段配置 rule.addUserColumn("t_user", "id"); // 添加 dept 字段配置 @@ -215,6 +228,7 @@ class DeptDataPermissionRuleTest extends BaseMockitoUnitTest { Expression expression = rule.getExpression(tableName, tableAlias); // 断言 assertEquals("u.dept_id IN (10, 20) OR u.id = 1", expression.toString()); + assertSame(deptDataPermission, loginUser.getContext(DeptDataPermissionRule.CONTEXT_KEY, DeptDataPermissionRespDTO.class)); } } diff --git a/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/web/TenantContextWebFilter.java b/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/web/TenantContextWebFilter.java index ec159e0ec..272419c09 100644 --- a/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/web/TenantContextWebFilter.java +++ b/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/web/TenantContextWebFilter.java @@ -1,7 +1,7 @@ package cn.iocoder.yudao.framework.tenant.core.web; -import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder; +import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils; import org.springframework.web.filter.OncePerRequestFilter; import javax.servlet.FilterChain; @@ -24,9 +24,9 @@ public class TenantContextWebFilter extends OncePerRequestFilter { protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { // 设置 - String tenantId = request.getHeader(HEADER_TENANT_ID); - if (StrUtil.isNotEmpty(tenantId)) { - TenantContextHolder.setTenantId(Long.valueOf(tenantId)); + Long tenantId = WebFrameworkUtils.getTenantId(request); + if (tenantId != null) { + TenantContextHolder.setTenantId(tenantId); } try { chain.doFilter(request, response); diff --git a/yudao-framework/yudao-spring-boot-starter-file/src/main/java/cn/iocoder/yudao/framework/file/core/client/ftp/FtpFileClient.java b/yudao-framework/yudao-spring-boot-starter-file/src/main/java/cn/iocoder/yudao/framework/file/core/client/ftp/FtpFileClient.java index d50bc8501..4ad345bdc 100644 --- a/yudao-framework/yudao-spring-boot-starter-file/src/main/java/cn/iocoder/yudao/framework/file/core/client/ftp/FtpFileClient.java +++ b/yudao-framework/yudao-spring-boot-starter-file/src/main/java/cn/iocoder/yudao/framework/file/core/client/ftp/FtpFileClient.java @@ -27,9 +27,11 @@ public class FtpFileClient extends AbstractFileClient { @Override protected void doInit() { - // 补全风格。例如说 Linux 是 /,Windows 是 \ - if (!config.getBasePath().endsWith(File.separator)) { - config.setBasePath(config.getBasePath() + File.separator); + // 把配置的 \ 替换成 /, 如果路径配置 \a\test, 替换成 /a/test, 替换方法已经处理 null 情况 + config.setBasePath(StrUtil.replace(config.getBasePath(), StrUtil.BACKSLASH, StrUtil.SLASH)); + // ftp的路径是 / 结尾 + if (!config.getBasePath().endsWith(StrUtil.SLASH)) { + config.setBasePath(config.getBasePath() + StrUtil.SLASH); } // 初始化 Ftp 对象 this.ftp = new Ftp(config.getHost(), config.getPort(), config.getUsername(), config.getPassword(), @@ -42,6 +44,7 @@ public class FtpFileClient extends AbstractFileClient { String filePath = getFilePath(path); String fileName = FileUtil.getName(filePath); String dir = StrUtil.removeSuffix(filePath, fileName); + ftp.reconnectIfTimeout(); boolean success = ftp.upload(dir, fileName, new ByteArrayInputStream(content)); if (!success) { throw new FtpException(StrUtil.format("上传文件到目标目录 ({}) 失败", filePath)); @@ -53,6 +56,7 @@ public class FtpFileClient extends AbstractFileClient { @Override public void delete(String path) { String filePath = getFilePath(path); + ftp.reconnectIfTimeout(); ftp.delFile(filePath); } @@ -60,8 +64,9 @@ public class FtpFileClient extends AbstractFileClient { public byte[] getContent(String path) { String filePath = getFilePath(path); String fileName = FileUtil.getName(filePath); - String dir = StrUtil.removeSuffix(path, fileName); + String dir = StrUtil.removeSuffix(filePath, fileName); ByteArrayOutputStream out = new ByteArrayOutputStream(); + ftp.reconnectIfTimeout(); ftp.download(dir, fileName, out); return out.toByteArray(); } @@ -70,4 +75,4 @@ public class FtpFileClient extends AbstractFileClient { return config.getBasePath() + path; } -} +} \ No newline at end of file diff --git a/yudao-framework/yudao-spring-boot-starter-security/pom.xml b/yudao-framework/yudao-spring-boot-starter-security/pom.xml index ba33598c5..4e32a6c7c 100644 --- a/yudao-framework/yudao-spring-boot-starter-security/pom.xml +++ b/yudao-framework/yudao-spring-boot-starter-security/pom.xml @@ -44,18 +44,11 @@ spring-boot-starter-security - + - org.activiti - activiti-engine - 7.1.0.M6 - - - * - * - - - true + cn.iocoder.boot + yudao-module-system-api + ${revision} diff --git a/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/config/SecurityProperties.java b/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/config/SecurityProperties.java index 537463e94..a9373cd9c 100644 --- a/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/config/SecurityProperties.java +++ b/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/config/SecurityProperties.java @@ -6,7 +6,6 @@ import org.springframework.validation.annotation.Validated; import javax.validation.constraints.NotEmpty; import javax.validation.constraints.NotNull; -import java.time.Duration; @ConfigurationProperties(prefix = "yudao.security") @Validated @@ -18,18 +17,6 @@ public class SecurityProperties { */ @NotEmpty(message = "Token Header 不能为空") private String tokenHeader; - /** - * Token 过期时间 - */ - @NotNull(message = "Token 过期时间不能为空") - private Duration tokenTimeout; - /** - * Session 过期时间 - * - * 当 User 用户超过当前时间未操作,则 Session 会过期 - */ - @NotNull(message = "Session 过期时间不能为空") - private Duration sessionTimeout; /** * mock 模式的开关 diff --git a/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/config/YudaoSecurityAutoConfiguration.java b/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/config/YudaoSecurityAutoConfiguration.java index 1db4797d4..4933969bc 100644 --- a/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/config/YudaoSecurityAutoConfiguration.java +++ b/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/config/YudaoSecurityAutoConfiguration.java @@ -1,15 +1,12 @@ package cn.iocoder.yudao.framework.security.config; import cn.iocoder.yudao.framework.security.core.aop.PreAuthenticatedAspect; -import cn.iocoder.yudao.framework.security.core.authentication.MultiUserDetailsAuthenticationProvider; import cn.iocoder.yudao.framework.security.core.context.TransmittableThreadLocalSecurityContextHolderStrategy; import cn.iocoder.yudao.framework.security.core.filter.TokenAuthenticationFilter; import cn.iocoder.yudao.framework.security.core.handler.AccessDeniedHandlerImpl; import cn.iocoder.yudao.framework.security.core.handler.AuthenticationEntryPointImpl; -import cn.iocoder.yudao.framework.security.core.handler.LogoutSuccessHandlerImpl; -import cn.iocoder.yudao.framework.security.core.service.SecurityAuthFrameworkService; -import cn.iocoder.yudao.framework.web.config.WebProperties; import cn.iocoder.yudao.framework.web.core.handler.GlobalExceptionHandler; +import cn.iocoder.yudao.module.system.api.auth.OAuth2TokenApi; import org.springframework.beans.factory.config.MethodInvokingFactoryBean; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; @@ -19,10 +16,8 @@ import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.security.web.access.AccessDeniedHandler; -import org.springframework.security.web.authentication.logout.LogoutSuccessHandler; import javax.annotation.Resource; -import java.util.List; /** * Spring Security 自动配置类,主要用于相关组件的配置 @@ -63,14 +58,6 @@ public class YudaoSecurityAutoConfiguration { return new AccessDeniedHandlerImpl(); } - /** - * 退出处理类 Bean - */ - @Bean - public LogoutSuccessHandler logoutSuccessHandler(MultiUserDetailsAuthenticationProvider authenticationProvider) { - return new LogoutSuccessHandlerImpl(securityProperties, authenticationProvider); - } - /** * Spring Security 加密器 * 考虑到安全性,这里采用 BCryptPasswordEncoder 加密器 @@ -86,19 +73,9 @@ public class YudaoSecurityAutoConfiguration { * Token 认证过滤器 Bean */ @Bean - public TokenAuthenticationFilter authenticationTokenFilter(MultiUserDetailsAuthenticationProvider authenticationProvider, - GlobalExceptionHandler globalExceptionHandler) { - return new TokenAuthenticationFilter(securityProperties, authenticationProvider, globalExceptionHandler); - } - - /** - * 身份验证的 Provider Bean,通过它实现账号 + 密码的认证 - */ - @Bean - public MultiUserDetailsAuthenticationProvider authenticationProvider( - List securityFrameworkServices, - WebProperties webProperties, PasswordEncoder passwordEncoder) { - return new MultiUserDetailsAuthenticationProvider(securityFrameworkServices, webProperties, passwordEncoder); + public TokenAuthenticationFilter authenticationTokenFilter(GlobalExceptionHandler globalExceptionHandler, + OAuth2TokenApi oauth2TokenApi) { + return new TokenAuthenticationFilter(securityProperties, globalExceptionHandler, oauth2TokenApi); } /** diff --git a/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/config/YudaoWebSecurityConfigurerAdapter.java b/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/config/YudaoWebSecurityConfigurerAdapter.java index 0c0dd3278..61edba6e8 100644 --- a/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/config/YudaoWebSecurityConfigurerAdapter.java +++ b/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/config/YudaoWebSecurityConfigurerAdapter.java @@ -1,7 +1,5 @@ package cn.iocoder.yudao.framework.security.config; -import cn.hutool.core.util.StrUtil; -import cn.iocoder.yudao.framework.security.core.authentication.MultiUserDetailsAuthenticationProvider; import cn.iocoder.yudao.framework.security.core.filter.TokenAuthenticationFilter; import cn.iocoder.yudao.framework.web.config.WebProperties; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; @@ -9,7 +7,6 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpMethod; import org.springframework.security.authentication.AuthenticationManager; -import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; @@ -17,7 +14,6 @@ import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.security.web.access.AccessDeniedHandler; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; -import org.springframework.security.web.authentication.logout.LogoutSuccessHandler; import javax.annotation.Resource; import java.util.List; @@ -34,8 +30,6 @@ public class YudaoWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdap @Resource private WebProperties webProperties; - @Resource - private MultiUserDetailsAuthenticationProvider authenticationProvider; /** * 认证失败处理类 Bean */ @@ -46,11 +40,6 @@ public class YudaoWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdap */ @Resource private AccessDeniedHandler accessDeniedHandler; - /** - * 退出处理类 Bean - */ - @Resource - private LogoutSuccessHandler logoutSuccessHandler; /** * Token 认证过滤器 Bean */ @@ -76,14 +65,6 @@ public class YudaoWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdap return super.authenticationManagerBean(); } - /** - * 身份认证接口 - */ - @Override - protected void configure(AuthenticationManagerBuilder auth) throws Exception { - auth.authenticationProvider(authenticationProvider); - } - /** * 配置 URL 的安全配置 * @@ -114,11 +95,8 @@ public class YudaoWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdap .headers().frameOptions().disable().and() // 一堆自定义的 Spring Security 处理器 .exceptionHandling().authenticationEntryPoint(authenticationEntryPoint) - .accessDeniedHandler(accessDeniedHandler).and() - // 登出地址的配置 - .logout().logoutSuccessHandler(logoutSuccessHandler).logoutRequestMatcher(request -> // 匹配多种用户类型的登出 - StrUtil.equalsAny(request.getRequestURI(), buildAdminApi("/system/logout"), - buildAppApi("/member/logout"))); + .accessDeniedHandler(accessDeniedHandler); + // 登录、登录暂时不使用 Spring Security 的拓展点,主要考虑一方面拓展多用户、多种登录方式相对复杂,一方面用户的学习成本较高 // 设置每个请求的权限 httpSecurity @@ -140,11 +118,7 @@ public class YudaoWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdap // 添加 JWT Filter httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class); } - - private String buildAdminApi(String url) { - return webProperties.getAdminApi().getPrefix() + url; - } - + private String buildAppApi(String url) { return webProperties.getAppApi().getPrefix() + url; } diff --git a/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/LoginUser.java b/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/LoginUser.java index 84c7f94c4..20d567963 100644 --- a/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/LoginUser.java +++ b/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/LoginUser.java @@ -1,14 +1,12 @@ package cn.iocoder.yudao.framework.security.core; import cn.hutool.core.map.MapUtil; -import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; import com.fasterxml.jackson.annotation.JsonIgnore; import lombok.Data; -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.core.userdetails.UserDetails; -import java.util.*; +import java.util.HashMap; +import java.util.Map; /** * 登录用户信息 @@ -16,7 +14,7 @@ import java.util.*; * @author 芋道源码 */ @Data -public class LoginUser implements UserDetails { +public class LoginUser { /** * 用户编号 @@ -28,39 +26,11 @@ public class LoginUser implements UserDetails { * 关联 {@link UserTypeEnum} */ private Integer userType; - /** - * 最后更新时间 - */ - private Date updateTime; - - /** - * 用户名 - */ - private String username; - /** - * 密码 - */ - private String password; - /** - * 状态 - */ - private Integer status; /** * 租户编号 */ private Long tenantId; - // ========== UserTypeEnum.ADMIN 独有字段 ========== - // TODO 芋艿:可以通过定义一个 Map exts 的方式,去除管理员的字段。不过这样会导致系统比较复杂,所以暂时不去掉先; - /** - * 角色编号数组 - */ - private Set roleIds; - /** - * 部门编号 - */ - private Long deptId; - // ========== 上下文 ========== /** * 上下文字段,不进行持久化 @@ -70,49 +40,6 @@ public class LoginUser implements UserDetails { @JsonIgnore private Map context; - @Override - @JsonIgnore// 避免序列化 - public String getPassword() { - return password; - } - - @Override - public String getUsername() { - return username; - } - - @Override - @JsonIgnore// 避免序列化 - public boolean isEnabled() { - return CommonStatusEnum.ENABLE.getStatus().equals(status); - } - - @Override - @JsonIgnore// 避免序列化 - public Collection getAuthorities() { - return new HashSet<>(); - } - - @Override - @JsonIgnore// 避免序列化 - public boolean isAccountNonExpired() { - return true; // 返回 true,不依赖 Spring Security 判断 - } - - @Override - @JsonIgnore// 避免序列化 - public boolean isAccountNonLocked() { - return true; // 返回 true,不依赖 Spring Security 判断 - } - - @Override - @JsonIgnore// 避免序列化 - public boolean isCredentialsNonExpired() { - return true; // 返回 true,不依赖 Spring Security 判断 - } - - // ========== 上下文 ========== - public void setContext(String key, Object value) { if (context == null) { context = new HashMap<>(); diff --git a/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/authentication/MultiUserDetailsAuthenticationProvider.java b/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/authentication/MultiUserDetailsAuthenticationProvider.java deleted file mode 100644 index dc8533f96..000000000 --- a/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/authentication/MultiUserDetailsAuthenticationProvider.java +++ /dev/null @@ -1,149 +0,0 @@ -package cn.iocoder.yudao.framework.security.core.authentication; - -import cn.hutool.core.lang.Assert; -import cn.hutool.core.util.StrUtil; -import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; -import cn.iocoder.yudao.framework.security.core.LoginUser; -import cn.iocoder.yudao.framework.security.core.service.SecurityAuthFrameworkService; -import cn.iocoder.yudao.framework.web.config.WebProperties; -import org.springframework.security.authentication.AuthenticationProvider; -import org.springframework.security.authentication.BadCredentialsException; -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; -import org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider; -import org.springframework.security.core.AuthenticationException; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.crypto.password.PasswordEncoder; - -import javax.servlet.http.HttpServletRequest; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -/** - * 支持多用户类型的 AuthenticationProvider 实现类 - * - * 为什么不用 {@link org.springframework.security.authentication.ProviderManager} 呢? - * 原因是,需要每个用户类型实现对应的 {@link AuthenticationProvider} + authentication,略显麻烦。实际,也是可以实现的。 - * - * 另外,额外支持 verifyTokenAndRefresh 校验令牌、logout 登出、mockLogin 模拟登陆等操作。 - * 实际上,它就是 {@link SecurityAuthFrameworkService} 定义的三个接口。 - * 因为需要支持多种类型,所以需要根据请求的 URL,判断出对应的用户类型,从而使用对应的 SecurityAuthFrameworkService 是吸纳 - * - * @see cn.iocoder.yudao.framework.common.enums.UserTypeEnum - * @author 芋道源码 - */ -public class MultiUserDetailsAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider { - - private final Map services = new HashMap<>(); - - private final WebProperties properties; - - private final PasswordEncoder passwordEncoder; - - public MultiUserDetailsAuthenticationProvider(List serviceList, - WebProperties properties, PasswordEncoder passwordEncoder) { - serviceList.forEach(service -> services.put(service.getUserType(), service)); - this.properties = properties; - this.passwordEncoder = passwordEncoder; - } - - // ========== AuthenticationProvider 相关 ========== - - @Override - protected UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) - throws AuthenticationException { - // 执行用户的加载 - return selectService(authentication).loadUserByUsername(username); - } - - private SecurityAuthFrameworkService selectService(UsernamePasswordAuthenticationToken authentication) { - // 第一步,获得用户类型 - UserTypeEnum userType = getUserType(authentication); - // 第二步,获得 SecurityAuthFrameworkService - SecurityAuthFrameworkService service = services.get(userType); - Assert.notNull(service, "用户类型({}) 找不到 SecurityAuthFrameworkService 实现类", userType); - return service; - } - - private UserTypeEnum getUserType(UsernamePasswordAuthenticationToken authentication) { - Assert.isInstanceOf(MultiUsernamePasswordAuthenticationToken.class, authentication); - MultiUsernamePasswordAuthenticationToken multiAuthentication = (MultiUsernamePasswordAuthenticationToken) authentication; - UserTypeEnum userType = multiAuthentication.getUserType(); - Assert.notNull(userType, "用户类型不能为空"); - return userType; - } - - @Override // copy 自 DaoAuthenticationProvider 的 additionalAuthenticationChecks 方法 - protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) - throws AuthenticationException { - // 校验 credentials - if (authentication.getCredentials() == null) { - this.logger.debug("Failed to authenticate since no credentials provided"); - throw new BadCredentialsException(this.messages.getMessage( - "AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials")); - } - // 校验 password - String presentedPassword = authentication.getCredentials().toString(); - if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) { - this.logger.debug("Failed to authenticate since password does not match stored value"); - throw new BadCredentialsException(this.messages.getMessage( - "AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials")); - } - } - - // ========== SecurityAuthFrameworkService 相关 ========== - - /** - * 校验 token 的有效性,并获取用户信息 - * 通过后,刷新 token 的过期时间 - * - * @param request 请求 - * @param token token - * @return 用户信息 - */ - public LoginUser verifyTokenAndRefresh(HttpServletRequest request, String token) { - return selectService(request).verifyTokenAndRefresh(token); - } - - /** - * 模拟指定用户编号的 LoginUser - * - * @param request 请求 - * @param userId 用户编号 - * @return 登录用户 - */ - public LoginUser mockLogin(HttpServletRequest request, Long userId) { - return selectService(request).mockLogin(userId); - } - - /** - * 基于 token 退出登录 - * - * @param request 请求 - * @param token token - */ - public void logout(HttpServletRequest request, String token) { - selectService(request).logout(token); - } - - private SecurityAuthFrameworkService selectService(HttpServletRequest request) { - // 第一步,获得用户类型 - UserTypeEnum userType = getUserType(request); - // 第二步,获得 SecurityAuthFrameworkService - SecurityAuthFrameworkService service = services.get(userType); - Assert.notNull(service, "URI({}) 用户类型({}) 找不到 SecurityAuthFrameworkService 实现类", - request.getRequestURI(), userType); - return service; - } - - private UserTypeEnum getUserType(HttpServletRequest request) { - if (request.getRequestURI().startsWith(properties.getAdminApi().getPrefix())) { - return UserTypeEnum.ADMIN; - } - if (request.getRequestURI().startsWith(properties.getAppApi().getPrefix())) { - return UserTypeEnum.MEMBER; - } - throw new IllegalArgumentException(StrUtil.format("URI({}) 找不到匹配的用户类型", request.getRequestURI())); - } - -} diff --git a/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/authentication/MultiUsernamePasswordAuthenticationToken.java b/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/authentication/MultiUsernamePasswordAuthenticationToken.java deleted file mode 100644 index f0bc8dfac..000000000 --- a/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/authentication/MultiUsernamePasswordAuthenticationToken.java +++ /dev/null @@ -1,43 +0,0 @@ -package cn.iocoder.yudao.framework.security.core.authentication; - -import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; -import lombok.Getter; -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; -import org.springframework.security.core.GrantedAuthority; - -import java.util.Collection; - -/** - * 支持多用户的 UsernamePasswordAuthenticationToken 实现类 - * - * @author 芋道源码 - */ -@Getter -public class MultiUsernamePasswordAuthenticationToken extends UsernamePasswordAuthenticationToken { - - /** - * 用户类型 - */ - private UserTypeEnum userType; - - public MultiUsernamePasswordAuthenticationToken(Object principal, Object credentials) { - super(principal, credentials); - } - - public MultiUsernamePasswordAuthenticationToken(Object principal, Object credentials, - Collection authorities) { - super(principal, credentials, authorities); - } - - public MultiUsernamePasswordAuthenticationToken(Object principal, Object credentials, UserTypeEnum userType) { - super(principal, credentials); - this.userType = userType; - } - - public MultiUsernamePasswordAuthenticationToken(Object principal, Object credentials, - Collection authorities, UserTypeEnum userType) { - super(principal, credentials, authorities); - this.userType = userType; - } - -} diff --git a/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/filter/TokenAuthenticationFilter.java b/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/filter/TokenAuthenticationFilter.java index 0908560c8..204b03fa0 100644 --- a/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/filter/TokenAuthenticationFilter.java +++ b/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/filter/TokenAuthenticationFilter.java @@ -1,14 +1,19 @@ package cn.iocoder.yudao.framework.security.core.filter; +import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.exception.ServiceException; import cn.iocoder.yudao.framework.common.pojo.CommonResult; import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils; import cn.iocoder.yudao.framework.security.config.SecurityProperties; import cn.iocoder.yudao.framework.security.core.LoginUser; -import cn.iocoder.yudao.framework.security.core.authentication.MultiUserDetailsAuthenticationProvider; import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils; import cn.iocoder.yudao.framework.web.core.handler.GlobalExceptionHandler; +import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils; +import cn.iocoder.yudao.module.system.api.auth.OAuth2TokenApi; +import cn.iocoder.yudao.module.system.api.auth.dto.OAuth2AccessTokenCheckRespDTO; import lombok.RequiredArgsConstructor; +import org.springframework.security.access.AccessDeniedException; import org.springframework.web.filter.OncePerRequestFilter; import javax.servlet.FilterChain; @@ -28,24 +33,26 @@ public class TokenAuthenticationFilter extends OncePerRequestFilter { private final SecurityProperties securityProperties; - private final MultiUserDetailsAuthenticationProvider authenticationProvider; - private final GlobalExceptionHandler globalExceptionHandler; + private final OAuth2TokenApi oauth2TokenApi; + @Override @SuppressWarnings("NullableProblems") protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { String token = SecurityFrameworkUtils.obtainAuthorization(request, securityProperties.getTokenHeader()); if (StrUtil.isNotEmpty(token)) { + Integer userType = WebFrameworkUtils.getLoginUserType(request); try { - // 验证 token 有效性 - LoginUser loginUser = authenticationProvider.verifyTokenAndRefresh(request, token); - // 模拟 Login 功能,方便日常开发调试 + // 1.1 基于 token 构建登录用户 + LoginUser loginUser = buildLoginUserByToken(token, userType); + // 1.2 模拟 Login 功能,方便日常开发调试 if (loginUser == null) { - loginUser = mockLoginUser(request, token); + loginUser = mockLoginUser(request, token, userType); } - // 设置当前用户 + + // 2. 设置当前用户 if (loginUser != null) { SecurityFrameworkUtils.setLoginUser(loginUser, request); } @@ -60,6 +67,25 @@ public class TokenAuthenticationFilter extends OncePerRequestFilter { chain.doFilter(request, response); } + private LoginUser buildLoginUserByToken(String token, Integer userType) { + try { + OAuth2AccessTokenCheckRespDTO accessToken = oauth2TokenApi.checkAccessToken(token); + if (accessToken == null) { + return null; + } + // 用户类型不匹配,无权限 + if (ObjectUtil.notEqual(accessToken.getUserType(), userType)) { + throw new AccessDeniedException("错误的用户类型"); + } + // 构建登录用户 + return new LoginUser().setId(accessToken.getUserId()).setUserType(accessToken.getUserType()) + .setTenantId(accessToken.getTenantId()); + } catch (ServiceException serviceException) { + // 校验 Token 不通过时,考虑到一些接口是无需登录的,所以直接返回 null 即可 + return null; + } + } + /** * 模拟登录用户,方便日常开发调试 * @@ -67,9 +93,10 @@ public class TokenAuthenticationFilter extends OncePerRequestFilter { * * @param request 请求 * @param token 模拟的 token,格式为 {@link SecurityProperties#getMockSecret()} + 用户编号 + * @param userType 用户类型 * @return 模拟的 LoginUser */ - private LoginUser mockLoginUser(HttpServletRequest request, String token) { + private LoginUser mockLoginUser(HttpServletRequest request, String token, Integer userType) { if (!securityProperties.getMockEnable()) { return null; } @@ -77,8 +104,10 @@ public class TokenAuthenticationFilter extends OncePerRequestFilter { if (!token.startsWith(securityProperties.getMockSecret())) { return null; } + // 构建模拟用户 Long userId = Long.valueOf(token.substring(securityProperties.getMockSecret().length())); - return authenticationProvider.mockLogin(request, userId); + return new LoginUser().setId(userId).setUserType(userType) + .setTenantId(WebFrameworkUtils.getTenantId(request)); } } diff --git a/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/handler/LogoutSuccessHandlerImpl.java b/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/handler/LogoutSuccessHandlerImpl.java deleted file mode 100644 index 1a642304c..000000000 --- a/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/handler/LogoutSuccessHandlerImpl.java +++ /dev/null @@ -1,40 +0,0 @@ -package cn.iocoder.yudao.framework.security.core.handler; - -import cn.hutool.core.util.StrUtil; -import cn.iocoder.yudao.framework.common.pojo.CommonResult; -import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils; -import cn.iocoder.yudao.framework.security.config.SecurityProperties; -import cn.iocoder.yudao.framework.security.core.authentication.MultiUserDetailsAuthenticationProvider; -import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils; -import lombok.AllArgsConstructor; -import org.springframework.security.core.Authentication; -import org.springframework.security.web.authentication.logout.LogoutSuccessHandler; - -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - - -/** - * 自定义退出处理器 - * - * @author ruoyi - */ -@AllArgsConstructor -public class LogoutSuccessHandlerImpl implements LogoutSuccessHandler { - - private final SecurityProperties securityProperties; - - private final MultiUserDetailsAuthenticationProvider authenticationProvider; - - @Override - public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) { - // 执行退出 - String token = SecurityFrameworkUtils.obtainAuthorization(request, securityProperties.getTokenHeader()); - if (StrUtil.isNotBlank(token)) { - authenticationProvider.logout(request, token); - } - // 返回成功 - ServletUtils.writeJSON(response, CommonResult.success(null)); - } - -} diff --git a/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/service/SecurityAuthFrameworkService.java b/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/service/SecurityAuthFrameworkService.java deleted file mode 100644 index 1f76e161f..000000000 --- a/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/service/SecurityAuthFrameworkService.java +++ /dev/null @@ -1,45 +0,0 @@ -package cn.iocoder.yudao.framework.security.core.service; - -import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; -import cn.iocoder.yudao.framework.security.core.LoginUser; -import org.springframework.security.core.userdetails.UserDetailsService; - -/** - * Security 框架 Auth Service 接口,定义不同用户类型的 {@link UserTypeEnum} 需要实现的方法 - * - * @author 芋道源码 - */ -public interface SecurityAuthFrameworkService extends UserDetailsService { - - /** - * 校验 token 的有效性,并获取用户信息 - * 通过后,刷新 token 的过期时间 - * - * @param token token - * @return 用户信息 - */ - LoginUser verifyTokenAndRefresh(String token); - - /** - * 模拟指定用户编号的 LoginUser - * - * @param userId 用户编号 - * @return 登录用户 - */ - LoginUser mockLogin(Long userId); - - /** - * 基于 token 退出登录 - * - * @param token token - */ - void logout(String token); - - /** - * 获得用户类型。每个用户类型,对应一个 SecurityAuthFrameworkService 实现类。 - * - * @return 用户类型 - */ - UserTypeEnum getUserType(); - -} diff --git a/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/util/SecurityFrameworkUtils.java b/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/util/SecurityFrameworkUtils.java index 562c6ed9f..253974539 100644 --- a/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/util/SecurityFrameworkUtils.java +++ b/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/util/SecurityFrameworkUtils.java @@ -11,7 +11,7 @@ import org.springframework.security.web.authentication.WebAuthenticationDetailsS import org.springframework.util.StringUtils; import javax.servlet.http.HttpServletRequest; -import java.util.Set; +import java.util.Collections; /** * 安全服务工具类 @@ -79,17 +79,6 @@ public class SecurityFrameworkUtils { return loginUser != null ? loginUser.getId() : null; } - /** - * 获得当前用户的角色编号数组 - * - * @return 角色编号数组 - */ - @Nullable - public static Set getLoginUserRoleIds() { - LoginUser loginUser = getLoginUser(); - return loginUser != null ? loginUser.getRoleIds() : null; - } - /** * 设置当前用户 * @@ -110,7 +99,7 @@ public class SecurityFrameworkUtils { private static Authentication buildAuthentication(LoginUser loginUser, HttpServletRequest request) { // 创建 UsernamePasswordAuthenticationToken 对象 UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken( - loginUser, null, loginUser.getAuthorities()); + loginUser, null, Collections.emptyList()); authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); return authenticationToken; } diff --git a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/config/YudaoWebAutoConfiguration.java b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/config/YudaoWebAutoConfiguration.java index a6f932dd0..a7c5b7a53 100644 --- a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/config/YudaoWebAutoConfiguration.java +++ b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/config/YudaoWebAutoConfiguration.java @@ -7,6 +7,7 @@ import cn.iocoder.yudao.framework.web.core.filter.DemoFilter; import cn.iocoder.yudao.framework.web.core.filter.XssFilter; import cn.iocoder.yudao.framework.web.core.handler.GlobalExceptionHandler; import cn.iocoder.yudao.framework.web.core.handler.GlobalResponseBodyHandler; +import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.EnableConfigurationProperties; @@ -65,6 +66,13 @@ public class YudaoWebAutoConfiguration implements WebMvcConfigurer { return new GlobalResponseBodyHandler(); } + @Bean + @SuppressWarnings("InstantiationOfUtilityClass") + public WebFrameworkUtils webFrameworkUtils(WebProperties webProperties) { + // 由于 WebFrameworkUtils 需要使用到 webProperties 属性,所以注册为一个 Bean + return new WebFrameworkUtils(webProperties); + } + // ========== Filter 相关 ========== /** diff --git a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/util/WebFrameworkUtils.java b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/util/WebFrameworkUtils.java index 273f34072..f5ac676fa 100644 --- a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/util/WebFrameworkUtils.java +++ b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/util/WebFrameworkUtils.java @@ -1,7 +1,9 @@ package cn.iocoder.yudao.framework.web.core.util; +import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.web.config.WebProperties; import org.springframework.web.context.request.RequestAttributes; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; @@ -21,16 +23,43 @@ public class WebFrameworkUtils { private static final String REQUEST_ATTRIBUTE_COMMON_RESULT = "common_result"; + private static final String HEADER_TENANT_ID = "tenant-id"; + + private static WebProperties properties; + + public WebFrameworkUtils(WebProperties webProperties) { + WebFrameworkUtils.properties = webProperties; + } + + /** + * 获得租户编号,从 header 中 + * 考虑到其它 framework 组件也会使用到租户编号,所以不得不放在 WebFrameworkUtils 统一提供 + * + * @param request 请求 + * @return 租户编号 + */ + public static Long getTenantId(HttpServletRequest request) { + String tenantId = request.getHeader(HEADER_TENANT_ID); + return StrUtil.isNotEmpty(tenantId) ? Long.valueOf(tenantId) : null; + } + public static void setLoginUserId(ServletRequest request, Long userId) { request.setAttribute(REQUEST_ATTRIBUTE_LOGIN_USER_ID, userId); } + /** + * 设置用户类型 + * + * @param request 请求 + * @param userType 用户类型 + */ public static void setLoginUserType(ServletRequest request, Integer userType) { request.setAttribute(REQUEST_ATTRIBUTE_LOGIN_USER_TYPE, userType); } /** * 获得当前用户的编号,从请求中 + * 注意:该方法仅限于 framework 框架使用!!! * * @param request 请求 * @return 用户编号 @@ -43,7 +72,8 @@ public class WebFrameworkUtils { } /** - * 获得当前用户的类型,从请求中 + * 获得当前用户的类型 + * 注意:该方法仅限于 web 相关的 framework 组件使用!!! * * @param request 请求 * @return 用户编号 @@ -52,7 +82,19 @@ public class WebFrameworkUtils { if (request == null) { return null; } - return (Integer) request.getAttribute(REQUEST_ATTRIBUTE_LOGIN_USER_TYPE); + // 1. 优先,从 Attribute 中获取 + Integer userType = (Integer) request.getAttribute(REQUEST_ATTRIBUTE_LOGIN_USER_TYPE); + if (userType != null) { + return userType; + } + // 2. 其次,基于 URL 前缀的约定 + if (request.getRequestURI().startsWith(properties.getAdminApi().getPrefix())) { + return UserTypeEnum.ADMIN.getValue(); + } + if (request.getRequestURI().startsWith(properties.getAppApi().getPrefix())) { + return UserTypeEnum.MEMBER.getValue(); + } + return null; } public static Integer getLoginUserType() { diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/FileController.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/FileController.java index e3d810dec..4500133ed 100644 --- a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/FileController.java +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/FileController.java @@ -4,6 +4,7 @@ import cn.hutool.core.io.IoUtil; import cn.iocoder.yudao.framework.common.pojo.CommonResult; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils; +import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog; import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FilePageReqVO; import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FileRespVO; import cn.iocoder.yudao.module.infra.convert.file.FileConvert; @@ -42,8 +43,9 @@ public class FileController { @ApiImplicitParam(name = "file", value = "文件附件", required = true, dataTypeClass = MultipartFile.class), @ApiImplicitParam(name = "path", value = "文件路径", example = "yudaoyuanma.png", dataTypeClass = String.class) }) + @OperateLog(logArgs = false) // 上传文件,没有记录操作日志的必要 public CommonResult uploadFile(@RequestParam("file") MultipartFile file, - @RequestParam("path") String path) throws Exception { + @RequestParam(value = "path", required = false) String path) throws Exception { return success(fileService.createFile(path, IoUtil.readBytes(file.getInputStream()))); } diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/file/FileServiceImpl.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/file/FileServiceImpl.java index 66af402e0..4f3736f1b 100644 --- a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/file/FileServiceImpl.java +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/file/FileServiceImpl.java @@ -2,6 +2,8 @@ package cn.iocoder.yudao.module.infra.service.file; import cn.hutool.core.io.FileTypeUtil; import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.StrUtil; +import cn.hutool.crypto.digest.DigestUtil; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.file.core.client.FileClient; import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FilePageReqVO; @@ -36,6 +38,12 @@ public class FileServiceImpl implements FileService { @Override public String createFile(String path, byte[] content) throws Exception { + // 计算默认的 path 名 + String type = FileTypeUtil.getType(new ByteArrayInputStream(content)); + if (StrUtil.isEmpty(path)) { + path = DigestUtil.md5Hex(content) + '.' + type; + } + // 上传到文件存储器 FileClient client = fileConfigService.getMasterFileClient(); Assert.notNull(client, "客户端(master) 不能为空"); @@ -46,7 +54,7 @@ public class FileServiceImpl implements FileService { file.setConfigId(client.getId()); file.setPath(path); file.setUrl(url); - file.setType(FileTypeUtil.getType(new ByteArrayInputStream(content))); + file.setType(type); file.setSize(content.length); fileMapper.insert(file); return url; diff --git a/yudao-module-member/yudao-module-member-api/src/main/java/cn/iocoder/yudao/module/member/enums/ErrorCodeConstants.java b/yudao-module-member/yudao-module-member-api/src/main/java/cn/iocoder/yudao/module/member/enums/ErrorCodeConstants.java index 9970c1ade..8b4380ca1 100644 --- a/yudao-module-member/yudao-module-member-api/src/main/java/cn/iocoder/yudao/module/member/enums/ErrorCodeConstants.java +++ b/yudao-module-member/yudao-module-member-api/src/main/java/cn/iocoder/yudao/module/member/enums/ErrorCodeConstants.java @@ -17,7 +17,6 @@ public interface ErrorCodeConstants { // ========== AUTH 模块 1004003000 ========== ErrorCode AUTH_LOGIN_BAD_CREDENTIALS = new ErrorCode(1004003000, "登录失败,账号密码不正确"); ErrorCode AUTH_LOGIN_USER_DISABLED = new ErrorCode(1004003001, "登录失败,账号被禁用"); - ErrorCode AUTH_LOGIN_FAIL_UNKNOWN = new ErrorCode(1004003002, "登录失败"); // 登录失败的兜底,未知原因 ErrorCode AUTH_TOKEN_EXPIRED = new ErrorCode(1004003004, "Token 已经过期"); ErrorCode AUTH_THIRD_LOGIN_NOT_BIND = new ErrorCode(1004003005, "未绑定账号,需要进行绑定"); diff --git a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/app/auth/AppAuthController.http b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/app/auth/AppAuthController.http index 7a10c7754..b1704f55d 100644 --- a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/app/auth/AppAuthController.http +++ b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/app/auth/AppAuthController.http @@ -1,5 +1,5 @@ ### 请求 /login 接口 => 成功 -POST {{appApi}}/member/login +POST {{appApi}}/member/auth/login Content-Type: application/json tenant-id: {{appTenentId}} @@ -19,7 +19,7 @@ tenant-id: {{appTenentId}} } ### 请求 /sms-login 接口 => 成功 -POST {{appApi}}/member/sms-login +POST {{appApi}}/member/auth/sms-login Content-Type: application/json tenant-id: {{appTenentId}} @@ -29,7 +29,12 @@ tenant-id: {{appTenentId}} } ### 请求 /logout 接口 => 成功 -POST {{appApi}}/member/logout +POST {{appApi}}/member/auth/logout Content-Type: application/json Authorization: Bearer c1b76bdaf2c146c581caa4d7fd81ee66 tenant-id: {{appTenentId}} + +### 请求 /auth/refresh-token 接口 => 成功 +POST {{appApi}}/member/auth/refresh-token?refreshToken=bc43d929094849a28b3a69f6e6940d70 +Content-Type: application/json +tenant-id: {{appTenentId}} diff --git a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/app/auth/AppAuthController.java b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/app/auth/AppAuthController.java index 3a78b3977..6532f7aca 100644 --- a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/app/auth/AppAuthController.java +++ b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/app/auth/AppAuthController.java @@ -1,7 +1,11 @@ package cn.iocoder.yudao.module.member.controller.app.auth; +import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog; +import cn.iocoder.yudao.framework.security.config.SecurityProperties; import cn.iocoder.yudao.framework.security.core.annotations.PreAuthenticated; +import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils; import cn.iocoder.yudao.module.member.controller.app.auth.vo.*; import cn.iocoder.yudao.module.member.service.auth.MemberAuthService; import io.swagger.annotations.Api; @@ -13,11 +17,10 @@ import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import javax.annotation.Resource; +import javax.servlet.http.HttpServletRequest; import javax.validation.Valid; import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; -import static cn.iocoder.yudao.framework.common.util.servlet.ServletUtils.getClientIP; -import static cn.iocoder.yudao.framework.common.util.servlet.ServletUtils.getUserAgent; import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; @Api(tags = "用户 APP - 认证") @@ -30,19 +33,39 @@ public class AppAuthController { @Resource private MemberAuthService authService; + @Resource + private SecurityProperties securityProperties; + @PostMapping("/login") @ApiOperation("使用手机 + 密码登录") public CommonResult login(@RequestBody @Valid AppAuthLoginReqVO reqVO) { - String token = authService.login(reqVO, getClientIP(), getUserAgent()); - return success(AppAuthLoginRespVO.builder().token(token).build()); + return success(authService.login(reqVO)); } + @PostMapping("/logout") + @ApiOperation("登出系统") + public CommonResult logout(HttpServletRequest request) { + String token = SecurityFrameworkUtils.obtainAuthorization(request, securityProperties.getTokenHeader()); + if (StrUtil.isNotBlank(token)) { + authService.logout(token); + } + return success(true); + } + + @PostMapping("/refresh-token") + @ApiOperation("刷新令牌") + @ApiImplicitParam(name = "refreshToken", value = "刷新令牌", required = true, dataTypeClass = String.class) + @OperateLog(enable = false) // 避免 Post 请求被记录操作日志 + public CommonResult refreshToken(@RequestParam("refreshToken") String refreshToken) { + return success(authService.refreshToken(refreshToken)); + } + + // ========== 短信登录相关 ========== + @PostMapping("/sms-login") @ApiOperation("使用手机 + 验证码登录") public CommonResult smsLogin(@RequestBody @Valid AppAuthSmsLoginReqVO reqVO) { - String token = authService.smsLogin(reqVO, getClientIP(), getUserAgent()); - // 返回结果 - return success(AppAuthLoginRespVO.builder().token(token).build()); + return success(authService.smsLogin(reqVO)); } @PostMapping("/send-sms-code") @@ -83,16 +106,14 @@ public class AppAuthController { @PostMapping("/social-quick-login") @ApiOperation(value = "社交快捷登录,使用 code 授权码", notes = "适合未登录的用户,但是社交账号已绑定用户") - public CommonResult socialLogin(@RequestBody @Valid AppAuthSocialQuickLoginReqVO reqVO) { - String token = authService.socialQuickLogin(reqVO, getClientIP(), getUserAgent()); - return success(AppAuthLoginRespVO.builder().token(token).build()); + public CommonResult socialQuickLogin(@RequestBody @Valid AppAuthSocialQuickLoginReqVO reqVO) { + return success(authService.socialQuickLogin(reqVO)); } @PostMapping("/social-bind-login") @ApiOperation(value = "社交绑定登录,使用 手机号 + 手机验证码", notes = "适合未登录的用户,进行登录 + 绑定") - public CommonResult socialLogin2(@RequestBody @Valid AppAuthSocialBindLoginReqVO reqVO) { - String token = authService.socialBindLogin(reqVO, getClientIP(), getUserAgent()); - return success(AppAuthLoginRespVO.builder().token(token).build()); + public CommonResult socialBindLogin(@RequestBody @Valid AppAuthSocialBindLoginReqVO reqVO) { + return success(authService.socialBindLogin(reqVO)); } } diff --git a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/app/auth/vo/AppAuthLoginRespVO.java b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/app/auth/vo/AppAuthLoginRespVO.java index 07ad43118..a63373071 100644 --- a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/app/auth/vo/AppAuthLoginRespVO.java +++ b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/app/auth/vo/AppAuthLoginRespVO.java @@ -7,14 +7,25 @@ import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; -@ApiModel("用户 APP - 手机密码登录 Response VO") +import java.util.Date; + +@ApiModel("用户 APP - 登录 Response VO") @Data @NoArgsConstructor @AllArgsConstructor @Builder public class AppAuthLoginRespVO { - @ApiModelProperty(value = "token", required = true, example = "yudaoyuanma") - private String token; + @ApiModelProperty(value = "用户编号", required = true, example = "1024") + private Long userId; + + @ApiModelProperty(value = "访问令牌", required = true, example = "happy") + private String accessToken; + + @ApiModelProperty(value = "刷新令牌", required = true, example = "nice") + private String refreshToken; + + @ApiModelProperty(value = "过期时间", required = true) + private Date expiresTime; } diff --git a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/convert/auth/AuthConvert.java b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/convert/auth/AuthConvert.java index a15555df1..5a44d76bd 100644 --- a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/convert/auth/AuthConvert.java +++ b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/convert/auth/AuthConvert.java @@ -1,17 +1,14 @@ package cn.iocoder.yudao.module.member.convert.auth; -import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; -import cn.iocoder.yudao.framework.security.core.LoginUser; import cn.iocoder.yudao.module.member.controller.app.auth.vo.*; import cn.iocoder.yudao.module.member.controller.app.social.vo.AppSocialUserUnbindReqVO; -import cn.iocoder.yudao.module.member.dal.dataobject.user.MemberUserDO; +import cn.iocoder.yudao.module.system.api.auth.dto.OAuth2AccessTokenRespDTO; import cn.iocoder.yudao.module.system.api.sms.dto.code.SmsCodeSendReqDTO; import cn.iocoder.yudao.module.system.api.sms.dto.code.SmsCodeUseReqDTO; import cn.iocoder.yudao.module.system.api.social.dto.SocialUserBindReqDTO; import cn.iocoder.yudao.module.system.api.social.dto.SocialUserUnbindReqDTO; import cn.iocoder.yudao.module.system.enums.sms.SmsSceneEnum; import org.mapstruct.Mapper; -import org.mapstruct.Mapping; import org.mapstruct.factory.Mappers; @Mapper @@ -19,14 +16,6 @@ public interface AuthConvert { AuthConvert INSTANCE = Mappers.getMapper(AuthConvert.class); - @Mapping(source = "mobile", target = "username") - LoginUser convert0(MemberUserDO bean); - - default LoginUser convert(MemberUserDO bean) { - // 目的,为了设置 UserTypeEnum.MEMBER.getValue() - return convert0(bean).setUserType(UserTypeEnum.MEMBER.getValue()); - } - SocialUserBindReqDTO convert(Long userId, Integer userType, AppAuthSocialBindLoginReqVO reqVO); SocialUserBindReqDTO convert(Long userId, Integer userType, AppAuthSocialQuickLoginReqVO reqVO); SocialUserUnbindReqDTO convert(Long userId, Integer userType, AppSocialUserUnbindReqVO reqVO); @@ -35,4 +24,6 @@ public interface AuthConvert { SmsCodeUseReqDTO convert(AppAuthResetPasswordReqVO reqVO, SmsSceneEnum scene, String usedIp); SmsCodeUseReqDTO convert(AppAuthSmsLoginReqVO reqVO, Integer scene, String usedIp); + AppAuthLoginRespVO convert(OAuth2AccessTokenRespDTO bean); + } diff --git a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/framework/package-info.java b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/framework/package-info.java new file mode 100644 index 000000000..02d61331a --- /dev/null +++ b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/framework/package-info.java @@ -0,0 +1,6 @@ +/** + * 属于 system 模块的 framework 封装 + * + * @author 芋道源码 + */ +package cn.iocoder.yudao.module.member.framework; diff --git a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/framework/security/config/SecurityConfiguration.java b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/framework/security/config/SecurityConfiguration.java new file mode 100644 index 000000000..c284b35d6 --- /dev/null +++ b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/framework/security/config/SecurityConfiguration.java @@ -0,0 +1,28 @@ +package cn.iocoder.yudao.module.member.framework.security.config; + +import cn.iocoder.yudao.framework.security.config.AuthorizeRequestsCustomizer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer; + +/** + * Member 模块的 Security 配置 + */ +@Configuration("memberSecurityConfiguration") +public class SecurityConfiguration { + + @Bean("memberAuthorizeRequestsCustomizer") + public AuthorizeRequestsCustomizer authorizeRequestsCustomizer() { + return new AuthorizeRequestsCustomizer() { + + @Override + public void customize(ExpressionUrlAuthorizationConfigurer.ExpressionInterceptUrlRegistry registry) { + // 登录的接口 + registry.antMatchers(buildAdminApi("/member/auth/logout")).permitAll(); + } + + }; + } + +} diff --git a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/framework/security/core/package-info.java b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/framework/security/core/package-info.java new file mode 100644 index 000000000..3abf5630f --- /dev/null +++ b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/framework/security/core/package-info.java @@ -0,0 +1,4 @@ +/** + * 占位 + */ +package cn.iocoder.yudao.module.member.framework.security.core; diff --git a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/auth/MemberAuthService.java b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/auth/MemberAuthService.java index f7aae6ed8..ded192217 100644 --- a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/auth/MemberAuthService.java +++ b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/auth/MemberAuthService.java @@ -1,6 +1,5 @@ package cn.iocoder.yudao.module.member.service.auth; -import cn.iocoder.yudao.framework.security.core.service.SecurityAuthFrameworkService; import cn.iocoder.yudao.module.member.controller.app.auth.vo.*; import javax.validation.Valid; @@ -12,47 +11,46 @@ import javax.validation.Valid; * * @author 芋道源码 */ -public interface MemberAuthService extends SecurityAuthFrameworkService { +public interface MemberAuthService { /** * 手机 + 密码登录 * * @param reqVO 登录信息 - * @param userIp 用户 IP - * @param userAgent 用户 UA - * @return 身份令牌,使用 JWT 方式 + * @return 登录结果 */ - String login(@Valid AppAuthLoginReqVO reqVO, String userIp, String userAgent); + AppAuthLoginRespVO login(@Valid AppAuthLoginReqVO reqVO); + + /** + * 基于 token 退出登录 + * + * @param token token + */ + void logout(String token); /** * 手机 + 验证码登陆 * * @param reqVO 登陆信息 - * @param userIp 用户 IP - * @param userAgent 用户 UA - * @return 身份令牌,使用 JWT 方式 + * @return 登录结果 */ - String smsLogin(@Valid AppAuthSmsLoginReqVO reqVO, String userIp, String userAgent); + AppAuthLoginRespVO smsLogin(@Valid AppAuthSmsLoginReqVO reqVO); /** * 社交登录,使用 code 授权码 * * @param reqVO 登录信息 - * @param userIp 用户 IP - * @param userAgent 用户 UA - * @return 身份令牌,使用 JWT 方式 + * @return 登录结果 */ - String socialQuickLogin(@Valid AppAuthSocialQuickLoginReqVO reqVO, String userIp, String userAgent); + AppAuthLoginRespVO socialQuickLogin(@Valid AppAuthSocialQuickLoginReqVO reqVO); /** * 社交登录,使用 手机号 + 手机验证码 * * @param reqVO 登录信息 - * @param userIp 用户 IP - * @param userAgent 用户 UA - * @return 身份令牌,使用 JWT 方式 + * @return 登录结果 */ - String socialBindLogin(@Valid AppAuthSocialBindLoginReqVO reqVO, String userIp, String userAgent); + AppAuthLoginRespVO socialBindLogin(@Valid AppAuthSocialBindLoginReqVO reqVO); /** * 获得社交认证 URL @@ -84,4 +82,11 @@ public interface MemberAuthService extends SecurityAuthFrameworkService { */ void sendSmsCode(Long userId, AppAuthSmsSendReqVO reqVO); + /** + * 刷新访问令牌 + * + * @param refreshToken 刷新令牌 + * @return 登录结果 + */ + AppAuthLoginRespVO refreshToken(String refreshToken); } diff --git a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/auth/MemberAuthServiceImpl.java b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/auth/MemberAuthServiceImpl.java index 5c2788df9..9fc86335e 100644 --- a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/auth/MemberAuthServiceImpl.java +++ b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/auth/MemberAuthServiceImpl.java @@ -1,35 +1,29 @@ package cn.iocoder.yudao.module.member.service.auth; import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.ObjectUtil; import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; import cn.iocoder.yudao.framework.common.util.monitor.TracerUtils; import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils; -import cn.iocoder.yudao.framework.security.core.LoginUser; -import cn.iocoder.yudao.framework.security.core.authentication.MultiUsernamePasswordAuthenticationToken; import cn.iocoder.yudao.module.member.controller.app.auth.vo.*; import cn.iocoder.yudao.module.member.convert.auth.AuthConvert; import cn.iocoder.yudao.module.member.dal.dataobject.user.MemberUserDO; import cn.iocoder.yudao.module.member.dal.mysql.user.MemberUserMapper; import cn.iocoder.yudao.module.member.service.user.MemberUserService; -import cn.iocoder.yudao.module.system.api.auth.UserSessionApi; +import cn.iocoder.yudao.module.system.api.auth.OAuth2TokenApi; +import cn.iocoder.yudao.module.system.api.auth.dto.OAuth2AccessTokenCreateReqDTO; +import cn.iocoder.yudao.module.system.api.auth.dto.OAuth2AccessTokenRespDTO; 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.sms.SmsCodeApi; 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.logger.LoginLogTypeEnum; import cn.iocoder.yudao.module.system.enums.logger.LoginResultEnum; import cn.iocoder.yudao.module.system.enums.sms.SmsSceneEnum; import com.google.common.annotations.VisibleForTesting; import lombok.extern.slf4j.Slf4j; -import org.springframework.context.annotation.Lazy; -import org.springframework.security.authentication.AuthenticationManager; -import org.springframework.security.authentication.BadCredentialsException; -import org.springframework.security.authentication.DisabledException; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.AuthenticationException; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -50,10 +44,6 @@ import static cn.iocoder.yudao.module.member.enums.ErrorCodeConstants.*; @Slf4j public class MemberAuthServiceImpl implements MemberAuthService { - @Resource - @Lazy // 延迟加载,因为存在相互依赖的问题 - private AuthenticationManager authenticationManager; - @Resource private MemberUserService userService; @Resource @@ -61,9 +51,9 @@ public class MemberAuthServiceImpl implements MemberAuthService { @Resource private LoginLogApi loginLogApi; @Resource - private UserSessionApi userSessionApi; - @Resource private SocialUserApi socialUserApi; + @Resource + private OAuth2TokenApi oauth2TokenApi; @Resource private PasswordEncoder passwordEncoder; @@ -71,44 +61,31 @@ public class MemberAuthServiceImpl implements MemberAuthService { private MemberUserMapper userMapper; @Override - public UserDetails loadUserByUsername(String mobile) throws UsernameNotFoundException { - // 获取 username 对应的 SysUserDO - MemberUserDO user = userService.getUserByMobile(mobile); - if (user == null) { - throw new UsernameNotFoundException(mobile); - } - // 创建 LoginUser 对象 - return AuthConvert.INSTANCE.convert(user); - } - - @Override - public String login(AppAuthLoginReqVO reqVO, String userIp, String userAgent) { + public AppAuthLoginRespVO login(AppAuthLoginReqVO reqVO) { // 使用手机 + 密码,进行登录。 - LoginUser loginUser = this.login0(reqVO.getMobile(), reqVO.getPassword()); + MemberUserDO user = login0(reqVO.getMobile(), reqVO.getPassword()); - // 缓存登录用户到 Redis 中,返回 Token 令牌 - return createUserSessionAfterLoginSuccess(loginUser, LoginLogTypeEnum.LOGIN_USERNAME, userIp, userAgent); + // 创建 Token 令牌,记录登录日志 + return createTokenAfterLoginSuccess(user, reqVO.getMobile(), LoginLogTypeEnum.LOGIN_MOBILE); } @Override @Transactional - public String smsLogin(AppAuthSmsLoginReqVO reqVO, String userIp, String userAgent) { + public AppAuthLoginRespVO smsLogin(AppAuthSmsLoginReqVO reqVO) { // 校验验证码 + String userIp = getClientIP(); smsCodeApi.useSmsCode(AuthConvert.INSTANCE.convert(reqVO, SmsSceneEnum.MEMBER_LOGIN.getScene(), userIp)); // 获得获得注册用户 MemberUserDO user = userService.createUserIfAbsent(reqVO.getMobile(), userIp); Assert.notNull(user, "获取用户失败,结果为空"); - // 执行登陆 - LoginUser loginUser = AuthConvert.INSTANCE.convert(user); - - // 缓存登录用户到 Redis 中,返回 Token 令牌 - return createUserSessionAfterLoginSuccess(loginUser, LoginLogTypeEnum.LOGIN_SMS, userIp, userAgent); + // 创建 Token 令牌,记录登录日志 + return createTokenAfterLoginSuccess(user, reqVO.getMobile(), LoginLogTypeEnum.LOGIN_SMS); } @Override - public String socialQuickLogin(AppAuthSocialQuickLoginReqVO reqVO, String userIp, String userAgent) { + public AppAuthLoginRespVO socialQuickLogin(AppAuthSocialQuickLoginReqVO reqVO) { // 使用 code 授权码,进行登录。然后,获得到绑定的用户编号 Long userId = socialUserApi.getBindUserId(UserTypeEnum.MEMBER.getValue(), reqVO.getType(), reqVO.getCode(), reqVO.getState()); @@ -122,31 +99,30 @@ public class MemberAuthServiceImpl implements MemberAuthService { throw exception(USER_NOT_EXISTS); } - // 创建 LoginUser 对象 - LoginUser loginUser = AuthConvert.INSTANCE.convert(user); - - // 缓存登录用户到 Redis 中,返回 Token 令牌 - return createUserSessionAfterLoginSuccess(loginUser, LoginLogTypeEnum.LOGIN_SOCIAL, userIp, userAgent); + // 创建 Token 令牌,记录登录日志 + return createTokenAfterLoginSuccess(user, null, LoginLogTypeEnum.LOGIN_SOCIAL); } @Override - public String socialBindLogin(AppAuthSocialBindLoginReqVO reqVO, String userIp, String userAgent) { + public AppAuthLoginRespVO socialBindLogin(AppAuthSocialBindLoginReqVO reqVO) { // 使用手机号、手机验证码登录 AppAuthSmsLoginReqVO loginReqVO = AppAuthSmsLoginReqVO.builder() .mobile(reqVO.getMobile()).code(reqVO.getSmsCode()).build(); - String token = this.smsLogin(loginReqVO, userIp, userAgent); - LoginUser loginUser = userSessionApi.getLoginUser(token); + AppAuthLoginRespVO token = smsLogin(loginReqVO); // 绑定社交用户 - socialUserApi.bindSocialUser(AuthConvert.INSTANCE.convert(loginUser.getId(), getUserType().getValue(), reqVO)); + socialUserApi.bindSocialUser(AuthConvert.INSTANCE.convert(token.getUserId(), getUserType().getValue(), reqVO)); return token; } - private String createUserSessionAfterLoginSuccess(LoginUser loginUser, LoginLogTypeEnum logType, String userIp, String userAgent) { + private AppAuthLoginRespVO createTokenAfterLoginSuccess(MemberUserDO user, String mobile, LoginLogTypeEnum logType) { // 插入登陆日志 - createLoginLog(loginUser.getUsername(), logType, LoginResultEnum.SUCCESS); - // 缓存登录用户到 Redis 中,返回 Token 令牌 - return userSessionApi.createUserSession(loginUser, userIp, userAgent); + createLoginLog(user.getId(), mobile, logType, LoginResultEnum.SUCCESS); + // 创建 Token 令牌 + OAuth2AccessTokenRespDTO accessTokenRespDTO = oauth2TokenApi.createAccessToken(new OAuth2AccessTokenCreateReqDTO() + .setUserId(user.getId()).setUserType(getUserType().getValue()).setClientId(OAuth2ClientIdEnum.DEFAULT.getId())); + // 构建返回结果 + return AuthConvert.INSTANCE.convert(accessTokenRespDTO); } @Override @@ -154,40 +130,32 @@ public class MemberAuthServiceImpl implements MemberAuthService { return socialUserApi.getAuthorizeUrl(type, redirectUri); } - private LoginUser login0(String username, String password) { - final LoginLogTypeEnum logType = LoginLogTypeEnum.LOGIN_USERNAME; - // 用户验证 - Authentication authentication; - try { - // 调用 Spring Security 的 AuthenticationManager#authenticate(...) 方法,使用账号密码进行认证 - // 在其内部,会调用到 loadUserByUsername 方法,获取 User 信息 - authentication = authenticationManager.authenticate(new MultiUsernamePasswordAuthenticationToken( - username, password, getUserType())); - } catch (BadCredentialsException badCredentialsException) { - this.createLoginLog(username, logType, LoginResultEnum.BAD_CREDENTIALS); + private MemberUserDO login0(String mobile, String password) { + final LoginLogTypeEnum logTypeEnum = LoginLogTypeEnum.LOGIN_MOBILE; + // 校验账号是否存在 + MemberUserDO user = userService.getUserByMobile(mobile); + if (user == null) { + createLoginLog(null, mobile, logTypeEnum, LoginResultEnum.BAD_CREDENTIALS); throw exception(AUTH_LOGIN_BAD_CREDENTIALS); - } catch (DisabledException disabledException) { - this.createLoginLog(username, logType, LoginResultEnum.USER_DISABLED); - throw exception(AUTH_LOGIN_USER_DISABLED); - } catch (AuthenticationException authenticationException) { - log.error("[login0][username({}) 发生未知异常]", username, authenticationException); - this.createLoginLog(username, logType, LoginResultEnum.UNKNOWN_ERROR); - throw exception(AUTH_LOGIN_FAIL_UNKNOWN); } - Assert.notNull(authentication.getPrincipal(), "Principal 不会为空"); - return (LoginUser) authentication.getPrincipal(); + if (!userService.isPasswordMatch(password, user.getPassword())) { + createLoginLog(user.getId(), mobile, logTypeEnum, LoginResultEnum.BAD_CREDENTIALS); + throw exception(AUTH_LOGIN_BAD_CREDENTIALS); + } + // 校验是否禁用 + if (ObjectUtil.notEqual(user.getStatus(), CommonStatusEnum.ENABLE.getStatus())) { + createLoginLog(user.getId(), mobile, logTypeEnum, LoginResultEnum.USER_DISABLED); + throw exception(AUTH_LOGIN_USER_DISABLED); + } + return user; } - private void createLoginLog(String mobile, LoginLogTypeEnum logType, LoginResultEnum loginResult) { - // 获得用户 - MemberUserDO user = userService.getUserByMobile(mobile); + private void createLoginLog(Long userId, String mobile, LoginLogTypeEnum logType, LoginResultEnum loginResult) { // 插入登录日志 LoginLogCreateReqDTO reqDTO = new LoginLogCreateReqDTO(); reqDTO.setLogType(logType.getType()); reqDTO.setTraceId(TracerUtils.getTraceId()); - if (user != null) { - reqDTO.setUserId(user.getId()); - } + reqDTO.setUserId(userId); reqDTO.setUserType(getUserType().getValue()); reqDTO.setUsername(mobile); reqDTO.setUserAgent(ServletUtils.getUserAgent()); @@ -195,72 +163,20 @@ public class MemberAuthServiceImpl implements MemberAuthService { reqDTO.setResult(loginResult.getResult()); loginLogApi.createLoginLog(reqDTO); // 更新最后登录时间 - if (user != null && Objects.equals(LoginResultEnum.SUCCESS.getResult(), loginResult.getResult())) { - userService.updateUserLogin(user.getId(), getClientIP()); + if (userId != null && Objects.equals(LoginResultEnum.SUCCESS.getResult(), loginResult.getResult())) { + userService.updateUserLogin(userId, getClientIP()); } } - @Override - public LoginUser verifyTokenAndRefresh(String token) { - // 获得 LoginUser - LoginUser loginUser = userSessionApi.getLoginUser(token); - if (loginUser == null) { - return null; - } - // 刷新 LoginUser 缓存 - this.refreshLoginUserCache(token, loginUser); - return loginUser; - } - - private void refreshLoginUserCache(String token, LoginUser loginUser) { - // 每 1/3 的 Session 超时时间,刷新 LoginUser 缓存 - if (System.currentTimeMillis() - loginUser.getUpdateTime().getTime() < - userSessionApi.getSessionTimeoutMillis() / 3) { - return; - } - - // 重新加载 UserDO 信息 - MemberUserDO user = userService.getUser(loginUser.getId()); - if (user == null || CommonStatusEnum.DISABLE.getStatus().equals(user.getStatus())) { - // 校验 token 时,用户被禁用的情况下,也认为 token 过期,方便前端跳转到登录界面 - throw exception(AUTH_TOKEN_EXPIRED); - } - - // 刷新 LoginUser 缓存 - userSessionApi.refreshUserSession(token, loginUser); - } - - @Override - public LoginUser mockLogin(Long userId) { - // 获取用户编号对应的 UserDO - MemberUserDO user = userService.getUser(userId); - if (user == null) { - throw new UsernameNotFoundException(String.valueOf(userId)); - } - - // 执行登陆 - this.createLoginLog(user.getMobile(), LoginLogTypeEnum.LOGIN_MOCK, LoginResultEnum.SUCCESS); - - // 创建 LoginUser 对象 - return AuthConvert.INSTANCE.convert(user); - } - @Override public void logout(String token) { - // 查询用户信息 - LoginUser loginUser = userSessionApi.getLoginUser(token); - if (loginUser == null) { + // 删除访问令牌 + OAuth2AccessTokenRespDTO accessTokenRespDTO = oauth2TokenApi.removeAccessToken(token); + if (accessTokenRespDTO == null) { return; } - // 删除 session - userSessionApi.deleteUserSession(token); - // 记录登出日志 - this.createLogoutLog(loginUser.getId(), loginUser.getUsername()); - } - - @Override - public UserTypeEnum getUserType() { - return UserTypeEnum.MEMBER; + // 删除成功,则记录登出日志 + createLogoutLog(accessTokenRespDTO.getUserId()); } @Override @@ -269,6 +185,7 @@ public class MemberAuthServiceImpl implements MemberAuthService { MemberUserDO userDO = checkOldPassword(userId, reqVO.getOldPassword()); // 更新用户密码 + // TODO 芋艿:需要重构到用户模块 userMapper.updateById(MemberUserDO.builder().id(userDO.getId()) .password(passwordEncoder.encode(reqVO.getPassword())).build()); } @@ -293,6 +210,12 @@ public class MemberAuthServiceImpl implements MemberAuthService { smsCodeApi.sendSmsCode(AuthConvert.INSTANCE.convert(reqVO).setCreateIp(getClientIP())); } + @Override + public AppAuthLoginRespVO refreshToken(String refreshToken) { + OAuth2AccessTokenRespDTO accessTokenDO = oauth2TokenApi.refreshAccessToken(refreshToken, OAuth2ClientIdEnum.DEFAULT.getId()); + return AuthConvert.INSTANCE.convert(accessTokenDO); + } + /** * 校验旧密码 * @@ -321,17 +244,29 @@ public class MemberAuthServiceImpl implements MemberAuthService { return user; } - private void createLogoutLog(Long userId, String username) { + private void createLogoutLog(Long userId) { LoginLogCreateReqDTO reqDTO = new LoginLogCreateReqDTO(); reqDTO.setLogType(LoginLogTypeEnum.LOGOUT_SELF.getType()); reqDTO.setTraceId(TracerUtils.getTraceId()); reqDTO.setUserId(userId); reqDTO.setUserType(getUserType().getValue()); - reqDTO.setUsername(username); + reqDTO.setUsername(getMobile(userId)); reqDTO.setUserAgent(ServletUtils.getUserAgent()); reqDTO.setUserIp(getClientIP()); reqDTO.setResult(LoginResultEnum.SUCCESS.getResult()); loginLogApi.createLoginLog(reqDTO); } + private String getMobile(Long userId) { + if (userId == null) { + return null; + } + MemberUserDO user = userService.getUser(userId); + return user != null ? user.getMobile() : null; + } + + private UserTypeEnum getUserType() { + return UserTypeEnum.MEMBER; + } + } diff --git a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/user/MemberUserService.java b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/user/MemberUserService.java index 3c0e62de8..3b63c8d71 100644 --- a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/user/MemberUserService.java +++ b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/user/MemberUserService.java @@ -69,4 +69,13 @@ public interface MemberUserService { */ void updateUserMobile(Long userId, AppUserUpdateMobileReqVO reqVO); + /** + * 判断密码是否匹配 + * + * @param rawPassword 未加密的密码 + * @param encodedPassword 加密后的密码 + * @return 是否匹配 + */ + boolean isPasswordMatch(String rawPassword, String encodedPassword); + } diff --git a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/user/MemberUserServiceImpl.java b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/user/MemberUserServiceImpl.java index 0522e8eda..ff91b0781 100644 --- a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/user/MemberUserServiceImpl.java +++ b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/user/MemberUserServiceImpl.java @@ -69,7 +69,7 @@ public class MemberUserServiceImpl implements MemberUserService { MemberUserDO user = new MemberUserDO(); user.setMobile(mobile); user.setStatus(CommonStatusEnum.ENABLE.getStatus()); // 默认开启 - user.setPassword(passwordEncoder.encode(password)); // 加密密码 + user.setPassword(encodePassword(password)); // 加密密码 user.setRegisterIp(registerIp); memberUserMapper.insert(user); return user; @@ -127,6 +127,21 @@ public class MemberUserServiceImpl implements MemberUserService { memberUserMapper.updateById(MemberUserDO.builder().id(userId).mobile(reqVO.getMobile()).build()); } + @Override + public boolean isPasswordMatch(String rawPassword, String encodedPassword) { + return passwordEncoder.matches(rawPassword, encodedPassword); + } + + /** + * 对密码进行加密 + * + * @param password 密码 + * @return 加密后的密码 + */ + private String encodePassword(String password) { + return passwordEncoder.encode(password); + } + @VisibleForTesting public MemberUserDO checkUserExists(Long id) { if (id == null) { diff --git a/yudao-module-member/yudao-module-member-biz/src/test/java/cn/iocoder/yudao/module/member/service/auth/MemberAuthServiceTest.java b/yudao-module-member/yudao-module-member-biz/src/test/java/cn/iocoder/yudao/module/member/service/auth/MemberAuthServiceTest.java index a982f9625..7218e6f1f 100644 --- a/yudao-module-member/yudao-module-member-biz/src/test/java/cn/iocoder/yudao/module/member/service/auth/MemberAuthServiceTest.java +++ b/yudao-module-member/yudao-module-member-biz/src/test/java/cn/iocoder/yudao/module/member/service/auth/MemberAuthServiceTest.java @@ -9,14 +9,13 @@ import cn.iocoder.yudao.module.member.controller.app.auth.vo.AppAuthUpdatePasswo import cn.iocoder.yudao.module.member.dal.dataobject.user.MemberUserDO; import cn.iocoder.yudao.module.member.dal.mysql.user.MemberUserMapper; import cn.iocoder.yudao.module.member.service.user.MemberUserService; -import cn.iocoder.yudao.module.system.api.auth.UserSessionApi; +import cn.iocoder.yudao.module.system.api.auth.OAuth2TokenApi; import cn.iocoder.yudao.module.system.api.logger.LoginLogApi; import cn.iocoder.yudao.module.system.api.sms.SmsCodeApi; import cn.iocoder.yudao.module.system.api.social.SocialUserApi; import org.junit.jupiter.api.Test; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.context.annotation.Import; -import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.crypto.password.PasswordEncoder; import javax.annotation.Resource; @@ -38,8 +37,8 @@ import static org.mockito.Mockito.when; @Import({MemberAuthServiceImpl.class, YudaoRedisAutoConfiguration.class}) public class MemberAuthServiceTest extends BaseDbAndRedisUnitTest { - @MockBean - private AuthenticationManager authenticationManager; + // TODO @芋艿:登录相关的单测,待补全 + @MockBean private MemberUserService userService; @MockBean @@ -47,7 +46,7 @@ public class MemberAuthServiceTest extends BaseDbAndRedisUnitTest { @MockBean private LoginLogApi loginLogApi; @MockBean - private UserSessionApi userSessionApi; + private OAuth2TokenApi oauth2TokenApi; @MockBean private SocialUserApi socialUserApi; @MockBean diff --git a/yudao-module-system/yudao-module-system-api/pom.xml b/yudao-module-system/yudao-module-system-api/pom.xml index 81b129916..655db05a7 100644 --- a/yudao-module-system/yudao-module-system-api/pom.xml +++ b/yudao-module-system/yudao-module-system-api/pom.xml @@ -29,13 +29,6 @@ true - - - cn.iocoder.boot - yudao-spring-boot-starter-security - true - - diff --git a/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/auth/OAuth2TokenApi.java b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/auth/OAuth2TokenApi.java new file mode 100644 index 000000000..ef82f5f53 --- /dev/null +++ b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/auth/OAuth2TokenApi.java @@ -0,0 +1,49 @@ +package cn.iocoder.yudao.module.system.api.auth; + +import cn.iocoder.yudao.module.system.api.auth.dto.OAuth2AccessTokenCheckRespDTO; +import cn.iocoder.yudao.module.system.api.auth.dto.OAuth2AccessTokenCreateReqDTO; +import cn.iocoder.yudao.module.system.api.auth.dto.OAuth2AccessTokenRespDTO; + +import javax.validation.Valid; + +/** + * OAuth2.0 Token API 接口 + * + * @author 芋道源码 + */ +public interface OAuth2TokenApi { + + /** + * 创建访问令牌 + * + * @param reqDTO 访问令牌的创建信息 + * @return 访问令牌的信息 + */ + OAuth2AccessTokenRespDTO createAccessToken(@Valid OAuth2AccessTokenCreateReqDTO reqDTO); + + /** + * 校验访问令牌 + * + * @param accessToken 访问令牌 + * @return 访问令牌的信息 + */ + OAuth2AccessTokenCheckRespDTO checkAccessToken(String accessToken); + + /** + * 移除访问令牌 + * + * @param accessToken 访问令牌 + * @return 访问令牌的信息 + */ + OAuth2AccessTokenRespDTO removeAccessToken(String accessToken); + + /** + * 刷新访问令牌 + * + * @param refreshToken 刷新令牌 + * @param clientId 客户端编号 + * @return 访问令牌的信息 + */ + OAuth2AccessTokenRespDTO refreshAccessToken(String refreshToken, Long clientId); + +} diff --git a/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/auth/UserSessionApi.java b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/auth/UserSessionApi.java deleted file mode 100644 index e7fdcb982..000000000 --- a/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/auth/UserSessionApi.java +++ /dev/null @@ -1,56 +0,0 @@ -package cn.iocoder.yudao.module.system.api.auth; - -import cn.iocoder.yudao.framework.security.core.LoginUser; - -import javax.validation.constraints.NotEmpty; -import javax.validation.constraints.NotNull; - -/** - * 在线用户 Session API 接口 - * - * @author 芋道源码 - */ -public interface UserSessionApi { - - /** - * 创建在线用户 Session - * - * @param loginUser 登录用户 - * @param userIp 用户 IP - * @param userAgent 用户 UA - * @return Token 令牌 - */ - String createUserSession(@NotNull(message = "登录用户不能为空") LoginUser loginUser, String userIp, String userAgent); - - /** - * 刷新在线用户 Session 的更新时间 - * - * @param token Token 令牌 - * @param loginUser 登录用户 - */ - void refreshUserSession(@NotEmpty(message = "Token 令牌不能为空") String token, - @NotNull(message = "登录用户不能为空") LoginUser loginUser); - - /** - * 删除在线用户 Session - * - * @param token Token 令牌 - */ - void deleteUserSession(String token); - - /** - * 获得 Token 令牌对应的在线用户 - * - * @param token Token 令牌 - * @return 在线用户 - */ - LoginUser getLoginUser(String token); - - /** - * 获得 Session 超时时间,单位:毫秒 - * - * @return 超时时间 - */ - Long getSessionTimeoutMillis(); - -} diff --git a/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/auth/dto/OAuth2AccessTokenCheckRespDTO.java b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/auth/dto/OAuth2AccessTokenCheckRespDTO.java new file mode 100644 index 000000000..5acdc1ea8 --- /dev/null +++ b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/auth/dto/OAuth2AccessTokenCheckRespDTO.java @@ -0,0 +1,28 @@ +package cn.iocoder.yudao.module.system.api.auth.dto; + +import lombok.Data; + +import java.io.Serializable; + +/** + * OAuth2.0 访问令牌的校验 Response DTO + * + * @author 芋道源码 + */ +@Data +public class OAuth2AccessTokenCheckRespDTO implements Serializable { + + /** + * 用户编号 + */ + private Long userId; + /** + * 用户类型 + */ + private Integer userType; + /** + * 租户编号 + */ + private Long tenantId; + +} diff --git a/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/auth/dto/OAuth2AccessTokenCreateReqDTO.java b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/auth/dto/OAuth2AccessTokenCreateReqDTO.java new file mode 100644 index 000000000..e9c5b953b --- /dev/null +++ b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/auth/dto/OAuth2AccessTokenCreateReqDTO.java @@ -0,0 +1,35 @@ +package cn.iocoder.yudao.module.system.api.auth.dto; + +import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; +import cn.iocoder.yudao.framework.common.validation.InEnum; +import lombok.Data; + +import javax.validation.constraints.NotNull; +import java.io.Serializable; + +/** + * OAuth2.0 访问令牌创建 Request DTO + * + * @author 芋道源码 + */ +@Data +public class OAuth2AccessTokenCreateReqDTO implements Serializable { + + /** + * 用户编号 + */ + @NotNull(message = "用户编号不能为空") + private Long userId; + /** + * 用户类型 + */ + @NotNull(message = "用户类型不能为空") + @InEnum(value = UserTypeEnum.class, message = "用户类型必须是 {value}") + private Integer userType; + /** + * 客户端编号 + */ + @NotNull(message = "客户端编号不能为空") + private Long clientId; + +} diff --git a/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/auth/dto/OAuth2AccessTokenRespDTO.java b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/auth/dto/OAuth2AccessTokenRespDTO.java new file mode 100644 index 000000000..768955594 --- /dev/null +++ b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/auth/dto/OAuth2AccessTokenRespDTO.java @@ -0,0 +1,39 @@ +package cn.iocoder.yudao.module.system.api.auth.dto; + +import lombok.Data; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.util.Date; + +/** + * OAuth2.0 访问令牌的信息 Response DTO + * + * @author 芋道源码 + */ +@Data +@Accessors(chain = true) +public class OAuth2AccessTokenRespDTO implements Serializable { + + /** + * 访问令牌 + */ + private String accessToken; + /** + * 刷新令牌 + */ + private String refreshToken; + /** + * 用户编号 + */ + private Long userId; + /** + * 用户类型 + */ + private Integer userType; + /** + * 过期时间 + */ + private Date expiresTime; + +} diff --git a/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/ErrorCodeConstants.java b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/ErrorCodeConstants.java index 34b0c8772..494f6937a 100644 --- a/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/ErrorCodeConstants.java +++ b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/ErrorCodeConstants.java @@ -12,7 +12,6 @@ public interface ErrorCodeConstants { // ========== AUTH 模块 1002000000 ========== ErrorCode AUTH_LOGIN_BAD_CREDENTIALS = new ErrorCode(1002000000, "登录失败,账号密码不正确"); ErrorCode AUTH_LOGIN_USER_DISABLED = new ErrorCode(1002000001, "登录失败,账号被禁用"); - ErrorCode AUTH_LOGIN_FAIL_UNKNOWN = new ErrorCode(1002000002, "登录失败"); // 登录失败的兜底,未知原因 ErrorCode AUTH_LOGIN_CAPTCHA_NOT_FOUND = new ErrorCode(1002000003, "验证码不存在"); ErrorCode AUTH_LOGIN_CAPTCHA_CODE_ERROR = new ErrorCode(1002000004, "验证码不正确"); ErrorCode AUTH_THIRD_LOGIN_NOT_BIND = new ErrorCode(1002000005, "未绑定账号,需要进行绑定"); @@ -120,8 +119,11 @@ public interface ErrorCodeConstants { ErrorCode SOCIAL_USER_UNBIND_NOT_SELF = new ErrorCode(1002018001, "社交解绑失败,非当前用户绑定"); ErrorCode SOCIAL_USER_NOT_FOUND = new ErrorCode(1002018002, "社交授权失败,找不到对应的用户"); - // ========== 系统铭感词 1002019000 ========= + // ========== 系统敏感词 1002019000 ========= ErrorCode SENSITIVE_WORD_NOT_EXISTS = new ErrorCode(1002019000, "系统敏感词在所有标签中都不存在"); ErrorCode SENSITIVE_WORD_EXISTS = new ErrorCode(1002019001, "系统敏感词已在标签中存在"); + // ========== 系统敏感词 1002020000 ========= + ErrorCode OAUTH2_CLIENT_NOT_EXISTS = new ErrorCode(1002020000, "OAuth2 客户端不存在"); + } diff --git a/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/auth/OAuth2ClientIdEnum.java b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/auth/OAuth2ClientIdEnum.java new file mode 100644 index 000000000..d41c2d25e --- /dev/null +++ b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/auth/OAuth2ClientIdEnum.java @@ -0,0 +1,17 @@ +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; + +} diff --git a/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/logger/LoginLogTypeEnum.java b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/logger/LoginLogTypeEnum.java index 2f845fd10..4c51f9168 100644 --- a/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/logger/LoginLogTypeEnum.java +++ b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/logger/LoginLogTypeEnum.java @@ -12,12 +12,10 @@ public enum LoginLogTypeEnum { LOGIN_USERNAME(100), // 使用账号登录 LOGIN_SOCIAL(101), // 使用社交登录 - LOGIN_MOCK(102), // 使用 Mock 登录 LOGIN_MOBILE(103), // 使用手机登陆 LOGIN_SMS(104), // 使用短信登陆 LOGOUT_SELF(200), // 自己主动登出 - LOGOUT_TIMEOUT(201), // 超时登出 LOGOUT_DELETE(202), // 强制退出 ; diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/api/auth/OAuth2TokenApiImpl.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/api/auth/OAuth2TokenApiImpl.java new file mode 100644 index 000000000..80b7b1811 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/api/auth/OAuth2TokenApiImpl.java @@ -0,0 +1,48 @@ +package cn.iocoder.yudao.module.system.api.auth; + +import cn.iocoder.yudao.module.system.api.auth.dto.OAuth2AccessTokenCheckRespDTO; +import cn.iocoder.yudao.module.system.api.auth.dto.OAuth2AccessTokenCreateReqDTO; +import cn.iocoder.yudao.module.system.api.auth.dto.OAuth2AccessTokenRespDTO; +import cn.iocoder.yudao.module.system.convert.auth.OAuth2TokenConvert; +import cn.iocoder.yudao.module.system.dal.dataobject.auth.OAuth2AccessTokenDO; +import cn.iocoder.yudao.module.system.service.auth.OAuth2TokenService; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; + +/** + * OAuth2.0 Token API 实现类 + * + * @author 芋道源码 + */ +@Service +public class OAuth2TokenApiImpl implements OAuth2TokenApi { + + @Resource + private OAuth2TokenService oauth2TokenService; + + @Override + public OAuth2AccessTokenRespDTO createAccessToken(OAuth2AccessTokenCreateReqDTO reqDTO) { + OAuth2AccessTokenDO accessTokenDO = oauth2TokenService.createAccessToken( + reqDTO.getUserId(), reqDTO.getUserType(), reqDTO.getClientId()); + return OAuth2TokenConvert.INSTANCE.convert2(accessTokenDO); + } + + @Override + public OAuth2AccessTokenCheckRespDTO checkAccessToken(String accessToken) { + return OAuth2TokenConvert.INSTANCE.convert(oauth2TokenService.checkAccessToken(accessToken)); + } + + @Override + public OAuth2AccessTokenRespDTO removeAccessToken(String accessToken) { + OAuth2AccessTokenDO accessTokenDO = oauth2TokenService.removeAccessToken(accessToken); + return OAuth2TokenConvert.INSTANCE.convert2(accessTokenDO); + } + + @Override + public OAuth2AccessTokenRespDTO refreshAccessToken(String refreshToken, Long clientId) { + OAuth2AccessTokenDO accessTokenDO = oauth2TokenService.refreshAccessToken(refreshToken, clientId); + return OAuth2TokenConvert.INSTANCE.convert2(accessTokenDO); + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/api/auth/UserSessionApiImpl.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/api/auth/UserSessionApiImpl.java deleted file mode 100644 index 63226c125..000000000 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/api/auth/UserSessionApiImpl.java +++ /dev/null @@ -1,47 +0,0 @@ -package cn.iocoder.yudao.module.system.api.auth; - -import cn.iocoder.yudao.framework.security.core.LoginUser; -import cn.iocoder.yudao.module.system.service.auth.UserSessionService; -import org.springframework.stereotype.Service; -import org.springframework.validation.annotation.Validated; - -import javax.annotation.Resource; - -/** - * 在线用户 Session API 实现类 - * - * @author 芋道源码 - */ -@Service -@Validated -public class UserSessionApiImpl implements UserSessionApi { - - @Resource - private UserSessionService userSessionService; - - @Override - public String createUserSession(LoginUser loginUser, String userIp, String userAgent) { - return userSessionService.createUserSession(loginUser, userIp, userAgent); - } - - @Override - public void refreshUserSession(String token, LoginUser loginUser) { - userSessionService.refreshUserSession(token, loginUser); - } - - @Override - public void deleteUserSession(String token) { - userSessionService.deleteUserSession(token); - } - - @Override - public LoginUser getLoginUser(String token) { - return userSessionService.getLoginUser(token); - } - - @Override - public Long getSessionTimeoutMillis() { - return userSessionService.getSessionTimeoutMillis(); - } - -} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/AuthController.http b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/AuthController.http index e6c70f9df..c2715634e 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/AuthController.http +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/AuthController.http @@ -1,5 +1,5 @@ ### 请求 /login 接口 => 成功 -POST {{baseUrl}}/system/login +POST {{baseUrl}}/system/auth/login Content-Type: application/json tenant-id: {{adminTenentId}} @@ -11,7 +11,7 @@ tenant-id: {{adminTenentId}} } ### 请求 /login 接口 => 成功(无验证码) -POST {{baseUrl}}/system/login +POST {{baseUrl}}/system/auth/login Content-Type: application/json tenant-id: {{adminTenentId}} @@ -21,7 +21,7 @@ tenant-id: {{adminTenentId}} } ### 请求 /get-permission-info 接口 => 成功 -GET {{baseUrl}}/system/get-permission-info +GET {{baseUrl}}/system/auth/get-permission-info Authorization: Bearer {{token}} tenant-id: {{adminTenentId}} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/AuthController.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/AuthController.java index 6fbf2a21a..64cda89d7 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/AuthController.java +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/AuthController.java @@ -1,14 +1,17 @@ package cn.iocoder.yudao.module.system.controller.admin.auth; +import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.common.pojo.CommonResult; import cn.iocoder.yudao.framework.common.util.collection.SetUtils; import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog; +import cn.iocoder.yudao.framework.security.config.SecurityProperties; 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.permission.MenuDO; import cn.iocoder.yudao.module.system.dal.dataobject.permission.RoleDO; import cn.iocoder.yudao.module.system.dal.dataobject.user.AdminUserDO; +import cn.iocoder.yudao.module.system.enums.logger.LoginLogTypeEnum; import cn.iocoder.yudao.module.system.enums.permission.MenuTypeEnum; import cn.iocoder.yudao.module.system.service.auth.AdminAuthService; import cn.iocoder.yudao.module.system.service.permission.PermissionService; @@ -24,18 +27,19 @@ import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import javax.annotation.Resource; +import javax.servlet.http.HttpServletRequest; import javax.validation.Valid; import java.util.List; +import java.util.Set; import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; -import static cn.iocoder.yudao.framework.common.util.servlet.ServletUtils.getClientIP; -import static cn.iocoder.yudao.framework.common.util.servlet.ServletUtils.getUserAgent; import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; -import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserRoleIds; +import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.obtainAuthorization; +import static java.util.Collections.singleton; @Api(tags = "管理后台 - 认证") @RestController -@RequestMapping("/system/auth") // 暂时不跟 /auth 结尾 +@RequestMapping("/system/auth") @Validated @Slf4j public class AuthController { @@ -51,13 +55,33 @@ public class AuthController { @Resource private SocialUserService socialUserService; + @Resource + private SecurityProperties securityProperties; + @PostMapping("/login") @ApiOperation("使用账号密码登录") @OperateLog(enable = false) // 避免 Post 请求被记录操作日志 public CommonResult login(@RequestBody @Valid AuthLoginReqVO reqVO) { - String token = authService.login(reqVO, getClientIP(), getUserAgent()); - // 返回结果 - return success(AuthLoginRespVO.builder().token(token).build()); + return success(authService.login(reqVO)); + } + + @PostMapping("/logout") + @ApiOperation("登出系统") + @OperateLog(enable = false) // 避免 Post 请求被记录操作日志 + public CommonResult logout(HttpServletRequest request) { + String token = obtainAuthorization(request, securityProperties.getTokenHeader()); + if (StrUtil.isNotBlank(token)) { + authService.logout(token, LoginLogTypeEnum.LOGOUT_SELF.getType()); + } + return success(true); + } + + @PostMapping("/refresh-token") + @ApiOperation("刷新令牌") + @ApiImplicitParam(name = "refreshToken", value = "刷新令牌", required = true, dataTypeClass = String.class) + @OperateLog(enable = false) // 避免 Post 请求被记录操作日志 + public CommonResult refreshToken(@RequestParam("refreshToken") String refreshToken) { + return success(authService.refreshToken(refreshToken)); } @GetMapping("/get-permission-info") @@ -69,12 +93,12 @@ public class AuthController { return null; } // 获得角色列表 - List roleList = roleService.getRolesFromCache(getLoginUserRoleIds()); + Set roleIds = permissionService.getUserRoleIds(getLoginUserId(), singleton(CommonStatusEnum.ENABLE.getStatus())); + List roleList = roleService.getRolesFromCache(roleIds); // 获得菜单列表 - List menuList = permissionService.getRoleMenuListFromCache( - getLoginUserRoleIds(), // 注意,基于登录的角色,因为后续的权限判断也是基于它 + List menuList = permissionService.getRoleMenuListFromCache(roleIds, SetUtils.asSet(MenuTypeEnum.DIR.getType(), MenuTypeEnum.MENU.getType(), MenuTypeEnum.BUTTON.getType()), - SetUtils.asSet(CommonStatusEnum.ENABLE.getStatus())); + singleton(CommonStatusEnum.ENABLE.getStatus())); // 只要开启的 // 拼接结果返回 return success(AuthConvert.INSTANCE.convert(user, roleList, menuList)); } @@ -82,11 +106,12 @@ public class AuthController { @GetMapping("/list-menus") @ApiOperation("获得登录用户的菜单列表") public CommonResult> getMenus() { + // 获得角色列表 + Set roleIds = permissionService.getUserRoleIds(getLoginUserId(), singleton(CommonStatusEnum.ENABLE.getStatus())); // 获得用户拥有的菜单列表 - List menuList = permissionService.getRoleMenuListFromCache( - getLoginUserRoleIds(), // 注意,基于登录的角色,因为后续的权限判断也是基于它 + List menuList = permissionService.getRoleMenuListFromCache(roleIds, SetUtils.asSet(MenuTypeEnum.DIR.getType(), MenuTypeEnum.MENU.getType()), // 只要目录和菜单类型 - SetUtils.asSet(CommonStatusEnum.ENABLE.getStatus())); // 只要开启的 + singleton(CommonStatusEnum.ENABLE.getStatus())); // 只要开启的 // 转换成 Tree 结构返回 return success(AuthConvert.INSTANCE.buildMenuTree(menuList)); } @@ -97,9 +122,7 @@ public class AuthController { @ApiOperation("使用短信验证码登录") @OperateLog(enable = false) // 避免 Post 请求被记录操作日志 public CommonResult smsLogin(@RequestBody @Valid AuthSmsLoginReqVO reqVO) { - String token = authService.smsLogin(reqVO, getClientIP(), getUserAgent()); - // 返回结果 - return success(AuthLoginRespVO.builder().token(token).build()); + return success(authService.smsLogin(reqVO)); } @PostMapping("/send-sms-code") @@ -127,18 +150,14 @@ public class AuthController { @ApiOperation("社交快捷登录,使用 code 授权码") @OperateLog(enable = false) // 避免 Post 请求被记录操作日志 public CommonResult socialQuickLogin(@RequestBody @Valid AuthSocialQuickLoginReqVO reqVO) { - String token = authService.socialQuickLogin(reqVO, getClientIP(), getUserAgent()); - // 返回结果 - return success(AuthLoginRespVO.builder().token(token).build()); + return success(authService.socialQuickLogin(reqVO)); } @PostMapping("/social-bind-login") @ApiOperation("社交绑定登录,使用 code 授权码 + 账号密码") @OperateLog(enable = false) // 避免 Post 请求被记录操作日志 public CommonResult socialBindLogin(@RequestBody @Valid AuthSocialBindLoginReqVO reqVO) { - String token = authService.socialBindLogin(reqVO, getClientIP(), getUserAgent()); - // 返回结果 - return success(AuthLoginRespVO.builder().token(token).build()); + return success(authService.socialBindLogin(reqVO)); } } diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/OAuth2ClientController.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/OAuth2ClientController.java new file mode 100755 index 000000000..2912957b8 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/OAuth2ClientController.java @@ -0,0 +1,74 @@ +package cn.iocoder.yudao.module.system.controller.admin.auth; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +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; +import cn.iocoder.yudao.module.system.controller.admin.auth.vo.client.OAuth2ClientRespVO; +import cn.iocoder.yudao.module.system.controller.admin.auth.vo.client.OAuth2ClientUpdateReqVO; +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.service.auth.OAuth2ClientService; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiImplicitParam; +import io.swagger.annotations.ApiOperation; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + +@Api(tags = "管理后台 - OAuth2 客户端") +@RestController +@RequestMapping("/system/oauth2-client") +@Validated +public class OAuth2ClientController { + + @Resource + private OAuth2ClientService oAuth2ClientService; + + @PostMapping("/create") + @ApiOperation("创建 OAuth2 客户端") + @PreAuthorize("@ss.hasPermission('system:oauth2-client:create')") + public CommonResult createOAuth2Client(@Valid @RequestBody OAuth2ClientCreateReqVO createReqVO) { + return success(oAuth2ClientService.createOAuth2Client(createReqVO)); + } + + @PutMapping("/update") + @ApiOperation("更新 OAuth2 客户端") + @PreAuthorize("@ss.hasPermission('system:oauth2-client:update')") + public CommonResult updateOAuth2Client(@Valid @RequestBody OAuth2ClientUpdateReqVO updateReqVO) { + oAuth2ClientService.updateOAuth2Client(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @ApiOperation("删除 OAuth2 客户端") + @ApiImplicitParam(name = "id", value = "编号", required = true, dataTypeClass = Long.class) + @PreAuthorize("@ss.hasPermission('system:oauth2-client:delete')") + public CommonResult deleteOAuth2Client(@RequestParam("id") Long id) { + oAuth2ClientService.deleteOAuth2Client(id); + return success(true); + } + + @GetMapping("/get") + @ApiOperation("获得 OAuth2 客户端") + @ApiImplicitParam(name = "id", value = "编号", required = true, example = "1024", dataTypeClass = Long.class) + @PreAuthorize("@ss.hasPermission('system:oauth2-client:query')") + public CommonResult getOAuth2Client(@RequestParam("id") Long id) { + OAuth2ClientDO oAuth2Client = oAuth2ClientService.getOAuth2Client(id); + return success(OAuth2ClientConvert.INSTANCE.convert(oAuth2Client)); + } + + @GetMapping("/page") + @ApiOperation("获得OAuth2 客户端分页") + @PreAuthorize("@ss.hasPermission('system:oauth2-client:query')") + public CommonResult> getOAuth2ClientPage(@Valid OAuth2ClientPageReqVO pageVO) { + PageResult pageResult = oAuth2ClientService.getOAuth2ClientPage(pageVO); + return success(OAuth2ClientConvert.INSTANCE.convertPage(pageResult)); + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/OAuth2Controller.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/OAuth2Controller.java new file mode 100644 index 000000000..d82731aab --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/OAuth2Controller.java @@ -0,0 +1,24 @@ +package cn.iocoder.yudao.module.system.controller.admin.auth; + +import io.swagger.annotations.Api; +import lombok.extern.slf4j.Slf4j; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@Api(tags = "管理后台 - OAuth2.0 授权") +@RestController +@RequestMapping("/system/oauth2") +@Validated +@Slf4j +public class OAuth2Controller { + +// POST oauth/token TokenEndpoint:Password、Implicit、Code、Refresh Token + +// POST oauth/check_token CheckTokenEndpoint + +// DELETE oauth/token ConsumerTokenServices#revokeToken + +// GET oauth/authorize AuthorizationEndpoint + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/OAuth2TokenController.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/OAuth2TokenController.java new file mode 100644 index 000000000..042a873bd --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/OAuth2TokenController.java @@ -0,0 +1,50 @@ +package cn.iocoder.yudao.module.system.controller.admin.auth; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.system.controller.admin.auth.vo.token.OAuth2AccessTokenPageReqVO; +import cn.iocoder.yudao.module.system.controller.admin.auth.vo.token.OAuth2AccessTokenRespVO; +import cn.iocoder.yudao.module.system.convert.auth.OAuth2TokenConvert; +import cn.iocoder.yudao.module.system.dal.dataobject.auth.OAuth2AccessTokenDO; +import cn.iocoder.yudao.module.system.enums.logger.LoginLogTypeEnum; +import cn.iocoder.yudao.module.system.service.auth.AdminAuthService; +import cn.iocoder.yudao.module.system.service.auth.OAuth2TokenService; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiImplicitParam; +import io.swagger.annotations.ApiOperation; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + +@Api(tags = "管理后台 - OAuth2.0 令牌") +@RestController +@RequestMapping("/system/oauth2-token") +public class OAuth2TokenController { + + @Resource + private OAuth2TokenService oauth2TokenService; + @Resource + private AdminAuthService authService; + + @GetMapping("/page") + @ApiOperation(value = "获得访问令牌分页", notes = "只返回有效期内的") + @PreAuthorize("@ss.hasPermission('system:oauth2-token:page')") + public CommonResult> getAccessTokenPage(@Valid OAuth2AccessTokenPageReqVO reqVO) { + PageResult pageResult = oauth2TokenService.getAccessTokenPage(reqVO); + return success(OAuth2TokenConvert.INSTANCE.convert(pageResult)); + } + + @DeleteMapping("/delete") + @ApiOperation("删除访问令牌") + @ApiImplicitParam(name = "accessToken", value = "访问令牌", required = true, dataTypeClass = String.class, example = "tudou") + @PreAuthorize("@ss.hasPermission('system:oauth2-token:delete')") + public CommonResult deleteAccessToken(@RequestParam("accessToken") String accessToken) { + authService.logout(accessToken, LoginLogTypeEnum.LOGOUT_DELETE.getType()); + return success(true); + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/UserSessionController.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/UserSessionController.java deleted file mode 100644 index 023532e08..000000000 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/UserSessionController.java +++ /dev/null @@ -1,79 +0,0 @@ -package cn.iocoder.yudao.module.system.controller.admin.auth; - -import cn.iocoder.yudao.module.system.controller.admin.auth.vo.session.UserSessionPageItemRespVO; -import cn.iocoder.yudao.module.system.controller.admin.auth.vo.session.UserSessionPageReqVO; -import cn.iocoder.yudao.module.system.convert.auth.UserSessionConvert; -import cn.iocoder.yudao.module.system.dal.dataobject.auth.UserSessionDO; -import cn.iocoder.yudao.module.system.dal.dataobject.dept.DeptDO; -import cn.iocoder.yudao.module.system.service.auth.UserSessionService; -import cn.iocoder.yudao.module.system.dal.dataobject.user.AdminUserDO; -import cn.iocoder.yudao.framework.common.pojo.CommonResult; -import cn.iocoder.yudao.framework.common.pojo.PageResult; -import cn.iocoder.yudao.framework.common.util.collection.MapUtils; -import cn.iocoder.yudao.module.system.service.dept.DeptService; -import cn.iocoder.yudao.module.system.service.user.AdminUserService; -import io.swagger.annotations.Api; -import io.swagger.annotations.ApiImplicitParam; -import io.swagger.annotations.ApiOperation; -import org.springframework.security.access.prepost.PreAuthorize; -import org.springframework.validation.annotation.Validated; -import org.springframework.web.bind.annotation.*; - -import javax.annotation.Resource; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; -import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; - -@Api(tags = "管理后台 - 用户 Session") -@RestController -@RequestMapping("/system/user-session") -public class UserSessionController { - - @Resource - private UserSessionService userSessionService; - @Resource - private AdminUserService userService; - - @Resource - private DeptService deptService; - - @GetMapping("/page") - @ApiOperation("获得 Session 分页列表") - @PreAuthorize("@ss.hasPermission('system:user-session:page')") - public CommonResult> getUserSessionPage(@Validated UserSessionPageReqVO reqVO) { - // 获得 Session 分页 - PageResult pageResult = userSessionService.getUserSessionPage(reqVO); - - // 获得拼接需要的数据 - Map userMap = userService.getUserMap( - convertList(pageResult.getList(), UserSessionDO::getUserId)); - Map deptMap = deptService.getDeptMap( - convertList(userMap.values(), AdminUserDO::getDeptId)); - // 拼接结果返回 - List sessionList = new ArrayList<>(pageResult.getList().size()); - pageResult.getList().forEach(session -> { - UserSessionPageItemRespVO respVO = UserSessionConvert.INSTANCE.convert(session); - sessionList.add(respVO); - // 设置用户账号 - MapUtils.findAndThen(userMap, session.getUserId(), user -> { - respVO.setUsername(user.getUsername()); - // 设置用户部门 - MapUtils.findAndThen(deptMap, user.getDeptId(), dept -> respVO.setDeptName(dept.getName())); - }); - }); - return success(new PageResult<>(sessionList, pageResult.getTotal())); - } - - @DeleteMapping("/delete") - @ApiOperation("删除 Session") - @ApiImplicitParam(name = "id", value = "Session 编号", required = true, dataTypeClass = Long.class, example = "1024") - @PreAuthorize("@ss.hasPermission('system:user-session:delete')") - public CommonResult deleteUserSession(@RequestParam("id") Long id) { - userSessionService.deleteUserSession(id); - return success(true); - } - -} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/vo/auth/AuthLoginRespVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/vo/auth/AuthLoginRespVO.java index bd13ba377..915a550c6 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/vo/auth/AuthLoginRespVO.java +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/vo/auth/AuthLoginRespVO.java @@ -7,14 +7,25 @@ import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; -@ApiModel("管理后台 - 账号密码登录 Response VO") +import java.util.Date; + +@ApiModel("管理后台 - 登录 Response VO") @Data @NoArgsConstructor @AllArgsConstructor @Builder public class AuthLoginRespVO { - @ApiModelProperty(value = "token", required = true, example = "yudaoyuanma") - private String token; + @ApiModelProperty(value = "用户编号", required = true, example = "1024") + private Long userId; + + @ApiModelProperty(value = "访问令牌", required = true, example = "happy") + private String accessToken; + + @ApiModelProperty(value = "刷新令牌", required = true, example = "nice") + private String refreshToken; + + @ApiModelProperty(value = "过期时间", required = true) + private Date expiresTime; } diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/vo/client/OAuth2ClientBaseVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/vo/client/OAuth2ClientBaseVO.java new file mode 100755 index 000000000..a281eed84 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/vo/client/OAuth2ClientBaseVO.java @@ -0,0 +1,51 @@ +package cn.iocoder.yudao.module.system.controller.admin.auth.vo.client; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import javax.validation.constraints.NotNull; +import java.util.List; + +/** +* OAuth2 客户端 Base VO,提供给添加、修改、详细的子 VO 使用 +* 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 +*/ +@Data +public class OAuth2ClientBaseVO { + + @ApiModelProperty(value = "客户端编号", required = true) + @NotNull(message = "客户端编号不能为空") + private Long id; + + @ApiModelProperty(value = "客户端密钥", required = true) + @NotNull(message = "客户端密钥不能为空") + private String secret; + + @ApiModelProperty(value = "应用名", required = true) + @NotNull(message = "应用名不能为空") + private String name; + + @ApiModelProperty(value = "应用图标", required = true) + @NotNull(message = "应用图标不能为空") + private String logo; + + @ApiModelProperty(value = "应用描述") + private String description; + + @ApiModelProperty(value = "状态", required = true) + @NotNull(message = "状态不能为空") + private Integer status; + + @ApiModelProperty(value = "访问令牌的有效期", required = true) + @NotNull(message = "访问令牌的有效期不能为空") + private Integer accessTokenValiditySeconds; + + @ApiModelProperty(value = "刷新令牌的有效期", required = true) + @NotNull(message = "刷新令牌的有效期不能为空") + private Integer refreshTokenValiditySeconds; + + @ApiModelProperty(value = "可重定向的 URI 地址", required = true) + @NotNull(message = "可重定向的 URI 地址不能为空") + private List redirectUris; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/vo/client/OAuth2ClientCreateReqVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/vo/client/OAuth2ClientCreateReqVO.java new file mode 100755 index 000000000..f49735ec5 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/vo/client/OAuth2ClientCreateReqVO.java @@ -0,0 +1,12 @@ +package cn.iocoder.yudao.module.system.controller.admin.auth.vo.client; + +import lombok.*; +import io.swagger.annotations.*; + +@ApiModel("管理后台 - OAuth2 客户端创建 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class OAuth2ClientCreateReqVO extends OAuth2ClientBaseVO { + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/vo/client/OAuth2ClientPageReqVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/vo/client/OAuth2ClientPageReqVO.java new file mode 100755 index 000000000..aa6434a28 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/vo/client/OAuth2ClientPageReqVO.java @@ -0,0 +1,19 @@ +package cn.iocoder.yudao.module.system.controller.admin.auth.vo.client; + +import lombok.*; +import io.swagger.annotations.*; +import cn.iocoder.yudao.framework.common.pojo.PageParam; + +@ApiModel("管理后台 - OAuth2 客户端分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class OAuth2ClientPageReqVO extends PageParam { + + @ApiModelProperty(value = "应用名") + private String name; + + @ApiModelProperty(value = "状态") + private Integer status; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/vo/client/OAuth2ClientRespVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/vo/client/OAuth2ClientRespVO.java new file mode 100755 index 000000000..ba0197844 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/vo/client/OAuth2ClientRespVO.java @@ -0,0 +1,20 @@ +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 java.util.Date; + +@ApiModel("管理后台 - OAuth2 客户端 Response VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class OAuth2ClientRespVO extends OAuth2ClientBaseVO { + + @ApiModelProperty(value = "创建时间", required = true) + private Date createTime; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/vo/client/OAuth2ClientUpdateReqVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/vo/client/OAuth2ClientUpdateReqVO.java new file mode 100755 index 000000000..bbcd96fd2 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/vo/client/OAuth2ClientUpdateReqVO.java @@ -0,0 +1,14 @@ +package cn.iocoder.yudao.module.system.controller.admin.auth.vo.client; + +import io.swagger.annotations.ApiModel; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +@ApiModel("管理后台 - OAuth2 客户端更新 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class OAuth2ClientUpdateReqVO extends OAuth2ClientBaseVO { + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/vo/session/UserSessionPageItemRespVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/vo/session/UserSessionPageItemRespVO.java deleted file mode 100644 index ad12877ef..000000000 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/vo/session/UserSessionPageItemRespVO.java +++ /dev/null @@ -1,38 +0,0 @@ -package cn.iocoder.yudao.module.system.controller.admin.auth.vo.session; - -import cn.iocoder.yudao.framework.common.pojo.PageParam; -import io.swagger.annotations.ApiModel; -import io.swagger.annotations.ApiModelProperty; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.NoArgsConstructor; - -import java.util.Date; - -@ApiModel(value = "管理后台 - 用户在线 Session Response VO", description = "相比用户基本信息来说,会多部门、用户账号等信息") -@Data -@NoArgsConstructor -@AllArgsConstructor -@EqualsAndHashCode(callSuper = true) -public class UserSessionPageItemRespVO extends PageParam { - - @ApiModelProperty(value = "Session 编号", required = true, example = "fe50b9f6-d177-44b1-8da9-72ea34f63db7") - private String id; - - @ApiModelProperty(value = "用户 IP", required = true, example = "127.0.0.1") - private String userIp; - - @ApiModelProperty(value = "浏览器 UserAgent", required = true, example = "Mozilla/5.0") - private String userAgent; - - @ApiModelProperty(value = "登录时间", required = true) - private Date createTime; - - @ApiModelProperty(value = "用户账号", required = true, example = "yudao") - private String username; - - @ApiModelProperty(value = "部门名称", example = "研发部") - private String deptName; - -} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/vo/session/UserSessionPageReqVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/vo/session/UserSessionPageReqVO.java deleted file mode 100644 index 7e85c87ba..000000000 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/vo/session/UserSessionPageReqVO.java +++ /dev/null @@ -1,20 +0,0 @@ -package cn.iocoder.yudao.module.system.controller.admin.auth.vo.session; - -import cn.iocoder.yudao.framework.common.pojo.PageParam; -import io.swagger.annotations.ApiModel; -import io.swagger.annotations.ApiModelProperty; -import lombok.Data; -import lombok.EqualsAndHashCode; - -@ApiModel("管理后台 - 在线用户 Session 分页 Request VO") -@Data -@EqualsAndHashCode(callSuper = true) -public class UserSessionPageReqVO extends PageParam { - - @ApiModelProperty(value = "用户 IP", example = "127.0.0.1", notes = "模糊匹配") - private String userIp; - - @ApiModelProperty(value = "用户账号", example = "yudao", notes = "模糊匹配") - private String username; - -} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/vo/token/OAuth2AccessTokenPageReqVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/vo/token/OAuth2AccessTokenPageReqVO.java new file mode 100644 index 000000000..77fbb0c4a --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/vo/token/OAuth2AccessTokenPageReqVO.java @@ -0,0 +1,23 @@ +package cn.iocoder.yudao.module.system.controller.admin.auth.vo.token; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@ApiModel("管理后台 - 访问令牌分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +public class OAuth2AccessTokenPageReqVO extends PageParam { + + @ApiModelProperty(value = "用户编号", required = true, example = "666") + private Long userId; + + @ApiModelProperty(value = "用户类型", required = true, example = "2", notes = "参见 UserTypeEnum 枚举") + private Integer userType; + + @ApiModelProperty(value = "客户端编号", required = true, example = "2") + private Long clientId; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/vo/token/OAuth2AccessTokenRespVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/vo/token/OAuth2AccessTokenRespVO.java new file mode 100644 index 000000000..4de251f93 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/vo/token/OAuth2AccessTokenRespVO.java @@ -0,0 +1,41 @@ +package cn.iocoder.yudao.module.system.controller.admin.auth.vo.token; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.Date; + +@ApiModel("管理后台 - 访问令牌 Response VO") +@Data +@NoArgsConstructor +@AllArgsConstructor +public class OAuth2AccessTokenRespVO { + + @ApiModelProperty(value = "编号", required = true, example = "1024") + private Long id; + + @ApiModelProperty(value = "访问令牌", required = true, example = "tudou") + private String accessToken; + + @ApiModelProperty(value = "刷新令牌", required = true, example = "nice") + private String refreshToken; + + @ApiModelProperty(value = "用户编号", required = true, example = "666") + private Long userId; + + @ApiModelProperty(value = "用户类型", required = true, example = "2", notes = "参见 UserTypeEnum 枚举") + private Integer userType; + + @ApiModelProperty(value = "客户端编号", required = true, example = "2") + private Long clientId; + + @ApiModelProperty(value = "创建时间", required = true) + private Date createTime; + + @ApiModelProperty(value = "过期时间", required = true) + private Date expiresTime; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/convert/auth/AuthConvert.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/convert/auth/AuthConvert.java index d7979bc81..9f8a32225 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/convert/auth/AuthConvert.java +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/convert/auth/AuthConvert.java @@ -1,20 +1,16 @@ package cn.iocoder.yudao.module.system.convert.auth; -import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; -import cn.iocoder.yudao.framework.security.core.LoginUser; -import cn.iocoder.yudao.module.system.api.sms.dto.code.SmsCodeSendReqDTO; import cn.iocoder.yudao.module.system.api.sms.dto.code.SmsCodeSendReqDTO; import cn.iocoder.yudao.module.system.api.sms.dto.code.SmsCodeUseReqDTO; import cn.iocoder.yudao.module.system.api.social.dto.SocialUserBindReqDTO; -import cn.iocoder.yudao.module.system.api.social.dto.SocialUserUnbindReqDTO; import cn.iocoder.yudao.module.system.controller.admin.auth.vo.auth.*; +import cn.iocoder.yudao.module.system.dal.dataobject.auth.OAuth2AccessTokenDO; import cn.iocoder.yudao.module.system.dal.dataobject.permission.MenuDO; import cn.iocoder.yudao.module.system.dal.dataobject.permission.RoleDO; import cn.iocoder.yudao.module.system.dal.dataobject.user.AdminUserDO; import cn.iocoder.yudao.module.system.enums.permission.MenuIdEnum; import org.mapstruct.Mapper; -import org.mapstruct.Mapping; import org.mapstruct.factory.Mappers; import org.slf4j.LoggerFactory; @@ -25,13 +21,7 @@ public interface AuthConvert { AuthConvert INSTANCE = Mappers.getMapper(AuthConvert.class); - @Mapping(source = "updateTime", target = "updateTime", ignore = true) // 字段相同,但是含义不同,忽略 - LoginUser convert0(AdminUserDO bean); - - default LoginUser convert(AdminUserDO bean) { - // 目的,为了设置 UserTypeEnum.ADMIN.getValue() - return convert0(bean).setUserType(UserTypeEnum.ADMIN.getValue()); - } + AuthLoginRespVO convert(OAuth2AccessTokenDO bean); default AuthPermissionInfoRespVO convert(AdminUserDO user, List roleList, List menuList) { return AuthPermissionInfoRespVO.builder() diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/convert/auth/OAuth2ClientConvert.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/convert/auth/OAuth2ClientConvert.java new file mode 100755 index 000000000..91ccb4ea8 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/convert/auth/OAuth2ClientConvert.java @@ -0,0 +1,33 @@ +package cn.iocoder.yudao.module.system.convert.auth; + +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.OAuth2ClientRespVO; +import cn.iocoder.yudao.module.system.controller.admin.auth.vo.client.OAuth2ClientUpdateReqVO; +import cn.iocoder.yudao.module.system.dal.dataobject.auth.OAuth2ClientDO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +import java.util.List; + +/** + * OAuth2 客户端 Convert + * + * @author 芋道源码 + */ +@Mapper +public interface OAuth2ClientConvert { + + OAuth2ClientConvert INSTANCE = Mappers.getMapper(OAuth2ClientConvert.class); + + OAuth2ClientDO convert(OAuth2ClientCreateReqVO bean); + + OAuth2ClientDO convert(OAuth2ClientUpdateReqVO bean); + + OAuth2ClientRespVO convert(OAuth2ClientDO bean); + + List convertList(List list); + + PageResult convertPage(PageResult page); + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/convert/auth/OAuth2TokenConvert.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/convert/auth/OAuth2TokenConvert.java new file mode 100644 index 000000000..882f69723 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/convert/auth/OAuth2TokenConvert.java @@ -0,0 +1,22 @@ +package cn.iocoder.yudao.module.system.convert.auth; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.system.api.auth.dto.OAuth2AccessTokenCheckRespDTO; +import cn.iocoder.yudao.module.system.api.auth.dto.OAuth2AccessTokenRespDTO; +import cn.iocoder.yudao.module.system.controller.admin.auth.vo.token.OAuth2AccessTokenRespVO; +import cn.iocoder.yudao.module.system.dal.dataobject.auth.OAuth2AccessTokenDO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +@Mapper +public interface OAuth2TokenConvert { + + OAuth2TokenConvert INSTANCE = Mappers.getMapper(OAuth2TokenConvert.class); + + OAuth2AccessTokenCheckRespDTO convert(OAuth2AccessTokenDO bean); + + PageResult convert(PageResult page); + + OAuth2AccessTokenRespDTO convert2(OAuth2AccessTokenDO bean); + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/convert/auth/UserSessionConvert.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/convert/auth/UserSessionConvert.java deleted file mode 100644 index d30dfdcbb..000000000 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/convert/auth/UserSessionConvert.java +++ /dev/null @@ -1,15 +0,0 @@ -package cn.iocoder.yudao.module.system.convert.auth; - -import cn.iocoder.yudao.module.system.controller.admin.auth.vo.session.UserSessionPageItemRespVO; -import cn.iocoder.yudao.module.system.dal.dataobject.auth.UserSessionDO; -import org.mapstruct.Mapper; -import org.mapstruct.factory.Mappers; - -@Mapper -public interface UserSessionConvert { - - UserSessionConvert INSTANCE = Mappers.getMapper(UserSessionConvert.class); - - UserSessionPageItemRespVO convert(UserSessionDO session); - -} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/auth/OAuth2AccessTokenDO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/auth/OAuth2AccessTokenDO.java new file mode 100644 index 000000000..aaaa8152c --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/auth/OAuth2AccessTokenDO.java @@ -0,0 +1,61 @@ +package cn.iocoder.yudao.module.system.dal.dataobject.auth; + +import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; +import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +import java.util.Date; + +/** + * OAuth2 访问令牌 DO + * + * 如下字段,暂时未使用,暂时不支持: + * user_name、authentication(用户信息) + * + * @author 芋道源码 + */ +@TableName("system_oauth2_access_token") +@Data +@EqualsAndHashCode(callSuper = true) +@Accessors(chain = true) +public class OAuth2AccessTokenDO extends TenantBaseDO { + + /** + * 编号,数据库递增 + */ + @TableId + private Long id; + /** + * 访问令牌 + */ + private String accessToken; + /** + * 刷新令牌 + */ + private String refreshToken; + /** + * 用户编号 + */ + private Long userId; + /** + * 用户类型 + * + * 枚举 {@link UserTypeEnum} + */ + private Integer userType; + /** + * 客户端编号 + * + * 关联 {@link OAuth2ClientDO#getId()} + */ + private Long clientId; + /** + * 过期时间 + */ + private Date expiresTime; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/auth/OAuth2ClientDO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/auth/OAuth2ClientDO.java new file mode 100644 index 000000000..cac80f5c2 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/auth/OAuth2ClientDO.java @@ -0,0 +1,73 @@ +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 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) + private Long id; + /** + * 客户端密钥 + */ + private String secret; + /** + * 应用名 + */ + private String name; + /** + * 应用图标 + */ + private String logo; + /** + * 应用描述 + */ + private String description; + /** + * 状态 + * + * 枚举 {@link CommonStatusEnum} + */ + private Integer status; + /** + * 访问令牌的有效期 + */ + private Integer accessTokenValiditySeconds; + /** + * 刷新令牌的有效期 + */ + private Integer refreshTokenValiditySeconds; + /** + * 可重定向的 URI 地址 + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private List redirectUris; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/auth/OAuth2CodeDO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/auth/OAuth2CodeDO.java new file mode 100644 index 000000000..7a851b01d --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/auth/OAuth2CodeDO.java @@ -0,0 +1,62 @@ +package cn.iocoder.yudao.module.system.dal.dataobject.auth; + +import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; +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; + +/** + * OAuth2 授权码 DO + * + * @author 芋道源码 + */ +@TableName("system_oauth2_code") +@Data +@EqualsAndHashCode(callSuper = true) +@Accessors(chain = true) +public class OAuth2CodeDO extends BaseDO { + + /** + * 编号,数据库递增 + */ + private Long id; + /** + * 授权码 + */ + private String code; + /** + * 用户编号 + */ + private Long userId; + /** + * 用户类型 + * + * 枚举 {@link UserTypeEnum} + */ + private Integer userType; + /** + * 客户端编号 + * + * 关联 {@link OAuth2ClientDO#getId()} + */ + private Long clientId; + /** + * 刷新令牌 + * + * 关联 {@link OAuth2RefreshTokenDO#getRefreshToken()} + */ + private String refreshToken; + /** + * 过期时间 + */ + private Date expiresTime; + /** + * 创建 IP + */ + private String createIp; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/auth/OAuth2RefreshTokenDO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/auth/OAuth2RefreshTokenDO.java new file mode 100644 index 000000000..1f0aa3032 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/auth/OAuth2RefreshTokenDO.java @@ -0,0 +1,52 @@ +package cn.iocoder.yudao.module.system.dal.dataobject.auth; + +import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; +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; + +/** + * OAuth2 刷新令牌 + * + * @author 芋道源码 + */ +@TableName("system_oauth2_refresh_token") +@Data +@EqualsAndHashCode(callSuper = true) +@Accessors(chain = true) +public class OAuth2RefreshTokenDO extends BaseDO { + + /** + * 编号,数据库字典 + */ + private Long id; + /** + * 刷新令牌 + */ + private String refreshToken; + /** + * 用户编号 + */ + private Long userId; + /** + * 用户类型 + * + * 枚举 {@link UserTypeEnum} + */ + private Integer userType; + /** + * 客户端编号 + * + * 关联 {@link OAuth2ClientDO#getId()} + */ + private Long clientId; + /** + * 过期时间 + */ + private Date expiresTime; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/auth/UserSessionDO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/auth/UserSessionDO.java deleted file mode 100644 index 9249dc22d..000000000 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/auth/UserSessionDO.java +++ /dev/null @@ -1,71 +0,0 @@ -package cn.iocoder.yudao.module.system.dal.dataobject.auth; - -import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; -import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; -import cn.iocoder.yudao.framework.security.core.LoginUser; -import com.baomidou.mybatisplus.annotation.*; -import lombok.Builder; -import lombok.Data; -import lombok.EqualsAndHashCode; - -import java.util.Date; - -/** - * 在线用户表 - * - * 我们已经将 {@link LoginUser} 缓存在 Redis 当中。 - * 这里额外存储在线用户到 MySQL 中,目的是为了方便管理界面可以灵活查询。 - * 同时,通过定时轮询 UserSessionDO 表,可以主动删除 Redis 的缓存,因为 Redis 的过期删除是延迟的。 - * - * @author 芋道源码 - */ -@TableName(value = "system_user_session") -@KeySequence(value = "system_user_session_seq") -@Data -@Builder -@EqualsAndHashCode(callSuper = true) -public class UserSessionDO extends BaseDO { - - /** - * 会话编号 - */ - private Long id; - /** - * 令牌 - */ - private String token; - - /** - * 用户编号 - * - * 关联 AdminUserDO.id 或者 MemberUserDO.id - */ - private Long userId; - /** - * 用户类型 - * - * 枚举 {@link UserTypeEnum} - */ - private Integer userType; - - /** - * 用户账号 - * - * 冗余,因为账号可以变更 - */ - private String username; - - /** - * 用户 IP - */ - private String userIp; - /** - * 浏览器 UA - */ - private String userAgent; - /** - * 会话超时时间 - */ - private Date sessionTimeout; - -} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/auth/OAuth2AccessTokenMapper.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/auth/OAuth2AccessTokenMapper.java new file mode 100644 index 000000000..6fadaf527 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/auth/OAuth2AccessTokenMapper.java @@ -0,0 +1,33 @@ +package cn.iocoder.yudao.module.system.dal.mysql.auth; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.system.controller.admin.auth.vo.token.OAuth2AccessTokenPageReqVO; +import cn.iocoder.yudao.module.system.dal.dataobject.auth.OAuth2AccessTokenDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.Date; +import java.util.List; + +@Mapper +public interface OAuth2AccessTokenMapper extends BaseMapperX { + + default OAuth2AccessTokenDO selectByAccessToken(String accessToken) { + return selectOne(OAuth2AccessTokenDO::getAccessToken, accessToken); + } + + default List selectListByRefreshToken(String refreshToken) { + return selectList(OAuth2AccessTokenDO::getRefreshToken, refreshToken); + } + + default PageResult selectPage(OAuth2AccessTokenPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .eqIfPresent(OAuth2AccessTokenDO::getUserId, reqVO.getUserId()) + .eqIfPresent(OAuth2AccessTokenDO::getUserType, reqVO.getUserType()) + .eqIfPresent(OAuth2AccessTokenDO::getClientId, reqVO.getClientId()) + .gt(OAuth2AccessTokenDO::getExpiresTime, new Date()) + .orderByDesc(OAuth2AccessTokenDO::getId)); + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/auth/OAuth2ClientMapper.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/auth/OAuth2ClientMapper.java new file mode 100755 index 000000000..818128128 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/auth/OAuth2ClientMapper.java @@ -0,0 +1,25 @@ +package cn.iocoder.yudao.module.system.dal.mysql.auth; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +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; + +/** + * OAuth2 客户端 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface OAuth2ClientMapper extends BaseMapperX { + + default PageResult selectPage(OAuth2ClientPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .likeIfPresent(OAuth2ClientDO::getName, reqVO.getName()) + .eqIfPresent(OAuth2ClientDO::getStatus, reqVO.getStatus()) + .orderByDesc(OAuth2ClientDO::getId)); + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/auth/OAuth2RefreshTokenMapper.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/auth/OAuth2RefreshTokenMapper.java new file mode 100644 index 000000000..0e46e4767 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/auth/OAuth2RefreshTokenMapper.java @@ -0,0 +1,20 @@ +package cn.iocoder.yudao.module.system.dal.mysql.auth; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.system.dal.dataobject.auth.OAuth2RefreshTokenDO; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface OAuth2RefreshTokenMapper extends BaseMapperX { + + default int deleteByRefreshToken(String refreshToken) { + return delete(new LambdaQueryWrapperX() + .eq(OAuth2RefreshTokenDO::getRefreshToken, refreshToken)); + } + + default OAuth2RefreshTokenDO selectByRefreshToken(String refreshToken) { + return selectOne(OAuth2RefreshTokenDO::getRefreshToken, refreshToken); + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/auth/UserSessionMapper.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/auth/UserSessionMapper.java deleted file mode 100644 index dea0d4792..000000000 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/auth/UserSessionMapper.java +++ /dev/null @@ -1,37 +0,0 @@ -package cn.iocoder.yudao.module.system.dal.mysql.auth; - -import cn.iocoder.yudao.framework.common.pojo.PageResult; -import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; -import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; -import cn.iocoder.yudao.module.system.controller.admin.auth.vo.session.UserSessionPageReqVO; -import cn.iocoder.yudao.module.system.dal.dataobject.auth.UserSessionDO; -import org.apache.ibatis.annotations.Mapper; - -import java.util.Collection; -import java.util.Date; -import java.util.List; - -@Mapper -public interface UserSessionMapper extends BaseMapperX { - - default PageResult selectPage(UserSessionPageReqVO reqVO, Collection userIds) { - return selectPage(reqVO, new LambdaQueryWrapperX() - .inIfPresent(UserSessionDO::getUserId, userIds) - .likeIfPresent(UserSessionDO::getUserIp, reqVO.getUserIp())); - } - - default List selectListBySessionTimoutLt() { - return selectList(new LambdaQueryWrapperX() - .lt(UserSessionDO::getSessionTimeout, new Date())); - } - - default void updateByToken(String token, UserSessionDO updateObj) { - update(updateObj, new LambdaQueryWrapperX() - .eq(UserSessionDO::getToken, token)); - } - - default void deleteByToken(String token) { - delete(new LambdaQueryWrapperX().eq(UserSessionDO::getToken, token)); - } - -} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/redis/RedisKeyConstants.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/redis/RedisKeyConstants.java index b17c6fb4d..839b973ef 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/redis/RedisKeyConstants.java +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/redis/RedisKeyConstants.java @@ -1,7 +1,7 @@ package cn.iocoder.yudao.module.system.dal.redis; import cn.iocoder.yudao.framework.redis.core.RedisKeyDefine; -import cn.iocoder.yudao.framework.security.core.LoginUser; +import cn.iocoder.yudao.module.system.dal.dataobject.auth.OAuth2AccessTokenDO; import java.time.Duration; @@ -18,9 +18,9 @@ public interface RedisKeyConstants { "captcha_code:%s", // 参数为 uuid STRING, String.class, RedisKeyDefine.TimeoutTypeEnum.DYNAMIC); - RedisKeyDefine LOGIN_USER = new RedisKeyDefine("登录用户的缓存", - "login_user:%s", // 参数为 token 令牌 - STRING, LoginUser.class, RedisKeyDefine.TimeoutTypeEnum.DYNAMIC); + RedisKeyDefine OAUTH2_ACCESS_TOKEN = new RedisKeyDefine("访问令牌的缓存", + "oauth2_access_token:%s", // 参数为访问令牌 token + STRING, OAuth2AccessTokenDO.class, RedisKeyDefine.TimeoutTypeEnum.DYNAMIC); RedisKeyDefine SOCIAL_AUTH_STATE = new RedisKeyDefine("社交登陆的 state", // 注意,它是被 JustAuth 的 justauth.type.prefix 使用到 "social_auth_state:%s", // 参数为 state diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/redis/auth/LoginUserRedisDAO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/redis/auth/LoginUserRedisDAO.java deleted file mode 100644 index 8af701f71..000000000 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/redis/auth/LoginUserRedisDAO.java +++ /dev/null @@ -1,52 +0,0 @@ -package cn.iocoder.yudao.module.system.dal.redis.auth; - -import cn.iocoder.yudao.framework.common.util.json.JsonUtils; -import cn.iocoder.yudao.framework.security.config.SecurityProperties; -import cn.iocoder.yudao.framework.security.core.LoginUser; -import org.springframework.data.redis.core.StringRedisTemplate; -import org.springframework.stereotype.Repository; - -import javax.annotation.Resource; - -import static cn.iocoder.yudao.module.system.dal.redis.RedisKeyConstants.LOGIN_USER; - -/** - * {@link LoginUser} 的 RedisDAO - * - * @author 芋道源码 - */ -@Repository -public class LoginUserRedisDAO { - - @Resource - private StringRedisTemplate stringRedisTemplate; - - @Resource - private SecurityProperties securityProperties; - - public LoginUser get(String token) { - String redisKey = formatKey(token); - return JsonUtils.parseObject(stringRedisTemplate.opsForValue().get(redisKey), LoginUser.class); - } - - public Boolean exists(String token) { - String redisKey = formatKey(token); - return stringRedisTemplate.hasKey(redisKey); - } - - public void set(String token, LoginUser loginUser) { - String redisKey = formatKey(token); - stringRedisTemplate.opsForValue().set(redisKey, JsonUtils.toJsonString(loginUser), - securityProperties.getSessionTimeout()); - } - - public void delete(String token) { - String redisKey = formatKey(token); - stringRedisTemplate.delete(redisKey); - } - - private static String formatKey(String token) { - return LOGIN_USER.formatKey(token); - } - -} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/redis/auth/OAuth2AccessTokenRedisDAO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/redis/auth/OAuth2AccessTokenRedisDAO.java new file mode 100644 index 000000000..3afa69300 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/redis/auth/OAuth2AccessTokenRedisDAO.java @@ -0,0 +1,54 @@ +package cn.iocoder.yudao.module.system.dal.redis.auth; + +import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import cn.iocoder.yudao.module.system.dal.dataobject.auth.OAuth2AccessTokenDO; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Repository; + +import javax.annotation.Resource; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import static cn.iocoder.yudao.module.system.dal.redis.RedisKeyConstants.OAUTH2_ACCESS_TOKEN; + +/** + * {@link OAuth2AccessTokenDO} 的 RedisDAO + * + * @author 芋道源码 + */ +@Repository +public class OAuth2AccessTokenRedisDAO { + + @Resource + private StringRedisTemplate stringRedisTemplate; + + public OAuth2AccessTokenDO get(String accessToken) { + String redisKey = formatKey(accessToken); + return JsonUtils.parseObject(stringRedisTemplate.opsForValue().get(redisKey), OAuth2AccessTokenDO.class); + } + + public void set(OAuth2AccessTokenDO accessTokenDO) { + String redisKey = formatKey(accessTokenDO.getAccessToken()); + // 清理多余字段,避免缓存 + accessTokenDO.setUpdater(null).setUpdateTime(null).setCreateTime(null).setCreator(null).setDeleted(null); + stringRedisTemplate.opsForValue().set(redisKey, JsonUtils.toJsonString(accessTokenDO), + accessTokenDO.getExpiresTime().getTime() - System.currentTimeMillis(), TimeUnit.MILLISECONDS); + } + + public void delete(String accessToken) { + String redisKey = formatKey(accessToken); + stringRedisTemplate.delete(redisKey); + } + + public void deleteList(Collection accessTokens) { + List redisKeys = CollectionUtils.convertList(accessTokens, OAuth2AccessTokenRedisDAO::formatKey); + stringRedisTemplate.delete(redisKeys); + } + + private static String formatKey(String accessToken) { + return String.format(OAUTH2_ACCESS_TOKEN.getKeyTemplate(), accessToken); + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/security/config/SecurityConfiguration.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/security/config/SecurityConfiguration.java index 567c98775..1233a2d81 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/security/config/SecurityConfiguration.java +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/security/config/SecurityConfiguration.java @@ -20,6 +20,8 @@ public class SecurityConfiguration { public void customize(ExpressionUrlAuthorizationConfigurer.ExpressionInterceptUrlRegistry registry) { // 登录的接口 registry.antMatchers(buildAdminApi("/system/auth/login")).permitAll(); + registry.antMatchers(buildAdminApi("/system/auth/logout")).permitAll(); + registry.antMatchers(buildAdminApi("/system/auth/refresh-token")).permitAll(); // 社交登陆的接口 registry.antMatchers(buildAdminApi("/system/auth/social-auth-redirect")).permitAll(); registry.antMatchers(buildAdminApi("/system/auth/social-quick-login")).permitAll(); diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/job/auth/UserSessionTimeoutJob.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/job/auth/UserSessionTimeoutJob.java deleted file mode 100644 index b2bb523e3..000000000 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/job/auth/UserSessionTimeoutJob.java +++ /dev/null @@ -1,32 +0,0 @@ -package cn.iocoder.yudao.module.system.job.auth; - -import cn.iocoder.yudao.framework.quartz.core.handler.JobHandler; -import cn.iocoder.yudao.module.system.service.auth.UserSessionService; -import cn.iocoder.yudao.framework.tenant.core.job.TenantJob; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Component; - -import javax.annotation.Resource; - -/** - * 用户 Session 超时 Job - * - * @author 願 - */ -@Component -@TenantJob -@Slf4j -public class UserSessionTimeoutJob implements JobHandler { - - @Resource - private UserSessionService userSessionService; - - @Override - public String execute(String param) throws Exception { - // 执行过期 - Long timeoutCount = userSessionService.deleteTimeoutSession(); - // 返回结果,记录每次的超时数量 - return String.format("移除在线会话数量为 %s 个", timeoutCount); - } - -} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/auth/AdminAuthService.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/auth/AdminAuthService.java index 84f742207..5f67f9696 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/auth/AdminAuthService.java +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/auth/AdminAuthService.java @@ -1,28 +1,33 @@ package cn.iocoder.yudao.module.system.service.auth; import cn.iocoder.yudao.module.system.controller.admin.auth.vo.auth.*; -import cn.iocoder.yudao.framework.security.core.service.SecurityAuthFrameworkService; import javax.validation.Valid; /** * 管理后台的认证 Service 接口 * - * 提供用户的账号密码登录、token 的校验等认证相关的功能 + * 提供用户的登录、登出的能力 * * @author 芋道源码 */ -public interface AdminAuthService extends SecurityAuthFrameworkService { +public interface AdminAuthService { /** * 账号登录 * * @param reqVO 登录信息 - * @param userIp 用户 IP - * @param userAgent 用户 UA - * @return 身份令牌,使用 JWT 方式 + * @return 登录结果 */ - String login(@Valid AuthLoginReqVO reqVO, String userIp, String userAgent); + AuthLoginRespVO login(@Valid AuthLoginReqVO reqVO); + + /** + * 基于 token 退出登录 + * + * @param token token + * @param logType 登出类型 + */ + void logout(String token, Integer logType); /** * 短信验证码发送 @@ -35,30 +40,32 @@ public interface AdminAuthService extends SecurityAuthFrameworkService { * 短信登录 * * @param reqVO 登录信息 - * @param userIp 用户 IP - * @param userAgent 用户 UA - * @return 身份令牌,使用 JWT 方式 + * @return 登录结果 */ - String smsLogin(AuthSmsLoginReqVO reqVO, String userIp, String userAgent) ; + AuthLoginRespVO smsLogin(AuthSmsLoginReqVO reqVO) ; /** * 社交快捷登录,使用 code 授权码 * * @param reqVO 登录信息 - * @param userIp 用户 IP - * @param userAgent 用户 UA - * @return 身份令牌,使用 JWT 方式 + * @return 登录结果 */ - String socialQuickLogin(@Valid AuthSocialQuickLoginReqVO reqVO, String userIp, String userAgent); + AuthLoginRespVO socialQuickLogin(@Valid AuthSocialQuickLoginReqVO reqVO); /** * 社交绑定登录,使用 code 授权码 + 账号密码 * * @param reqVO 登录信息 - * @param userIp 用户 IP - * @param userAgent 用户 UA - * @return 身份令牌,使用 JWT 方式 + * @return 登录结果 */ - String socialBindLogin(@Valid AuthSocialBindLoginReqVO reqVO, String userIp, String userAgent); + AuthLoginRespVO socialBindLogin(@Valid AuthSocialBindLoginReqVO reqVO); + + /** + * 刷新访问令牌 + * + * @param refreshToken 刷新令牌 + * @return 登录结果 + */ + AuthLoginRespVO refreshToken(String refreshToken); } diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/auth/AdminAuthServiceImpl.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/auth/AdminAuthServiceImpl.java index 6476102bf..743c72aaa 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/auth/AdminAuthServiceImpl.java +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/auth/AdminAuthServiceImpl.java @@ -1,47 +1,37 @@ package cn.iocoder.yudao.module.system.service.auth; +import cn.hutool.core.util.ObjectUtil; import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; import cn.iocoder.yudao.framework.common.util.monitor.TracerUtils; import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils; import cn.iocoder.yudao.framework.common.util.validation.ValidationUtils; -import cn.iocoder.yudao.framework.security.core.LoginUser; -import cn.iocoder.yudao.framework.security.core.authentication.MultiUsernamePasswordAuthenticationToken; 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.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.logger.LoginLogTypeEnum; import cn.iocoder.yudao.module.system.enums.logger.LoginResultEnum; import cn.iocoder.yudao.module.system.enums.sms.SmsSceneEnum; import cn.iocoder.yudao.module.system.service.common.CaptchaService; import cn.iocoder.yudao.module.system.service.logger.LoginLogService; -import cn.iocoder.yudao.module.system.service.permission.PermissionService; +import cn.iocoder.yudao.module.system.service.member.MemberService; import cn.iocoder.yudao.module.system.service.social.SocialUserService; import cn.iocoder.yudao.module.system.service.user.AdminUserService; +import com.google.common.annotations.VisibleForTesting; import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Lazy; -import org.springframework.security.authentication.AuthenticationManager; -import org.springframework.security.authentication.BadCredentialsException; -import org.springframework.security.authentication.DisabledException; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.AuthenticationException; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; -import org.springframework.util.Assert; import javax.annotation.Resource; import javax.validation.Validator; import java.util.Objects; -import java.util.Set; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.framework.common.util.servlet.ServletUtils.getClientIP; import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.*; -import static java.util.Collections.singleton; /** * Auth Service 实现类 @@ -53,22 +43,17 @@ import static java.util.Collections.singleton; public class AdminAuthServiceImpl implements AdminAuthService { @Resource - @Lazy // 延迟加载,因为存在相互依赖的问题 - private AuthenticationManager authenticationManager; - - @Autowired - @SuppressWarnings("SpringJavaAutowiredFieldsWarningInspection") // UserService 存在重名 private AdminUserService userService; @Resource - private PermissionService permissionService; - @Resource private CaptchaService captchaService; @Resource private LoginLogService loginLogService; @Resource - private UserSessionService userSessionService; + private OAuth2TokenService oauth2TokenService; @Resource private SocialUserService socialUserService; + @Resource + private MemberService memberService; @Resource private Validator validator; @@ -77,39 +62,15 @@ public class AdminAuthServiceImpl implements AdminAuthService { private SmsCodeApi smsCodeApi; @Override - public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { - // 获取 username 对应的 AdminUserDO - AdminUserDO user = userService.getUserByUsername(username); - if (user == null) { - throw new UsernameNotFoundException(username); - } - // 创建 LoginUser 对象 - return buildLoginUser(user); - } - - @Override - public LoginUser mockLogin(Long userId) { - // 获取用户编号对应的 AdminUserDO - AdminUserDO user = userService.getUser(userId); - if (user == null) { - throw new UsernameNotFoundException(String.valueOf(userId)); - } - createLoginLog(user.getUsername(), LoginLogTypeEnum.LOGIN_MOCK, LoginResultEnum.SUCCESS); - - // 创建 LoginUser 对象 - return buildLoginUser(user); - } - - @Override - public String login(AuthLoginReqVO reqVO, String userIp, String userAgent) { + public AuthLoginRespVO login(AuthLoginReqVO reqVO) { // 判断验证码是否正确 verifyCaptcha(reqVO); // 使用账号密码,进行登录 - LoginUser loginUser = login0(reqVO.getUsername(), reqVO.getPassword()); + AdminUserDO user = login0(reqVO.getUsername(), reqVO.getPassword()); - // 缓存登陆用户到 Redis 中,返回 Token 令牌 - return createUserSessionAfterLoginSuccess(loginUser, LoginLogTypeEnum.LOGIN_USERNAME, userIp, userAgent); + // 创建 Token 令牌,记录登录日志 + return createTokenAfterLoginSuccess(user.getId(), reqVO.getUsername(), LoginLogTypeEnum.LOGIN_USERNAME); } @Override @@ -123,9 +84,9 @@ public class AdminAuthServiceImpl implements AdminAuthService { } @Override - public String smsLogin(AuthSmsLoginReqVO reqVO, String userIp, String userAgent) { + public AuthLoginRespVO smsLogin(AuthSmsLoginReqVO reqVO) { // 校验验证码 - smsCodeApi.useSmsCode(AuthConvert.INSTANCE.convert(reqVO, SmsSceneEnum.ADMIN_MEMBER_LOGIN.getScene(), userIp)); + smsCodeApi.useSmsCode(AuthConvert.INSTANCE.convert(reqVO, SmsSceneEnum.ADMIN_MEMBER_LOGIN.getScene(), getClientIP())); // 获得用户信息 AdminUserDO user = userService.getUserByMobile(reqVO.getMobile()); @@ -133,14 +94,12 @@ public class AdminAuthServiceImpl implements AdminAuthService { throw exception(USER_NOT_EXISTS); } - // 创建 LoginUser 对象 - LoginUser loginUser = buildLoginUser(user); - // 缓存登陆用户到 Redis 中,返回 sessionId 编号 - return createUserSessionAfterLoginSuccess(loginUser, LoginLogTypeEnum.LOGIN_MOBILE, userIp, userAgent); + return createTokenAfterLoginSuccess(user.getId(), reqVO.getMobile(), LoginLogTypeEnum.LOGIN_MOBILE); } - private void verifyCaptcha(AuthLoginReqVO reqVO) { + @VisibleForTesting + void verifyCaptcha(AuthLoginReqVO reqVO) { // 如果验证码关闭,则不进行校验 if (!captchaService.isCaptchaEnable()) { return; @@ -152,53 +111,47 @@ public class AdminAuthServiceImpl implements AdminAuthService { String code = captchaService.getCaptchaCode(reqVO.getUuid()); if (code == null) { // 创建登录失败日志(验证码不存在) - this.createLoginLog(reqVO.getUsername(), logTypeEnum, LoginResultEnum.CAPTCHA_NOT_FOUND); + createLoginLog(null, reqVO.getUsername(), logTypeEnum, LoginResultEnum.CAPTCHA_NOT_FOUND); throw exception(AUTH_LOGIN_CAPTCHA_NOT_FOUND); } // 验证码不正确 if (!code.equals(reqVO.getCode())) { // 创建登录失败日志(验证码不正确) - this.createLoginLog(reqVO.getUsername(), logTypeEnum, LoginResultEnum.CAPTCHA_CODE_ERROR); + createLoginLog(null, reqVO.getUsername(), logTypeEnum, LoginResultEnum.CAPTCHA_CODE_ERROR); throw exception(AUTH_LOGIN_CAPTCHA_CODE_ERROR); } // 正确,所以要删除下验证码 captchaService.deleteCaptchaCode(reqVO.getUuid()); } - private LoginUser login0(String username, String password) { + @VisibleForTesting + AdminUserDO login0(String username, String password) { final LoginLogTypeEnum logTypeEnum = LoginLogTypeEnum.LOGIN_USERNAME; - // 用户验证 - Authentication authentication; - try { - // 调用 Spring Security 的 AuthenticationManager#authenticate(...) 方法,使用账号密码进行认证 - // 在其内部,会调用到 loadUserByUsername 方法,获取 User 信息 - authentication = authenticationManager.authenticate(new MultiUsernamePasswordAuthenticationToken( - username, password, getUserType())); - } catch (BadCredentialsException badCredentialsException) { - this.createLoginLog(username, logTypeEnum, LoginResultEnum.BAD_CREDENTIALS); + // 校验账号是否存在 + AdminUserDO user = userService.getUserByUsername(username); + if (user == null) { + createLoginLog(null, username, logTypeEnum, LoginResultEnum.BAD_CREDENTIALS); throw exception(AUTH_LOGIN_BAD_CREDENTIALS); - } catch (DisabledException disabledException) { - this.createLoginLog(username, logTypeEnum, LoginResultEnum.USER_DISABLED); - throw exception(AUTH_LOGIN_USER_DISABLED); - } catch (AuthenticationException authenticationException) { - log.error("[login0][username({}) 发生未知异常]", username, authenticationException); - this.createLoginLog(username, logTypeEnum, LoginResultEnum.UNKNOWN_ERROR); - throw exception(AUTH_LOGIN_FAIL_UNKNOWN); } - Assert.notNull(authentication.getPrincipal(), "Principal 不会为空"); - return (LoginUser) authentication.getPrincipal(); + if (!userService.isPasswordMatch(password, user.getPassword())) { + createLoginLog(user.getId(), username, logTypeEnum, LoginResultEnum.BAD_CREDENTIALS); + throw exception(AUTH_LOGIN_BAD_CREDENTIALS); + } + // 校验是否禁用 + if (ObjectUtil.notEqual(user.getStatus(), CommonStatusEnum.ENABLE.getStatus())) { + createLoginLog(user.getId(), username, logTypeEnum, LoginResultEnum.USER_DISABLED); + throw exception(AUTH_LOGIN_USER_DISABLED); + } + return user; } - private void createLoginLog(String username, LoginLogTypeEnum logTypeEnum, LoginResultEnum loginResult) { - // 获得用户 - AdminUserDO user = userService.getUserByUsername(username); + private void createLoginLog(Long userId, String username, + LoginLogTypeEnum logTypeEnum, LoginResultEnum loginResult) { // 插入登录日志 LoginLogCreateReqDTO reqDTO = new LoginLogCreateReqDTO(); reqDTO.setLogType(logTypeEnum.getType()); reqDTO.setTraceId(TracerUtils.getTraceId()); - if (user != null) { - reqDTO.setUserId(user.getId()); - } + reqDTO.setUserId(userId); reqDTO.setUserType(getUserType().getValue()); reqDTO.setUsername(username); reqDTO.setUserAgent(ServletUtils.getUserAgent()); @@ -206,23 +159,13 @@ public class AdminAuthServiceImpl implements AdminAuthService { reqDTO.setResult(loginResult.getResult()); loginLogService.createLoginLog(reqDTO); // 更新最后登录时间 - if (user != null && Objects.equals(LoginResultEnum.SUCCESS.getResult(), loginResult.getResult())) { - userService.updateUserLogin(user.getId(), ServletUtils.getClientIP()); + if (userId != null && Objects.equals(LoginResultEnum.SUCCESS.getResult(), loginResult.getResult())) { + userService.updateUserLogin(userId, ServletUtils.getClientIP()); } } - /** - * 获得 User 拥有的角色编号数组 - * - * @param userId 用户编号 - * @return 角色编号数组 - */ - private Set getUserRoleIds(Long userId) { - return permissionService.getUserRoleIds(userId, singleton(CommonStatusEnum.ENABLE.getStatus())); - } - @Override - public String socialQuickLogin(AuthSocialQuickLoginReqVO reqVO, String userIp, String userAgent) { + public AuthLoginRespVO socialQuickLogin(AuthSocialQuickLoginReqVO reqVO) { // 使用 code 授权码,进行登录。然后,获得到绑定的用户编号 Long userId = socialUserService.getBindUserId(UserTypeEnum.ADMIN.getValue(), reqVO.getType(), reqVO.getCode(), reqVO.getState()); @@ -230,105 +173,82 @@ public class AdminAuthServiceImpl implements AdminAuthService { throw exception(AUTH_THIRD_LOGIN_NOT_BIND); } - // 自动登录 + // 获得用户 AdminUserDO user = userService.getUser(userId); if (user == null) { throw exception(USER_NOT_EXISTS); } - // 创建 LoginUser 对象 - LoginUser loginUser = buildLoginUser(user); - - // 缓存登录用户到 Redis 中,返回 Token 令牌 - return createUserSessionAfterLoginSuccess(loginUser, LoginLogTypeEnum.LOGIN_SOCIAL, userIp, userAgent); + // 创建 Token 令牌,记录登录日志 + return createTokenAfterLoginSuccess(user.getId(), null, LoginLogTypeEnum.LOGIN_SOCIAL); } @Override - public String socialBindLogin(AuthSocialBindLoginReqVO reqVO, String userIp, String userAgent) { + public AuthLoginRespVO socialBindLogin(AuthSocialBindLoginReqVO reqVO) { // 使用账号密码,进行登录。 - LoginUser loginUser = login0(reqVO.getUsername(), reqVO.getPassword()); + AdminUserDO user = login0(reqVO.getUsername(), reqVO.getPassword()); // 绑定社交用户 - socialUserService.bindSocialUser(AuthConvert.INSTANCE.convert(loginUser.getId(), getUserType().getValue(), reqVO)); + socialUserService.bindSocialUser(AuthConvert.INSTANCE.convert(user.getId(), getUserType().getValue(), reqVO)); - // 缓存登录用户到 Redis 中,返回 Token 令牌 - return createUserSessionAfterLoginSuccess(loginUser, LoginLogTypeEnum.LOGIN_SOCIAL, userIp, userAgent); - } - - private String createUserSessionAfterLoginSuccess(LoginUser loginUser, LoginLogTypeEnum logType, String userIp, String userAgent) { - // 插入登陆日志 - createLoginLog(loginUser.getUsername(), logType, LoginResultEnum.SUCCESS); - // 缓存登录用户到 Redis 中,返回 Token 令牌 - return userSessionService.createUserSession(loginUser, userIp, userAgent); + // 创建 Token 令牌,记录登录日志 + return createTokenAfterLoginSuccess(user.getId(), reqVO.getUsername(), LoginLogTypeEnum.LOGIN_SOCIAL); } @Override - public void logout(String token) { - // 查询用户信息 - LoginUser loginUser = userSessionService.getLoginUser(token); - if (loginUser == null) { + public AuthLoginRespVO refreshToken(String refreshToken) { + OAuth2AccessTokenDO accessTokenDO = oauth2TokenService.refreshAccessToken(refreshToken, OAuth2ClientIdEnum.DEFAULT.getId()); + return AuthConvert.INSTANCE.convert(accessTokenDO); + } + + private AuthLoginRespVO createTokenAfterLoginSuccess(Long userId, String username, LoginLogTypeEnum logType) { + // 插入登陆日志 + createLoginLog(userId, username, logType, LoginResultEnum.SUCCESS); + // 创建访问令牌 + OAuth2AccessTokenDO accessTokenDO = oauth2TokenService.createAccessToken(userId, getUserType().getValue(), + OAuth2ClientIdEnum.DEFAULT.getId()); + // 构建返回结果 + return AuthConvert.INSTANCE.convert(accessTokenDO); + } + + @Override + public void logout(String token, Integer logType) { + // 删除访问令牌 + OAuth2AccessTokenDO accessTokenDO = oauth2TokenService.removeAccessToken(token); + if (accessTokenDO == null) { return; } - // 删除 session - userSessionService.deleteUserSession(token); - // 记录登出日志 - createLogoutLog(loginUser.getId(), loginUser.getUsername()); + // 删除成功,则记录登出日志 + createLogoutLog(accessTokenDO.getUserId(), accessTokenDO.getUserType(), logType); } - @Override - public UserTypeEnum getUserType() { - return UserTypeEnum.ADMIN; - } - - private void createLogoutLog(Long userId, String username) { + private void createLogoutLog(Long userId, Integer userType, Integer logType) { LoginLogCreateReqDTO reqDTO = new LoginLogCreateReqDTO(); - reqDTO.setLogType(LoginLogTypeEnum.LOGOUT_SELF.getType()); + reqDTO.setLogType(logType); reqDTO.setTraceId(TracerUtils.getTraceId()); reqDTO.setUserId(userId); - reqDTO.setUserType(getUserType().getValue()); - reqDTO.setUsername(username); + reqDTO.setUserType(userType); + if (ObjectUtil.notEqual(getUserType(), userType)) { + reqDTO.setUsername(getUsername(userId)); + } else { + reqDTO.setUsername(memberService.getMemberUserMobile(userId)); + } reqDTO.setUserAgent(ServletUtils.getUserAgent()); reqDTO.setUserIp(ServletUtils.getClientIP()); reqDTO.setResult(LoginResultEnum.SUCCESS.getResult()); loginLogService.createLoginLog(reqDTO); } - @Override - public LoginUser verifyTokenAndRefresh(String token) { - // 获得 LoginUser - LoginUser loginUser = userSessionService.getLoginUser(token); - if (loginUser == null) { + private String getUsername(Long userId) { + if (userId == null) { return null; } - // 刷新 LoginUser 缓存 - return this.refreshLoginUserCache(token, loginUser); + AdminUserDO user = userService.getUser(userId); + return user != null ? user.getUsername() : null; } - private LoginUser refreshLoginUserCache(String token, LoginUser loginUser) { - // 每 1/3 的 Session 超时时间,刷新 LoginUser 缓存 - if (System.currentTimeMillis() - loginUser.getUpdateTime().getTime() < - userSessionService.getSessionTimeoutMillis() / 3) { - return loginUser; - } - - // 重新加载 AdminUserDO 信息 - AdminUserDO user = userService.getUser(loginUser.getId()); - if (user == null || CommonStatusEnum.DISABLE.getStatus().equals(user.getStatus())) { - throw exception(AUTH_TOKEN_EXPIRED); // 校验 token 时,用户被禁用的情况下,也认为 token 过期,方便前端跳转到登录界面 - } - - // 刷新 LoginUser 缓存 - LoginUser newLoginUser= this.buildLoginUser(user); - userSessionService.refreshUserSession(token, newLoginUser); - return newLoginUser; - } - - private LoginUser buildLoginUser(AdminUserDO user) { - LoginUser loginUser = AuthConvert.INSTANCE.convert(user); - // 补全字段 - loginUser.setDeptId(user.getDeptId()); - loginUser.setRoleIds(this.getUserRoleIds(loginUser.getId())); - return loginUser; + private UserTypeEnum getUserType() { + return UserTypeEnum.ADMIN; } } diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/auth/AdminOAuth2Service.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/auth/AdminOAuth2Service.java new file mode 100644 index 000000000..5c10cdfb8 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/auth/AdminOAuth2Service.java @@ -0,0 +1,14 @@ +package cn.iocoder.yudao.module.system.service.auth; + +/** + * 管理后台的 OAuth2 Service 接口 + * + * 将自身的 AdminUser 用户,授权给第三方应用,采用 OAuth2.0 的协议。 + * + * 问题:为什么自身也作为一个第三方应用,也走这套流程呢? + * 回复:当然可以这么做,采用 Implicit 模式。考虑到大多数开发者使用不到这个特性,OAuth2.0 毕竟有一定学习成本,所以暂时没有采取这种方式。 + * + * @author 芋道源码 + */ +public interface AdminOAuth2Service { +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/auth/OAuth2ClientService.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/auth/OAuth2ClientService.java new file mode 100644 index 000000000..8d97ded3d --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/auth/OAuth2ClientService.java @@ -0,0 +1,66 @@ +package cn.iocoder.yudao.module.system.service.auth; + +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; +import cn.iocoder.yudao.module.system.controller.admin.auth.vo.client.OAuth2ClientUpdateReqVO; +import cn.iocoder.yudao.module.system.dal.dataobject.auth.OAuth2ClientDO; + +import javax.validation.Valid; + +/** + * OAuth2.0 Client Service 接口 + * + * 从功能上,和 JdbcClientDetailsService 的功能,提供客户端的操作 + * + * @author 芋道源码 + */ +public interface OAuth2ClientService { + + /** + * 创建OAuth2 客户端 + * + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createOAuth2Client(@Valid OAuth2ClientCreateReqVO createReqVO); + + /** + * 更新OAuth2 客户端 + * + * @param updateReqVO 更新信息 + */ + void updateOAuth2Client(@Valid OAuth2ClientUpdateReqVO updateReqVO); + + /** + * 删除OAuth2 客户端 + * + * @param id 编号 + */ + void deleteOAuth2Client(Long id); + + /** + * 获得OAuth2 客户端 + * + * @param id 编号 + * @return OAuth2 客户端 + */ + OAuth2ClientDO getOAuth2Client(Long id); + + /** + * 获得OAuth2 客户端分页 + * + * @param pageReqVO 分页查询 + * @return OAuth2 客户端分页 + */ + PageResult getOAuth2ClientPage(OAuth2ClientPageReqVO pageReqVO); + + /** + * 从缓存中,校验客户端是否合法 + * + * @param id 客户端编号 + * @return 客户端 + */ + OAuth2ClientDO validOAuthClientFromCache(Long id); + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/auth/OAuth2ClientServiceImpl.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/auth/OAuth2ClientServiceImpl.java new file mode 100644 index 000000000..51cde3f0f --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/auth/OAuth2ClientServiceImpl.java @@ -0,0 +1,77 @@ +package cn.iocoder.yudao.module.system.service.auth; + +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; +import cn.iocoder.yudao.module.system.controller.admin.auth.vo.client.OAuth2ClientUpdateReqVO; +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 org.springframework.stereotype.Service; + +import javax.annotation.Resource; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.OAUTH2_CLIENT_NOT_EXISTS; + +/** + * OAuth2.0 Client Service 实现类 + * + * @author 芋道源码 + */ +@Service +public class OAuth2ClientServiceImpl implements OAuth2ClientService { + + @Resource + private OAuth2ClientMapper oauth2ClientMapper; + + @Override + public Long createOAuth2Client(OAuth2ClientCreateReqVO createReqVO) { + // 插入 + OAuth2ClientDO oAuth2Client = OAuth2ClientConvert.INSTANCE.convert(createReqVO); + oauth2ClientMapper.insert(oAuth2Client); + // 返回 + return oAuth2Client.getId(); + } + + @Override + public void updateOAuth2Client(OAuth2ClientUpdateReqVO updateReqVO) { + // 校验存在 + this.validateOAuth2ClientExists(updateReqVO.getId()); + // 更新 + OAuth2ClientDO updateObj = OAuth2ClientConvert.INSTANCE.convert(updateReqVO); + oauth2ClientMapper.updateById(updateObj); + } + + @Override + public void deleteOAuth2Client(Long id) { + // 校验存在 + this.validateOAuth2ClientExists(id); + // 删除 + oauth2ClientMapper.deleteById(id); + } + + private void validateOAuth2ClientExists(Long id) { + if (oauth2ClientMapper.selectById(id) == null) { + throw exception(OAUTH2_CLIENT_NOT_EXISTS); + } + } + + @Override + public OAuth2ClientDO getOAuth2Client(Long id) { + return oauth2ClientMapper.selectById(id); + } + + @Override + public PageResult getOAuth2ClientPage(OAuth2ClientPageReqVO pageReqVO) { + return oauth2ClientMapper.selectPage(pageReqVO); + } + + @Override + public OAuth2ClientDO validOAuthClientFromCache(Long id) { + return new OAuth2ClientDO().setId(id) + .setAccessTokenValiditySeconds(60 * 30) + .setRefreshTokenValiditySeconds(60 * 60 * 24 * 30); + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/auth/OAuth2CodeService.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/auth/OAuth2CodeService.java new file mode 100644 index 000000000..f8a788eb0 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/auth/OAuth2CodeService.java @@ -0,0 +1,11 @@ +package cn.iocoder.yudao.module.system.service.auth; + +/** + * OAuth2.0 授权码 Service 接口 + * + * 从功能上,和 Spring Security OAuth 的 JdbcAuthorizationCodeServices 的功能,提供授权码的操作 + * + * @author 芋道源码 + */ +public class OAuth2CodeService { +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/auth/OAuth2TokenService.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/auth/OAuth2TokenService.java new file mode 100644 index 000000000..541d2c7c7 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/auth/OAuth2TokenService.java @@ -0,0 +1,77 @@ +package cn.iocoder.yudao.module.system.service.auth; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.system.controller.admin.auth.vo.token.OAuth2AccessTokenPageReqVO; +import cn.iocoder.yudao.module.system.dal.dataobject.auth.OAuth2AccessTokenDO; + +/** + * OAuth2.0 Token Service 接口 + * + * 从功能上,和 Spring Security OAuth 的 DefaultTokenServices + JdbcTokenStore 的功能,提供访问令牌、刷新令牌的操作 + * + * @author 芋道源码 + */ +public interface OAuth2TokenService { + + /** + * 创建访问令牌 + * 注意:该流程中,会包含创建刷新令牌的创建 + * + * 参考 DefaultTokenServices 的 createAccessToken 方法 + * + * @param userId 用户编号 + * @param userType 用户类型 + * @param clientId 客户端编号 + * @return 访问令牌的信息 + */ + OAuth2AccessTokenDO createAccessToken(Long userId, Integer userType, Long clientId); + + /** + * 刷新访问令牌 + * + * 参考 DefaultTokenServices 的 refreshAccessToken 方法 + * + * @param refreshToken 刷新令牌 + * @param clientId 客户端编号 + * @return 访问令牌的信息 + */ + OAuth2AccessTokenDO refreshAccessToken(String refreshToken, Long clientId); + + /** + * 获得访问令牌 + * + * 参考 DefaultTokenServices 的 getAccessToken 方法 + * + * @param accessToken 访问令牌 + * @return 访问令牌的信息 + */ + OAuth2AccessTokenDO getAccessToken(String accessToken); + + /** + * 校验访问令牌 + * + * @param accessToken 访问令牌 + * @return 访问令牌的信息 + */ + OAuth2AccessTokenDO checkAccessToken(String accessToken); + + /** + * 移除访问令牌 + * 注意:该流程中,会移除相关的刷新令牌 + * + * 参考 DefaultTokenServices 的 revokeToken 方法 + * + * @param accessToken 刷新令牌 + * @return 访问令牌的信息 + */ + OAuth2AccessTokenDO removeAccessToken(String accessToken); + + /** + * 获得访问令牌分页 + * + * @param reqVO 请求 + * @return 访问令牌分页 + */ + PageResult getAccessTokenPage(OAuth2AccessTokenPageReqVO reqVO); + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/auth/OAuth2TokenServiceImpl.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/auth/OAuth2TokenServiceImpl.java new file mode 100644 index 000000000..f8d824690 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/auth/OAuth2TokenServiceImpl.java @@ -0,0 +1,163 @@ +package cn.iocoder.yudao.module.system.service.auth; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.IdUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.date.DateUtils; +import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder; +import cn.iocoder.yudao.module.system.controller.admin.auth.vo.token.OAuth2AccessTokenPageReqVO; +import cn.iocoder.yudao.module.system.dal.dataobject.auth.OAuth2AccessTokenDO; +import cn.iocoder.yudao.module.system.dal.dataobject.auth.OAuth2ClientDO; +import cn.iocoder.yudao.module.system.dal.dataobject.auth.OAuth2RefreshTokenDO; +import cn.iocoder.yudao.module.system.dal.mysql.auth.OAuth2AccessTokenMapper; +import cn.iocoder.yudao.module.system.dal.mysql.auth.OAuth2RefreshTokenMapper; +import cn.iocoder.yudao.module.system.dal.redis.auth.OAuth2AccessTokenRedisDAO; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import java.util.Calendar; +import java.util.List; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; + +/** + * OAuth2.0 Token Service 实现类 + * + * @author 芋道源码 + */ +@Service +public class OAuth2TokenServiceImpl implements OAuth2TokenService { + + @Resource + private OAuth2AccessTokenMapper oauth2AccessTokenMapper; + @Resource + private OAuth2RefreshTokenMapper oauth2RefreshTokenMapper; + + @Resource + private OAuth2AccessTokenRedisDAO oauth2AccessTokenRedisDAO; + + @Resource + private OAuth2ClientService oauth2ClientService; + + @Override + @Transactional + public OAuth2AccessTokenDO createAccessToken(Long userId, Integer userType, Long clientId) { + OAuth2ClientDO clientDO = oauth2ClientService.validOAuthClientFromCache(clientId); + // 创建刷新令牌 + OAuth2RefreshTokenDO refreshTokenDO = createOAuth2RefreshToken(userId, userType, clientDO); + // 创建访问令牌 + return createOAuth2AccessToken(refreshTokenDO, clientDO); + } + + @Override + public OAuth2AccessTokenDO refreshAccessToken(String refreshToken, Long clientId) { + // 查询访问令牌 + OAuth2RefreshTokenDO refreshTokenDO = oauth2RefreshTokenMapper.selectByRefreshToken(refreshToken); + if (refreshTokenDO == null) { + throw exception(GlobalErrorCodeConstants.BAD_REQUEST, "无效的刷新令牌"); + } + + // 校验 Client 匹配 + OAuth2ClientDO clientDO = oauth2ClientService.validOAuthClientFromCache(clientId); + if (ObjectUtil.notEqual(clientId, refreshTokenDO.getClientId())) { + throw exception(GlobalErrorCodeConstants.BAD_REQUEST, "刷新令牌的客户端编号不正确"); + } + + // 移除相关的访问令牌 + List accessTokenDOs = oauth2AccessTokenMapper.selectListByRefreshToken(refreshToken); + if (CollUtil.isNotEmpty(accessTokenDOs)) { + oauth2AccessTokenMapper.deleteBatchIds(convertSet(accessTokenDOs, OAuth2AccessTokenDO::getId)); + oauth2AccessTokenRedisDAO.deleteList(convertSet(accessTokenDOs, OAuth2AccessTokenDO::getAccessToken)); + } + + // 已过期的情况下,删除刷新令牌 + if (DateUtils.isExpired(refreshTokenDO.getExpiresTime())) { + oauth2AccessTokenMapper.deleteById(refreshTokenDO.getId()); + throw exception(GlobalErrorCodeConstants.UNAUTHORIZED, "刷新令牌已过期"); + } + + // 创建访问令牌 + return createOAuth2AccessToken(refreshTokenDO, clientDO); + } + + @Override + public OAuth2AccessTokenDO getAccessToken(String accessToken) { + // 优先从 Redis 中获取 + OAuth2AccessTokenDO accessTokenDO = oauth2AccessTokenRedisDAO.get(accessToken); + if (accessTokenDO != null) { + return accessTokenDO; + } + + // 获取不到,从 MySQL 中获取 + accessTokenDO = oauth2AccessTokenMapper.selectById(accessToken); + // 如果在 MySQL 存在,则往 Redis 中写入 + if (accessTokenDO != null && !DateUtils.isExpired(accessTokenDO.getExpiresTime())) { + oauth2AccessTokenRedisDAO.set(accessTokenDO); + } + return accessTokenDO; + } + + @Override + public OAuth2AccessTokenDO checkAccessToken(String accessToken) { + OAuth2AccessTokenDO accessTokenDO = getAccessToken(accessToken); + if (accessTokenDO == null) { + throw exception(GlobalErrorCodeConstants.UNAUTHORIZED, "访问令牌不存在"); + } + if (DateUtils.isExpired(accessTokenDO.getExpiresTime())) { + throw exception(GlobalErrorCodeConstants.UNAUTHORIZED, "访问令牌已过期"); + } + return accessTokenDO; + } + + @Override + public OAuth2AccessTokenDO removeAccessToken(String accessToken) { + // 删除访问令牌 + OAuth2AccessTokenDO accessTokenDO = oauth2AccessTokenMapper.selectByAccessToken(accessToken); + if (accessTokenDO == null) { + return null; + } + oauth2AccessTokenMapper.deleteById(accessTokenDO.getId()); + oauth2AccessTokenRedisDAO.delete(accessToken); + // 删除刷新令牌 + oauth2RefreshTokenMapper.deleteByRefreshToken(accessTokenDO.getRefreshToken()); + return accessTokenDO; + } + + @Override + public PageResult getAccessTokenPage(OAuth2AccessTokenPageReqVO reqVO) { + return oauth2AccessTokenMapper.selectPage(reqVO); + } + + private OAuth2AccessTokenDO createOAuth2AccessToken(OAuth2RefreshTokenDO refreshTokenDO, OAuth2ClientDO clientDO) { + OAuth2AccessTokenDO accessTokenDO = new OAuth2AccessTokenDO().setAccessToken(generateAccessToken()) + .setUserId(refreshTokenDO.getUserId()).setUserType(refreshTokenDO.getUserType()).setClientId(clientDO.getId()) + .setRefreshToken(refreshTokenDO.getRefreshToken()) + .setExpiresTime(DateUtils.addDate(Calendar.SECOND, clientDO.getAccessTokenValiditySeconds())); + accessTokenDO.setTenantId(TenantContextHolder.getTenantId()); // 手动设置租户编号,避免缓存到 Redis 的时候,无对应的租户编号 + oauth2AccessTokenMapper.insert(accessTokenDO); + // 记录到 Redis 中 + oauth2AccessTokenRedisDAO.set(accessTokenDO); + return accessTokenDO; + } + + private OAuth2RefreshTokenDO createOAuth2RefreshToken(Long userId, Integer userType, OAuth2ClientDO clientDO) { + OAuth2RefreshTokenDO refreshToken = new OAuth2RefreshTokenDO().setRefreshToken(generateRefreshToken()) + .setUserId(userId).setUserType(userType).setClientId(clientDO.getId()) + .setExpiresTime(DateUtils.addDate(Calendar.SECOND, clientDO.getRefreshTokenValiditySeconds())); + oauth2RefreshTokenMapper.insert(refreshToken); + return refreshToken; + } + + private static String generateAccessToken() { + return IdUtil.fastSimpleUUID(); + } + + private static String generateRefreshToken() { + return IdUtil.fastSimpleUUID(); + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/auth/UserSessionService.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/auth/UserSessionService.java deleted file mode 100644 index 844cef250..000000000 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/auth/UserSessionService.java +++ /dev/null @@ -1,77 +0,0 @@ -package cn.iocoder.yudao.module.system.service.auth; - -import cn.iocoder.yudao.framework.security.core.LoginUser; -import cn.iocoder.yudao.module.system.controller.admin.auth.vo.session.UserSessionPageReqVO; -import cn.iocoder.yudao.module.system.dal.dataobject.auth.UserSessionDO; -import cn.iocoder.yudao.framework.common.pojo.PageResult; - -/** - * 在线用户 Session Service 接口 - * - * @author 芋道源码 - */ -public interface UserSessionService { - - /** - * 获得在线用户分页列表 - * - * @param reqVO 分页条件 - * @return 份额与列表 - */ - PageResult getUserSessionPage(UserSessionPageReqVO reqVO); - - /** - * 移除超时的在线用户 - * - * @return {@link Long } 移出的超时用户数量 - **/ - long deleteTimeoutSession(); - - /** - * 创建在线用户 Session - * - * @param loginUser 登录用户 - * @param userIp 用户 IP - * @param userAgent 用户 UA - * @return Token 令牌 - */ - String createUserSession(LoginUser loginUser, String userIp, String userAgent); - - /** - * 刷新在线用户 Session 的更新时间 - * - * @param token 令牌 - * @param loginUser 登录用户 - */ - void refreshUserSession(String token, LoginUser loginUser); - - /** - * 删除在线用户 Session - * - * @param token token 令牌 - */ - void deleteUserSession(String token); - - /** - * 删除在线用户 Session - * - * @param id 编号 - */ - void deleteUserSession(Long id); - - /** - * 获得 Token 对应的在线用户 - * - * @param token 令牌 - * @return 在线用户 - */ - LoginUser getLoginUser(String token); - - /** - * 获得 Session 超时时间,单位:毫秒 - * - * @return 超时时间 - */ - Long getSessionTimeoutMillis(); - -} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/auth/UserSessionServiceImpl.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/auth/UserSessionServiceImpl.java deleted file mode 100644 index 333e54c69..000000000 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/auth/UserSessionServiceImpl.java +++ /dev/null @@ -1,177 +0,0 @@ -package cn.iocoder.yudao.module.system.service.auth; - -import cn.hutool.core.collection.CollUtil; -import cn.hutool.core.util.IdUtil; -import cn.hutool.core.util.StrUtil; -import cn.iocoder.yudao.framework.common.pojo.PageResult; -import cn.iocoder.yudao.framework.common.util.monitor.TracerUtils; -import cn.iocoder.yudao.framework.security.config.SecurityProperties; -import cn.iocoder.yudao.framework.security.core.LoginUser; -import cn.iocoder.yudao.module.system.api.logger.dto.LoginLogCreateReqDTO; -import cn.iocoder.yudao.module.system.controller.admin.auth.vo.session.UserSessionPageReqVO; -import cn.iocoder.yudao.module.system.dal.dataobject.auth.UserSessionDO; -import cn.iocoder.yudao.module.system.dal.dataobject.user.AdminUserDO; -import cn.iocoder.yudao.module.system.dal.mysql.auth.UserSessionMapper; -import cn.iocoder.yudao.module.system.dal.redis.auth.LoginUserRedisDAO; -import cn.iocoder.yudao.module.system.enums.logger.LoginLogTypeEnum; -import cn.iocoder.yudao.module.system.enums.logger.LoginResultEnum; -import cn.iocoder.yudao.module.system.service.logger.LoginLogService; -import cn.iocoder.yudao.module.system.service.user.AdminUserService; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Service; - -import javax.annotation.Resource; -import java.time.Duration; -import java.util.Collection; -import java.util.Date; -import java.util.List; - -import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; -import static cn.iocoder.yudao.framework.common.util.date.DateUtils.addTime; - -/** - * 在线用户 Session Service 实现类 - * - * @author 芋道源码 - */ -@Slf4j -@Service -public class UserSessionServiceImpl implements UserSessionService { - - @Resource - private UserSessionMapper userSessionMapper; - - @Resource - private AdminUserService userService; - @Resource - private LoginLogService loginLogService; - - @Resource - private LoginUserRedisDAO loginUserRedisDAO; - - @Resource - private SecurityProperties securityProperties; - - @Override - public PageResult getUserSessionPage(UserSessionPageReqVO reqVO) { - // 处理基于用户昵称的查询 - Collection userIds = null; - if (StrUtil.isNotEmpty(reqVO.getUsername())) { - userIds = convertSet(userService.getUsersByUsername(reqVO.getUsername()), AdminUserDO::getId); - if (CollUtil.isEmpty(userIds)) { - return PageResult.empty(); - } - } - return userSessionMapper.selectPage(reqVO, userIds); - } - - @Override - public long deleteTimeoutSession() { - // 获取 db 里已经超时的用户列表 - List timeoutSessions = userSessionMapper.selectListBySessionTimoutLt(); - if (CollUtil.isEmpty(timeoutSessions)) { - return 0L; - } - - // 由于过期的用户一般不多,所以顺序遍历,进行清理 - int count = 0; - for (UserSessionDO session : timeoutSessions) { - // 基于 Redis 二次判断,同时也保证 Redis Key 的立即过期,避免延迟导致浪费内存空间 - if (loginUserRedisDAO.exists(session.getToken())) { - continue; - } - userSessionMapper.deleteById(session.getId()); - // 记录退出日志 - createLogoutLog(session, LoginLogTypeEnum.LOGOUT_TIMEOUT); - count++; - } - return count; - } - - private void createLogoutLog(UserSessionDO session, LoginLogTypeEnum type) { - LoginLogCreateReqDTO reqDTO = new LoginLogCreateReqDTO(); - reqDTO.setLogType(type.getType()); - reqDTO.setTraceId(TracerUtils.getTraceId()); - reqDTO.setUserId(session.getUserId()); - reqDTO.setUserType(session.getUserType()); - reqDTO.setUsername(session.getUsername()); - reqDTO.setUserAgent(session.getUserAgent()); - reqDTO.setUserIp(session.getUserIp()); - reqDTO.setResult(LoginResultEnum.SUCCESS.getResult()); - loginLogService.createLoginLog(reqDTO); - } - - @Override - public String createUserSession(LoginUser loginUser, String userIp, String userAgent) { - // 生成 Session 编号 - String token = generateToken(); - // 写入 Redis 缓存 - loginUser.setUpdateTime(new Date()); - loginUserRedisDAO.set(token, loginUser); - // 写入 DB 中 - UserSessionDO userSession = UserSessionDO.builder().token(token) - .userId(loginUser.getId()).userType(loginUser.getUserType()) - .userIp(userIp).userAgent(userAgent).username(loginUser.getUsername()) - .sessionTimeout(addTime(Duration.ofMillis(getSessionTimeoutMillis()))) - .build(); - userSessionMapper.insert(userSession); - // 返回 Token 令牌 - return token; - } - - @Override - public void refreshUserSession(String token, LoginUser loginUser) { - // 写入 Redis 缓存 - loginUser.setUpdateTime(new Date()); - loginUserRedisDAO.set(token, loginUser); - // 更新 DB 中 - UserSessionDO updateObj = UserSessionDO.builder().build(); - updateObj.setUsername(loginUser.getUsername()); - updateObj.setUpdateTime(new Date()); - updateObj.setSessionTimeout(addTime(Duration.ofMillis(getSessionTimeoutMillis()))); - userSessionMapper.updateByToken(token, updateObj); - } - - @Override - public void deleteUserSession(String token) { - // 删除 Redis 缓存 - loginUserRedisDAO.delete(token); - // 删除 DB 记录 - userSessionMapper.deleteByToken(token); - // 无需记录日志,因为退出那已经记录 - } - - @Override - public void deleteUserSession(Long id) { - UserSessionDO session = userSessionMapper.selectById(id); - if (session == null) { - return; - } - // 删除 Redis 缓存 - loginUserRedisDAO.delete(session.getToken()); - // 删除 DB 记录 - userSessionMapper.deleteById(id); - // 记录退出日志 - createLogoutLog(session, LoginLogTypeEnum.LOGOUT_DELETE); - } - - @Override - public LoginUser getLoginUser(String token) { - return loginUserRedisDAO.get(token); - } - - @Override - public Long getSessionTimeoutMillis() { - return securityProperties.getSessionTimeout().toMillis(); - } - - /** - * 生成 Token 令牌,目前采用 UUID 算法 - * - * @return Session 编号 - */ - private static String generateToken() { - return IdUtil.fastSimpleUUID(); - } - -} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/permission/PermissionServiceImpl.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/permission/PermissionServiceImpl.java index 08473d8cc..76e2055b3 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/permission/PermissionServiceImpl.java +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/permission/PermissionServiceImpl.java @@ -3,12 +3,12 @@ package cn.iocoder.yudao.module.system.service.permission; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.collection.CollectionUtil; import cn.hutool.core.util.ArrayUtil; +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; import cn.iocoder.yudao.framework.common.util.collection.MapUtils; import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import cn.iocoder.yudao.framework.datapermission.core.annotation.DataPermission; import cn.iocoder.yudao.framework.datapermission.core.dept.service.dto.DeptDataPermissionRespDTO; -import cn.iocoder.yudao.framework.security.core.LoginUser; -import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils; import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore; import cn.iocoder.yudao.module.system.dal.dataobject.dept.DeptDO; import cn.iocoder.yudao.module.system.dal.dataobject.permission.MenuDO; @@ -22,6 +22,8 @@ import cn.iocoder.yudao.module.system.dal.mysql.permission.UserRoleMapper; import cn.iocoder.yudao.module.system.enums.permission.DataScopeEnum; import cn.iocoder.yudao.module.system.mq.producer.permission.PermissionProducer; import cn.iocoder.yudao.module.system.service.dept.DeptService; +import cn.iocoder.yudao.module.system.service.user.AdminUserService; +import com.google.common.base.Suppliers; import com.google.common.collect.ImmutableMultimap; import com.google.common.collect.Multimap; import com.google.common.collect.Sets; @@ -36,6 +38,10 @@ import org.springframework.transaction.support.TransactionSynchronizationManager import javax.annotation.PostConstruct; import javax.annotation.Resource; import java.util.*; +import java.util.function.Supplier; + +import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; +import static java.util.Collections.singleton; /** * 权限 Service 实现类 @@ -46,11 +52,6 @@ import java.util.*; @Slf4j public class PermissionServiceImpl implements PermissionService { - /** - * LoginUser 的 Context 缓存 Key - */ - public static final String CONTEXT_KEY = PermissionServiceImpl.class.getSimpleName(); - /** * 定时执行 {@link #schedulePeriodicRefresh()} 的周期 * 因为已经通过 Redis Pub/Sub 机制,所以频率不需要高 @@ -93,6 +94,8 @@ public class PermissionServiceImpl implements PermissionService { private MenuService menuService; @Resource private DeptService deptService; + @Resource + private AdminUserService userService; @Resource private PermissionProducer permissionProducer; @@ -319,7 +322,7 @@ public class PermissionServiceImpl implements PermissionService { } // 获得当前登录的角色。如果为空,说明没有权限 - Set roleIds = SecurityFrameworkUtils.getLoginUserRoleIds(); + Set roleIds = getUserRoleIds(getLoginUserId(), singleton(CommonStatusEnum.ENABLE.getStatus())); if (CollUtil.isEmpty(roleIds)) { return false; } @@ -354,7 +357,7 @@ public class PermissionServiceImpl implements PermissionService { } // 获得当前登录的角色。如果为空,说明没有权限 - Set roleIds = SecurityFrameworkUtils.getLoginUserRoleIds(); + Set roleIds = getUserRoleIds(getLoginUserId(), singleton(CommonStatusEnum.ENABLE.getStatus())); if (CollUtil.isEmpty(roleIds)) { return false; } @@ -368,16 +371,18 @@ public class PermissionServiceImpl implements PermissionService { } @Override - public DeptDataPermissionRespDTO getDeptDataPermission(LoginUser loginUser) { - // 判断是否 context 已经缓存 - DeptDataPermissionRespDTO result = loginUser.getContext(CONTEXT_KEY, DeptDataPermissionRespDTO.class); - if (result != null) { + @DataPermission(enable = false) // 关闭数据权限,不然就会出现递归获取数据权限的问题 + public DeptDataPermissionRespDTO getDeptDataPermission(Long userId) { + DeptDataPermissionRespDTO result = new DeptDataPermissionRespDTO(); + // 获得用户的角色 + Set roleIds = getUserRoleIds(userId, singleton(CommonStatusEnum.ENABLE.getStatus())); + if (CollUtil.isEmpty(roleIds)) { return result; } - - // 创建 DeptDataPermissionRespDTO 对象 - result = new DeptDataPermissionRespDTO(); - List roles = roleService.getRolesFromCache(loginUser.getRoleIds()); + List roles = roleService.getRolesFromCache(roleIds); + // 获得用户的部门编号的缓存,通过 Guava 的 Suppliers 惰性求值,即有且仅有第一次发起 DB 的查询 + Supplier userDeptIdCache = Suppliers.memoize(() -> userService.getUser(userId).getDeptId()); + // 遍历每个角色,计算 for (RoleDO role : roles) { // 为空时,跳过 if (role.getDataScope() == null) { @@ -393,20 +398,20 @@ public class PermissionServiceImpl implements PermissionService { CollUtil.addAll(result.getDeptIds(), role.getDataScopeDeptIds()); // 自定义可见部门时,保证可以看到自己所在的部门。否则,一些场景下可能会有问题。 // 例如说,登录时,基于 t_user 的 username 查询会可能被 dept_id 过滤掉 - CollUtil.addAll(result.getDeptIds(), loginUser.getDeptId()); + CollUtil.addAll(result.getDeptIds(), userDeptIdCache.get()); continue; } // 情况三,DEPT_ONLY if (Objects.equals(role.getDataScope(), DataScopeEnum.DEPT_ONLY.getScope())) { - CollectionUtils.addIfNotNull(result.getDeptIds(), loginUser.getDeptId()); + CollectionUtils.addIfNotNull(result.getDeptIds(), userDeptIdCache.get()); continue; } // 情况四,DEPT_DEPT_AND_CHILD if (Objects.equals(role.getDataScope(), DataScopeEnum.DEPT_AND_CHILD.getScope())) { - List depts = deptService.getDeptsByParentIdFromCache(loginUser.getDeptId(), true); + List depts = deptService.getDeptsByParentIdFromCache(userDeptIdCache.get(), true); CollUtil.addAll(result.getDeptIds(), CollectionUtils.convertList(depts, DeptDO::getId)); - //添加本身部门id - CollUtil.addAll(result.getDeptIds(), loginUser.getDeptId()); + // 添加本身部门编号 + CollUtil.addAll(result.getDeptIds(), userDeptIdCache.get()); continue; } // 情况五,SELF @@ -415,11 +420,8 @@ public class PermissionServiceImpl implements PermissionService { continue; } // 未知情况,error log 即可 - log.error("[getDeptDataPermission][LoginUser({}) role({}) 无法处理]", loginUser.getId(), JsonUtils.toJsonString(result)); + log.error("[getDeptDataPermission][LoginUser({}) role({}) 无法处理]", userId, JsonUtils.toJsonString(result)); } - - // 添加到缓存,并返回 - loginUser.setContext(CONTEXT_KEY, result); return result; } diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/tenant/TenantServiceImpl.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/tenant/TenantServiceImpl.java index e0038ffcd..160ffe824 100755 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/tenant/TenantServiceImpl.java +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/tenant/TenantServiceImpl.java @@ -81,6 +81,7 @@ public class TenantServiceImpl implements TenantService { @Getter private volatile Date maxUpdateTime; + @SuppressWarnings("SpringJavaAutowiredFieldsWarningInspection") @Autowired(required = false) // 由于 yudao.tenant.enable 配置项,可以关闭多租户的功能,所以这里只能不强制注入 private TenantProperties tenantProperties; diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/user/AdminUserService.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/user/AdminUserService.java index d6a836f01..ae3245de2 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/user/AdminUserService.java +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/user/AdminUserService.java @@ -105,7 +105,6 @@ public interface AdminUserService { */ AdminUserDO getUserByMobile(String mobile); - /** * 获得用户分页列表 * @@ -209,4 +208,13 @@ public interface AdminUserService { */ List getUsersByStatus(Integer status); + /** + * 判断密码是否匹配 + * + * @param rawPassword 未加密的密码 + * @param encodedPassword 加密后的密码 + * @return 是否匹配 + */ + boolean isPasswordMatch(String rawPassword, String encodedPassword); + } diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/user/AdminUserServiceImpl.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/user/AdminUserServiceImpl.java index 44934c6fa..57fd49a7d 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/user/AdminUserServiceImpl.java +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/user/AdminUserServiceImpl.java @@ -25,6 +25,7 @@ import cn.iocoder.yudao.module.system.service.tenant.TenantService; import com.google.common.annotations.VisibleForTesting; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Lazy; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -61,6 +62,7 @@ public class AdminUserServiceImpl implements AdminUserService { @Resource private PasswordEncoder passwordEncoder; @Resource + @Lazy // 延迟,避免循环依赖报错 private TenantService tenantService; @Resource @@ -146,7 +148,7 @@ public class AdminUserServiceImpl implements AdminUserService { checkOldPassword(id, reqVO.getOldPassword()); // 执行更新 AdminUserDO updateObj = new AdminUserDO().setId(id); - updateObj.setPassword(passwordEncoder.encode(reqVO.getNewPassword())); // 加密密码 + updateObj.setPassword(encodePassword(reqVO.getNewPassword())); // 加密密码 userMapper.updateById(updateObj); } @@ -170,7 +172,7 @@ public class AdminUserServiceImpl implements AdminUserService { // 更新密码 AdminUserDO updateObj = new AdminUserDO(); updateObj.setId(id); - updateObj.setPassword(passwordEncoder.encode(password)); // 加密密码 + updateObj.setPassword(encodePassword(password)); // 加密密码 userMapper.updateById(updateObj); } @@ -203,11 +205,6 @@ public class AdminUserServiceImpl implements AdminUserService { return userMapper.selectByUsername(username); } - /** - * 通过手机号获取用户 - * @param mobile - * @return - */ @Override public AdminUserDO getUserByMobile(String mobile) { return userMapper.selectByMobile(mobile); @@ -393,7 +390,7 @@ public class AdminUserServiceImpl implements AdminUserService { if (user == null) { throw exception(USER_NOT_EXISTS); } - if (!passwordEncoder.matches(oldPassword, user.getPassword())) { + if (!isPasswordMatch(oldPassword, user.getPassword())) { throw exception(USER_PASSWORD_FAILED); } } @@ -419,7 +416,7 @@ public class AdminUserServiceImpl implements AdminUserService { AdminUserDO existUser = userMapper.selectByUsername(importUser.getUsername()); if (existUser == null) { userMapper.insert(UserConvert.INSTANCE.convert(importUser) - .setPassword(passwordEncoder.encode(userInitPassword))); // 设置默认密码 + .setPassword(encodePassword(userInitPassword))); // 设置默认密码 respVO.getCreateUsernames().add(importUser.getUsername()); return; } @@ -441,4 +438,19 @@ public class AdminUserServiceImpl implements AdminUserService { return userMapper.selectListByStatus(status); } + @Override + public boolean isPasswordMatch(String rawPassword, String encodedPassword) { + return passwordEncoder.matches(rawPassword, encodedPassword); + } + + /** + * 对密码进行加密 + * + * @param password 密码 + * @return 加密后的密码 + */ + private String encodePassword(String password) { + return passwordEncoder.encode(password); + } + } diff --git a/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/auth/AuthServiceImplTest.java b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/auth/AuthServiceImplTest.java index 47b767eb5..7355c4d77 100644 --- a/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/auth/AuthServiceImplTest.java +++ b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/auth/AuthServiceImplTest.java @@ -1,42 +1,33 @@ package cn.iocoder.yudao.module.system.service.auth; import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; -import cn.iocoder.yudao.framework.security.core.LoginUser; +import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; import cn.iocoder.yudao.framework.test.core.util.AssertUtils; import cn.iocoder.yudao.module.system.api.sms.SmsCodeApi; import cn.iocoder.yudao.module.system.controller.admin.auth.vo.auth.AuthLoginReqVO; +import cn.iocoder.yudao.module.system.controller.admin.auth.vo.auth.AuthLoginRespVO; +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.logger.LoginLogTypeEnum; import cn.iocoder.yudao.module.system.enums.logger.LoginResultEnum; import cn.iocoder.yudao.module.system.service.common.CaptchaService; -import cn.iocoder.yudao.module.system.service.dept.PostService; import cn.iocoder.yudao.module.system.service.logger.LoginLogService; -import cn.iocoder.yudao.module.system.service.permission.PermissionService; import cn.iocoder.yudao.module.system.service.social.SocialUserService; import cn.iocoder.yudao.module.system.service.user.AdminUserService; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.context.annotation.Import; -import org.springframework.security.authentication.AuthenticationManager; -import org.springframework.security.authentication.BadCredentialsException; -import org.springframework.security.authentication.DisabledException; -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.AuthenticationException; -import org.springframework.security.core.userdetails.UsernameNotFoundException; import javax.annotation.Resource; import javax.validation.Validator; -import java.util.Set; +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.*; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString; import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.*; -import static java.util.Collections.singleton; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.*; @@ -49,23 +40,15 @@ public class AuthServiceImplTest extends BaseDbUnitTest { @MockBean private AdminUserService userService; @MockBean - private PermissionService permissionService; - @MockBean - private AuthenticationManager authenticationManager; - @MockBean - private Authentication authentication; - @MockBean private CaptchaService captchaService; @MockBean private LoginLogService loginLogService; @MockBean - private UserSessionService userSessionService; - @MockBean private SocialUserService socialService; @MockBean - private PostService postService; - @MockBean private SmsCodeApi smsCodeApi; + @MockBean + private OAuth2TokenService oauth2TokenService; @MockBean private Validator validator; @@ -76,71 +59,102 @@ public class AuthServiceImplTest extends BaseDbUnitTest { } @Test - public void testLoadUserByUsername_success() { + public void testLogin0_success() { // 准备参数 String username = randomString(); - // mock 方法 - AdminUserDO user = randomPojo(AdminUserDO.class, o -> o.setUsername(username)); + String password = randomString(); + // mock user 数据 + AdminUserDO user = randomPojo(AdminUserDO.class, o -> o.setUsername(username) + .setPassword(password).setStatus(CommonStatusEnum.ENABLE.getStatus())); + when(userService.getUserByUsername(eq(username))).thenReturn(user); + // mock password 匹配 + when(userService.isPasswordMatch(eq(password), eq(user.getPassword()))).thenReturn(true); + + // 调用 + AdminUserDO loginUser = authService.login0(username, password); + // 校验 + assertPojoEquals(user, loginUser); + } + + @Test + public void testLogin0_userNotFound() { + // 准备参数 + String username = randomString(); + String password = randomString(); + + // 调用, 并断言异常 + AssertUtils.assertServiceException(() -> authService.login0(username, password), + AUTH_LOGIN_BAD_CREDENTIALS); + verify(loginLogService).createLoginLog( + argThat(o -> o.getLogType().equals(LoginLogTypeEnum.LOGIN_USERNAME.getType()) + && o.getResult().equals(LoginResultEnum.BAD_CREDENTIALS.getResult()) + && o.getUserId() == null) + ); + } + + @Test + public void testLogin0_badCredentials() { + // 准备参数 + String username = randomString(); + String password = randomString(); + // mock user 数据 + AdminUserDO user = randomPojo(AdminUserDO.class, o -> o.setUsername(username) + .setPassword(password).setStatus(CommonStatusEnum.ENABLE.getStatus())); when(userService.getUserByUsername(eq(username))).thenReturn(user); - // 调用 - LoginUser loginUser = (LoginUser) authService.loadUserByUsername(username); - // 校验 - AssertUtils.assertPojoEquals(user, loginUser, "updateTime"); + // 调用, 并断言异常 + AssertUtils.assertServiceException(() -> authService.login0(username, password), + AUTH_LOGIN_BAD_CREDENTIALS); + verify(loginLogService).createLoginLog( + argThat(o -> o.getLogType().equals(LoginLogTypeEnum.LOGIN_USERNAME.getType()) + && o.getResult().equals(LoginResultEnum.BAD_CREDENTIALS.getResult()) + && o.getUserId().equals(user.getId())) + ); } @Test - public void testLoadUserByUsername_userNotFound() { + public void testLogin0_userDisabled() { // 准备参数 String username = randomString(); - // mock 方法 + String password = randomString(); + // mock user 数据 + AdminUserDO user = randomPojo(AdminUserDO.class, o -> o.setUsername(username) + .setPassword(password).setStatus(CommonStatusEnum.DISABLE.getStatus())); + when(userService.getUserByUsername(eq(username))).thenReturn(user); + // mock password 匹配 + when(userService.isPasswordMatch(eq(password), eq(user.getPassword()))).thenReturn(true); // 调用, 并断言异常 - assertThrows(UsernameNotFoundException.class, // 抛出 UsernameNotFoundException 异常 - () -> authService.loadUserByUsername(username), - username); // 异常提示为 username + AssertUtils.assertServiceException(() -> authService.login0(username, password), + AUTH_LOGIN_USER_DISABLED); + verify(loginLogService).createLoginLog( + argThat(o -> o.getLogType().equals(LoginLogTypeEnum.LOGIN_USERNAME.getType()) + && o.getResult().equals(LoginResultEnum.USER_DISABLED.getResult()) + && o.getUserId().equals(user.getId())) + ); } @Test - public void testMockLogin_success() { - // 准备参数 - Long userId = randomLongId(); - // mock 方法 01 - AdminUserDO user = randomPojo(AdminUserDO.class, o -> o.setId(userId)); - when(userService.getUser(eq(userId))).thenReturn(user); - // mock 方法 02 - Set roleIds = randomSet(Long.class); - when(permissionService.getUserRoleIds(eq(userId), eq(singleton(CommonStatusEnum.ENABLE.getStatus())))) - .thenReturn(roleIds); - - // 调用 - LoginUser loginUser = authService.mockLogin(userId); - // 断言 - AssertUtils.assertPojoEquals(user, loginUser, "updateTime"); - assertEquals(roleIds, loginUser.getRoleIds()); - } - - @Test - public void testMockLogin_userNotFound() { - // 准备参数 - Long userId = randomLongId(); - // mock 方法 - - // 调用, 并断言异常 - assertThrows(UsernameNotFoundException.class, // 抛出 UsernameNotFoundException 异常 - () -> authService.mockLogin(userId), - String.valueOf(userId)); // 异常提示为 userId - } - - @Test - public void testLogin_captchaNotFound() { + public void testCaptcha_success() { + // 准备参数 + AuthLoginReqVO reqVO = randomPojo(AuthLoginReqVO.class); + + // mock 验证码正确 + when(captchaService.getCaptchaCode(reqVO.getUuid())).thenReturn(reqVO.getCode()); + + // 调用 + authService.verifyCaptcha(reqVO); + // 断言 + verify(captchaService).deleteCaptchaCode(reqVO.getUuid()); + } + + @Test + public void testCaptcha_notFound() { // 准备参数 AuthLoginReqVO reqVO = randomPojo(AuthLoginReqVO.class); - String userIp = randomString(); - String userAgent = randomString(); // 调用, 并断言异常 - assertServiceException(() -> authService.login(reqVO, userIp, userAgent), AUTH_LOGIN_CAPTCHA_NOT_FOUND); + assertServiceException(() -> authService.verifyCaptcha(reqVO), AUTH_LOGIN_CAPTCHA_NOT_FOUND); // 校验调用参数 verify(loginLogService, times(1)).createLoginLog( argThat(o -> o.getLogType().equals(LoginLogTypeEnum.LOGIN_USERNAME.getType()) @@ -149,10 +163,8 @@ public class AuthServiceImplTest extends BaseDbUnitTest { } @Test - public void testLogin_captchaCodeError() { + public void testCaptcha_codeError() { // 准备参数 - String userIp = randomString(); - String userAgent = randomString(); AuthLoginReqVO reqVO = randomPojo(AuthLoginReqVO.class); // mock 验证码不正确 @@ -160,114 +172,42 @@ public class AuthServiceImplTest extends BaseDbUnitTest { when(captchaService.getCaptchaCode(reqVO.getUuid())).thenReturn(code); // 调用, 并断言异常 - assertServiceException(() -> authService.login(reqVO, userIp, userAgent), AUTH_LOGIN_CAPTCHA_CODE_ERROR); + assertServiceException(() -> authService.verifyCaptcha(reqVO), AUTH_LOGIN_CAPTCHA_CODE_ERROR); // 校验调用参数 - verify(loginLogService, times(1)).createLoginLog( + verify(loginLogService).createLoginLog( argThat(o -> o.getLogType().equals(LoginLogTypeEnum.LOGIN_USERNAME.getType()) && o.getResult().equals(LoginResultEnum.CAPTCHA_CODE_ERROR.getResult())) ); } - @Test - public void testLogin_badCredentials() { - // 准备参数 - String userIp = randomString(); - String userAgent = randomString(); - AuthLoginReqVO reqVO = randomPojo(AuthLoginReqVO.class); - // mock 验证码正确 - when(captchaService.getCaptchaCode(reqVO.getUuid())).thenReturn(reqVO.getCode()); - // mock 抛出异常 - when(authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(reqVO.getUsername(), reqVO.getPassword()))) - .thenThrow(new BadCredentialsException("测试账号或密码不正确")); - - // 调用, 并断言异常 - assertServiceException(() -> authService.login(reqVO, userIp, userAgent), AUTH_LOGIN_BAD_CREDENTIALS); - // 校验调用参数 - verify(captchaService, times(1)).deleteCaptchaCode(reqVO.getUuid()); - verify(loginLogService, times(1)).createLoginLog( - argThat(o -> o.getLogType().equals(LoginLogTypeEnum.LOGIN_USERNAME.getType()) - && o.getResult().equals(LoginResultEnum.BAD_CREDENTIALS.getResult())) - ); - } - - @Test - public void testLogin_userDisabled() { - // 准备参数 - String userIp = randomString(); - String userAgent = randomString(); - AuthLoginReqVO reqVO = randomPojo(AuthLoginReqVO.class); - - // mock 验证码正确 - when(captchaService.getCaptchaCode(reqVO.getUuid())).thenReturn(reqVO.getCode()); - // mock 抛出异常 - when(authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(reqVO.getUsername(), reqVO.getPassword()))) - .thenThrow(new DisabledException("测试用户被禁用")); - - // 调用, 并断言异常 - assertServiceException(() -> authService.login(reqVO, userIp, userAgent), AUTH_LOGIN_USER_DISABLED); - // 校验调用参数 - verify(captchaService, times(1)).deleteCaptchaCode(reqVO.getUuid()); - verify(loginLogService, times(1)).createLoginLog( - argThat(o -> o.getLogType().equals(LoginLogTypeEnum.LOGIN_USERNAME.getType()) - && o.getResult().equals(LoginResultEnum.USER_DISABLED.getResult())) - ); - } - - @Test - public void testLogin_unknownError() { - // 准备参数 - String userIp = randomString(); - String userAgent = randomString(); - AuthLoginReqVO reqVO = randomPojo(AuthLoginReqVO.class); - // mock 验证码正确 - when(captchaService.getCaptchaCode(reqVO.getUuid())).thenReturn(reqVO.getCode()); - // mock 抛出异常 - when(authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(reqVO.getUsername(), reqVO.getPassword()))) - .thenThrow(new AuthenticationException("测试未知异常") {}); - - // 调用, 并断言异常 - assertServiceException(() -> authService.login(reqVO, userIp, userAgent), AUTH_LOGIN_FAIL_UNKNOWN); - // 校验调用参数 - verify(captchaService, times(1)).deleteCaptchaCode(reqVO.getUuid()); - verify(loginLogService, times(1)).createLoginLog( - argThat(o -> o.getLogType().equals(LoginLogTypeEnum.LOGIN_USERNAME.getType()) - && o.getResult().equals(LoginResultEnum.UNKNOWN_ERROR.getResult())) - ); - } - @Test public void testLogin_success() { // 准备参数 - String userIp = randomString(); - String userAgent = randomString(); - AuthLoginReqVO reqVO = randomPojo(AuthLoginReqVO.class); + AuthLoginReqVO reqVO = randomPojo(AuthLoginReqVO.class, o -> + o.setUsername("test_username").setPassword("test_password")); // mock 验证码正确 when(captchaService.getCaptchaCode(reqVO.getUuid())).thenReturn(reqVO.getCode()); - // mock authentication - Long userId = randomLongId(); - Set userRoleIds = randomSet(Long.class); - LoginUser loginUser = randomPojo(LoginUser.class, o -> { - o.setId(userId); - o.setRoleIds(userRoleIds); - }); - when(authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(reqVO.getUsername(), reqVO.getPassword()))) - .thenReturn(authentication); - when(authentication.getPrincipal()).thenReturn(loginUser); - // mock 获得 User 拥有的角色编号数组 - when(permissionService.getUserRoleIds(userId, singleton(CommonStatusEnum.ENABLE.getStatus()))).thenReturn(userRoleIds); + // mock user 数据 + AdminUserDO user = randomPojo(AdminUserDO.class, o -> o.setId(1L).setUsername("test_username") + .setPassword("test_password").setStatus(CommonStatusEnum.ENABLE.getStatus())); + when(userService.getUserByUsername(eq("test_username"))).thenReturn(user); + // mock password 匹配 + when(userService.isPasswordMatch(eq("test_password"), eq(user.getPassword()))).thenReturn(true); // mock 缓存登录用户到 Redis - String token = randomString(); - when(userSessionService.createUserSession(loginUser, userIp, userAgent)).thenReturn(token); + OAuth2AccessTokenDO accessTokenDO = randomPojo(OAuth2AccessTokenDO.class, o -> o.setUserId(1L) + .setUserType(UserTypeEnum.ADMIN.getValue())); + when(oauth2TokenService.createAccessToken(eq(1L), eq(UserTypeEnum.ADMIN.getValue()), eq(1L))) + .thenReturn(accessTokenDO); // 调用, 并断言异常 - String login = authService.login(reqVO, userIp, userAgent); - assertEquals(token, login); + AuthLoginRespVO loginRespVO = authService.login(reqVO); + assertPojoEquals(accessTokenDO, loginRespVO); // 校验调用参数 - verify(captchaService, times(1)).deleteCaptchaCode(reqVO.getUuid()); - verify(loginLogService, times(1)).createLoginLog( + verify(loginLogService).createLoginLog( argThat(o -> o.getLogType().equals(LoginLogTypeEnum.LOGIN_USERNAME.getType()) - && o.getResult().equals(LoginResultEnum.SUCCESS.getResult())) + && o.getResult().equals(LoginResultEnum.SUCCESS.getResult()) + && o.getUserId().equals(user.getId())) ); } @@ -275,17 +215,28 @@ public class AuthServiceImplTest extends BaseDbUnitTest { public void testLogout_success() { // 准备参数 String token = randomString(); - LoginUser loginUser = randomPojo(LoginUser.class); // mock - when(userSessionService.getLoginUser(token)).thenReturn(loginUser); + OAuth2AccessTokenDO accessTokenDO = randomPojo(OAuth2AccessTokenDO.class, o -> o.setUserId(1L) + .setUserType(UserTypeEnum.ADMIN.getValue())); + when(oauth2TokenService.removeAccessToken(eq(token))).thenReturn(accessTokenDO); + // 调用 - authService.logout(token); + authService.logout(token, LoginLogTypeEnum.LOGOUT_SELF.getType()); // 校验调用参数 - verify(userSessionService, times(1)).deleteUserSession(token); - verify(loginLogService, times(1)).createLoginLog( - argThat(o -> o.getLogType().equals(LoginLogTypeEnum.LOGOUT_SELF.getType()) + verify(loginLogService).createLoginLog(argThat(o -> o.getLogType().equals(LoginLogTypeEnum.LOGOUT_SELF.getType()) && o.getResult().equals(LoginResultEnum.SUCCESS.getResult())) ); } + @Test + public void testLogout_fail() { + // 准备参数 + String token = randomString(); + + // 调用 + authService.logout(token, LoginLogTypeEnum.LOGOUT_SELF.getType()); + // 校验调用参数 + verify(loginLogService, never()).createLoginLog(any()); + } + } diff --git a/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/auth/OAuth2ClientServiceImplTest.java b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/auth/OAuth2ClientServiceImplTest.java new file mode 100755 index 000000000..816215f4d --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/auth/OAuth2ClientServiceImplTest.java @@ -0,0 +1,128 @@ +package cn.iocoder.yudao.module.system.service.auth; + +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; +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.OAuth2ClientUpdateReqVO; +import cn.iocoder.yudao.module.system.dal.dataobject.auth.OAuth2ClientDO; +import cn.iocoder.yudao.module.system.dal.mysql.auth.OAuth2ClientMapper; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.springframework.context.annotation.Import; + +import javax.annotation.Resource; + +import static cn.iocoder.yudao.framework.common.util.object.ObjectUtils.cloneIgnoreId; +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.module.system.enums.ErrorCodeConstants.OAUTH2_CLIENT_NOT_EXISTS; +import static org.junit.jupiter.api.Assertions.*; + +/** +* {@link OAuth2ClientServiceImpl} 的单元测试类 +* +* @author 芋道源码 +*/ +@Import(OAuth2ClientServiceImpl.class) +public class OAuth2ClientServiceImplTest extends BaseDbUnitTest { + + @Resource + private OAuth2ClientServiceImpl oAuth2ClientService; + + @Resource + private OAuth2ClientMapper oAuth2ClientMapper; + + @Test + public void testCreateOAuth2Client_success() { + // 准备参数 + OAuth2ClientCreateReqVO reqVO = randomPojo(OAuth2ClientCreateReqVO.class); + + // 调用 + Long oauth2ClientId = oAuth2ClientService.createOAuth2Client(reqVO); + // 断言 + assertNotNull(oauth2ClientId); + // 校验记录的属性是否正确 + OAuth2ClientDO oAuth2Client = oAuth2ClientMapper.selectById(oauth2ClientId); + assertPojoEquals(reqVO, oAuth2Client); + } + + @Test + public void testUpdateOAuth2Client_success() { + // mock 数据 + OAuth2ClientDO dbOAuth2Client = randomPojo(OAuth2ClientDO.class); + oAuth2ClientMapper.insert(dbOAuth2Client);// @Sql: 先插入出一条存在的数据 + // 准备参数 + OAuth2ClientUpdateReqVO reqVO = randomPojo(OAuth2ClientUpdateReqVO.class, o -> { + o.setId(dbOAuth2Client.getId()); // 设置更新的 ID + }); + + // 调用 + oAuth2ClientService.updateOAuth2Client(reqVO); + // 校验是否更新正确 + OAuth2ClientDO oAuth2Client = oAuth2ClientMapper.selectById(reqVO.getId()); // 获取最新的 + assertPojoEquals(reqVO, oAuth2Client); + } + + @Test + public void testUpdateOAuth2Client_notExists() { + // 准备参数 + OAuth2ClientUpdateReqVO reqVO = randomPojo(OAuth2ClientUpdateReqVO.class); + + // 调用, 并断言异常 + assertServiceException(() -> oAuth2ClientService.updateOAuth2Client(reqVO), OAUTH2_CLIENT_NOT_EXISTS); + } + + @Test + public void testDeleteOAuth2Client_success() { + // mock 数据 + OAuth2ClientDO dbOAuth2Client = randomPojo(OAuth2ClientDO.class); + oAuth2ClientMapper.insert(dbOAuth2Client);// @Sql: 先插入出一条存在的数据 + // 准备参数 + Long id = dbOAuth2Client.getId(); + + // 调用 + oAuth2ClientService.deleteOAuth2Client(id); + // 校验数据不存在了 + assertNull(oAuth2ClientMapper.selectById(id)); + } + + @Test + public void testDeleteOAuth2Client_notExists() { + // 准备参数 + Long id = randomLongId(); + + // 调用, 并断言异常 + assertServiceException(() -> oAuth2ClientService.deleteOAuth2Client(id), OAUTH2_CLIENT_NOT_EXISTS); + } + + @Test + @Disabled + public void testGetOAuth2ClientPage() { + // mock 数据 + OAuth2ClientDO dbOAuth2Client = randomPojo(OAuth2ClientDO.class, o -> { // 等会查询到 + o.setName("潜龙"); + o.setStatus(CommonStatusEnum.ENABLE.getStatus()); + }); + oAuth2ClientMapper.insert(dbOAuth2Client); + // 测试 name 不匹配 + oAuth2ClientMapper.insert(cloneIgnoreId(dbOAuth2Client, o -> o.setName("凤凰"))); + // 测试 status 不匹配 + oAuth2ClientMapper.insert(cloneIgnoreId(dbOAuth2Client, o -> o.setStatus(CommonStatusEnum.ENABLE.getStatus()))); + // 准备参数 + OAuth2ClientPageReqVO reqVO = new OAuth2ClientPageReqVO(); + reqVO.setName("long"); + reqVO.setStatus(CommonStatusEnum.ENABLE.getStatus()); + + // 调用 + PageResult pageResult = oAuth2ClientService.getOAuth2ClientPage(reqVO); + // 断言 + assertEquals(1, pageResult.getTotal()); + assertEquals(1, pageResult.getList().size()); + assertPojoEquals(dbOAuth2Client, pageResult.getList().get(0)); + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/auth/UserSessionServiceImplTest.java b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/auth/UserSessionServiceImplTest.java deleted file mode 100644 index ab739be79..000000000 --- a/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/auth/UserSessionServiceImplTest.java +++ /dev/null @@ -1,250 +0,0 @@ -package cn.iocoder.yudao.module.system.service.auth; - -import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; -import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; -import cn.iocoder.yudao.framework.common.pojo.PageResult; -import cn.iocoder.yudao.framework.common.util.date.DateUtils; -import cn.iocoder.yudao.framework.common.util.object.ObjectUtils; -import cn.iocoder.yudao.framework.security.config.SecurityProperties; -import cn.iocoder.yudao.framework.security.core.LoginUser; -import cn.iocoder.yudao.framework.test.core.ut.BaseDbAndRedisUnitTest; -import cn.iocoder.yudao.module.system.controller.admin.auth.vo.session.UserSessionPageReqVO; -import cn.iocoder.yudao.module.system.dal.dataobject.auth.UserSessionDO; -import cn.iocoder.yudao.module.system.dal.dataobject.user.AdminUserDO; -import cn.iocoder.yudao.module.system.dal.mysql.auth.UserSessionMapper; -import cn.iocoder.yudao.module.system.dal.redis.auth.LoginUserRedisDAO; -import cn.iocoder.yudao.module.system.enums.common.SexEnum; -import cn.iocoder.yudao.module.system.enums.logger.LoginLogTypeEnum; -import cn.iocoder.yudao.module.system.enums.logger.LoginResultEnum; -import cn.iocoder.yudao.module.system.service.logger.LoginLogService; -import cn.iocoder.yudao.module.system.service.user.AdminUserService; -import org.junit.jupiter.api.BeforeEach; -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.time.Duration; -import java.util.Calendar; - -import static cn.hutool.core.util.RandomUtil.randomEle; -import static cn.iocoder.yudao.framework.common.util.date.DateUtils.addTime; -import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertPojoEquals; -import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*; -import static java.util.Collections.singletonList; -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.ArgumentMatchers.argThat; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -/** - * {@link UserSessionServiceImpl} 的单元测试 - * - * @author Lyon - */ -@Import({UserSessionServiceImpl.class, LoginUserRedisDAO.class}) -public class UserSessionServiceImplTest extends BaseDbAndRedisUnitTest { - - @Resource - private UserSessionServiceImpl userSessionService; - - @Resource - private UserSessionMapper userSessionMapper; - - @MockBean - private AdminUserService userService; - @MockBean - private LoginLogService loginLogService; - @Resource - private LoginUserRedisDAO loginUserRedisDAO; - - @MockBean - private SecurityProperties securityProperties; - - @BeforeEach - public void setUp() { - when(securityProperties.getSessionTimeout()).thenReturn(Duration.ofDays(1L)); - } - - @Test - public void testGetUserSessionPage_success() { - // mock 数据 - AdminUserDO dbUser = randomPojo(AdminUserDO.class, o -> { - o.setSex(randomEle(SexEnum.values()).getSex()); - o.setStatus(CommonStatusEnum.ENABLE.getStatus()); - }); - when(userService.getUsersByUsername(eq(dbUser.getUsername()))).thenReturn(singletonList(dbUser)); - // 插入可被查询到的数据 - String userIp = randomString(); - UserSessionDO dbSession = randomPojo(UserSessionDO.class, o -> { - o.setUserId(dbUser.getId()); - o.setUserType(randomEle(UserTypeEnum.values()).getValue()); - o.setUserIp(userIp); - }); - userSessionMapper.insert(dbSession); - // 测试 username 不匹配 - userSessionMapper.insert(ObjectUtils.cloneIgnoreId(dbSession, o -> o.setUserId(123456L))); - // 测试 userIp 不匹配 - userSessionMapper.insert(ObjectUtils.cloneIgnoreId(dbSession, o -> o.setUserIp("testUserIp"))); - // 准备参数 - UserSessionPageReqVO reqVO = new UserSessionPageReqVO(); - reqVO.setUsername(dbUser.getUsername()); - reqVO.setUserIp(userIp); - - // 调用 - PageResult pageResult = userSessionService.getUserSessionPage(reqVO); - // 断言 - assertEquals(1, pageResult.getTotal()); - assertEquals(1, pageResult.getList().size()); - assertPojoEquals(dbSession, pageResult.getList().get(0)); - } - - @Test - public void testClearSessionTimeout_none() { - // mock db 数据 - UserSessionDO userSession = randomPojo(UserSessionDO.class, o -> { - o.setUserType(randomEle(UserTypeEnum.values()).getValue()); - o.setSessionTimeout(addTime(Duration.ofDays(1))); - }); - userSessionMapper.insert(userSession); - - // 调用 - long count = userSessionService.deleteTimeoutSession(); - // 断言 - assertEquals(0, count); - assertPojoEquals(userSession, userSessionMapper.selectById(userSession.getId())); // 未删除 - } - - @Test // Redis 还存在的情况 - public void testClearSessionTimeout_exists() { - // mock db 数据 - UserSessionDO userSession = randomPojo(UserSessionDO.class, o -> { - o.setUserType(randomEle(UserTypeEnum.values()).getValue()); - o.setSessionTimeout(DateUtils.addDate(Calendar.DAY_OF_YEAR, -1)); - }); - userSessionMapper.insert(userSession); - // mock redis 数据 - loginUserRedisDAO.set(userSession.getToken(), new LoginUser()); - - // 调用 - long count = userSessionService.deleteTimeoutSession(); - // 断言 - assertEquals(0, count); - assertPojoEquals(userSession, userSessionMapper.selectById(userSession.getId())); // 未删除 - } - - @Test - public void testClearSessionTimeout_success() { - // mock db 数据 - UserSessionDO userSession = randomPojo(UserSessionDO.class, o -> { - o.setUserType(randomEle(UserTypeEnum.values()).getValue()); - o.setSessionTimeout(DateUtils.addDate(Calendar.DAY_OF_YEAR, -1)); - }); - userSessionMapper.insert(userSession); - - // 清空超时数据 - long count = userSessionService.deleteTimeoutSession(); - // 校验 - assertEquals(1, count); - assertNull(userSessionMapper.selectById(userSession.getId())); // 已删除 - verify(loginLogService).createLoginLog(argThat(loginLog -> { - assertPojoEquals(userSession, loginLog); - assertEquals(LoginLogTypeEnum.LOGOUT_TIMEOUT.getType(), loginLog.getLogType()); - assertEquals(LoginResultEnum.SUCCESS.getResult(), loginLog.getResult()); - return true; - })); - } - - @Test - public void testCreateUserSession_success() { - // 准备参数 - String userIp = randomString(); - String userAgent = randomString(); - LoginUser loginUser = randomPojo(LoginUser.class, o -> { - o.setUserType(randomEle(UserTypeEnum.values()).getValue()); - o.setTenantId(0L); // 租户设置为 0,因为暂未启用多租户组件 - }); - - // 调用 - String token = userSessionService.createUserSession(loginUser, userIp, userAgent); - // 校验 UserSessionDO 记录 - UserSessionDO userSessionDO = userSessionMapper.selectOne(UserSessionDO::getToken, token); - assertPojoEquals(loginUser, userSessionDO, "id", "updateTime"); - assertEquals(token, userSessionDO.getToken()); - assertEquals(userIp, userSessionDO.getUserIp()); - assertEquals(userAgent, userSessionDO.getUserAgent()); - // 校验 LoginUser 缓存 - LoginUser redisLoginUser = loginUserRedisDAO.get(token); - assertPojoEquals(loginUser, redisLoginUser, "username", "password"); - } - - @Test - public void testCreateRefreshUserSession() { - // 准备参数 - String token = randomString(); - - // mock redis 数据 - LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setUserType(randomEle(UserTypeEnum.values()).getValue())); - loginUserRedisDAO.set(token, loginUser); - // mock db 数据 - UserSessionDO userSession = randomPojo(UserSessionDO.class, o -> { - o.setUserType(randomEle(UserTypeEnum.values()).getValue()); - o.setToken(token); - }); - userSessionMapper.insert(userSession); - - // 调用 - userSessionService.refreshUserSession(token, loginUser); - // 校验 LoginUser 缓存 - LoginUser redisLoginUser = loginUserRedisDAO.get(token); - assertPojoEquals(redisLoginUser, loginUser, "username", "password"); - // 校验 UserSessionDO 记录 - UserSessionDO updateDO = userSessionMapper.selectOne(UserSessionDO::getToken, token); - assertEquals(updateDO.getUsername(), loginUser.getUsername()); - assertNotNull(userSession.getUpdateTime()); - assertNotNull(userSession.getSessionTimeout()); - } - - @Test - public void testDeleteUserSession_Token() { - // 准备参数 - String token = randomString(); - - // mock redis 数据 - loginUserRedisDAO.set(token, new LoginUser()); - // mock db 数据 - UserSessionDO userSession = randomPojo(UserSessionDO.class, o -> { - o.setUserType(randomEle(UserTypeEnum.values()).getValue()); - o.setToken(token); - }); - userSessionMapper.insert(userSession); - - // 调用 - userSessionService.deleteUserSession(token); - // 校验数据不存在了 - assertNull(loginUserRedisDAO.get(token)); - assertNull(userSessionMapper.selectOne(UserSessionDO::getToken, token)); - } - - @Test - public void testDeleteUserSession_Id() { - // mock db 数据 - UserSessionDO userSession = randomPojo(UserSessionDO.class, o -> { - o.setUserType(randomEle(UserTypeEnum.values()).getValue()); - }); - userSessionMapper.insert(userSession); - // mock redis 数据 - loginUserRedisDAO.set(userSession.getToken(), new LoginUser()); - - // 准备参数 - Long id = userSession.getId(); - - // 调用 - userSessionService.deleteUserSession(id); - // 校验数据不存在了 - assertNull(loginUserRedisDAO.get(userSession.getToken())); - assertNull(userSessionMapper.selectById(id)); - } - -} diff --git a/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/permission/PermissionServiceTest.java b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/permission/PermissionServiceTest.java index 7ac339f54..12fddf38a 100644 --- a/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/permission/PermissionServiceTest.java +++ b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/permission/PermissionServiceTest.java @@ -1,20 +1,22 @@ package cn.iocoder.yudao.module.system.service.permission; import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.datapermission.core.dept.service.dto.DeptDataPermissionRespDTO; +import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; import cn.iocoder.yudao.module.system.dal.dataobject.dept.DeptDO; import cn.iocoder.yudao.module.system.dal.dataobject.permission.RoleDO; import cn.iocoder.yudao.module.system.dal.dataobject.permission.RoleMenuDO; import cn.iocoder.yudao.module.system.dal.dataobject.permission.UserRoleDO; +import cn.iocoder.yudao.module.system.dal.dataobject.user.AdminUserDO; import cn.iocoder.yudao.module.system.dal.mysql.permission.RoleMenuBatchInsertMapper; import cn.iocoder.yudao.module.system.dal.mysql.permission.RoleMenuMapper; import cn.iocoder.yudao.module.system.dal.mysql.permission.UserRoleBatchInsertMapper; import cn.iocoder.yudao.module.system.dal.mysql.permission.UserRoleMapper; +import cn.iocoder.yudao.module.system.enums.permission.DataScopeEnum; import cn.iocoder.yudao.module.system.mq.producer.permission.PermissionProducer; import cn.iocoder.yudao.module.system.service.dept.DeptService; -import cn.iocoder.yudao.framework.datapermission.core.dept.service.dto.DeptDataPermissionRespDTO; -import cn.iocoder.yudao.framework.security.core.LoginUser; -import cn.iocoder.yudao.module.system.enums.permission.DataScopeEnum; -import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; +import cn.iocoder.yudao.module.system.service.user.AdminUserService; import org.junit.jupiter.api.Test; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.context.annotation.Import; @@ -25,10 +27,10 @@ import java.util.List; import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertPojoEquals; import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId; import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo; +import static java.util.Collections.singleton; import static java.util.Collections.singletonList; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.ArgumentMatchers.same; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -54,6 +56,9 @@ public class PermissionServiceTest extends BaseDbUnitTest { private MenuService menuService; @MockBean private DeptService deptService; + @MockBean + private AdminUserService userService; + @MockBean private PermissionProducer permissionProducer; @@ -124,112 +129,119 @@ public class PermissionServiceTest extends BaseDbUnitTest { assertPojoEquals(dbUserRoles.get(0), userRoleDO02); } - @Test // 测试从 context 获取的场景 - public void testGetDeptDataPermission_fromContext() { - // 准备参数 - LoginUser loginUser = randomPojo(LoginUser.class); - // mock 方法 - DeptDataPermissionRespDTO respDTO = new DeptDataPermissionRespDTO(); - loginUser.setContext(PermissionServiceImpl.CONTEXT_KEY, respDTO); - - // 调用 - DeptDataPermissionRespDTO result = permissionService.getDeptDataPermission(loginUser); - // 断言 - assertSame(respDTO, result); - } - @Test public void testGetDeptDataPermission_All() { // 准备参数 - LoginUser loginUser = randomPojo(LoginUser.class); - // mock 方法 - RoleDO roleDO = randomPojo(RoleDO.class, o -> o.setDataScope(DataScopeEnum.ALL.getScope())); - when(roleService.getRolesFromCache(same(loginUser.getRoleIds()))).thenReturn(singletonList(roleDO)); + Long userId = 1L; + // mock 用户的角色编号 + userRoleMapper.insert(new UserRoleDO().setUserId(userId).setRoleId(2L)); + // mock 获得用户的角色 + RoleDO roleDO = randomPojo(RoleDO.class, o -> o.setDataScope(DataScopeEnum.ALL.getScope()) + .setStatus(CommonStatusEnum.ENABLE.getStatus())); + when(roleService.getRolesFromCache(eq(singleton(2L)))).thenReturn(singletonList(roleDO)); + when(roleService.getRoleFromCache(eq(2L))).thenReturn(roleDO); // 调用 - DeptDataPermissionRespDTO result = permissionService.getDeptDataPermission(loginUser); + DeptDataPermissionRespDTO result = permissionService.getDeptDataPermission(userId); // 断言 assertTrue(result.getAll()); assertFalse(result.getSelf()); assertTrue(CollUtil.isEmpty(result.getDeptIds())); - assertSame(result, loginUser.getContext(PermissionServiceImpl.CONTEXT_KEY, DeptDataPermissionRespDTO.class)); } @Test public void testGetDeptDataPermission_DeptCustom() { // 准备参数 - LoginUser loginUser = randomPojo(LoginUser.class); - // mock 方法 - RoleDO roleDO = randomPojo(RoleDO.class, o -> o.setDataScope(DataScopeEnum.DEPT_CUSTOM.getScope())); - when(roleService.getRolesFromCache(same(loginUser.getRoleIds()))).thenReturn(singletonList(roleDO)); + Long userId = 1L; + // mock 用户的角色编号 + userRoleMapper.insert(new UserRoleDO().setUserId(userId).setRoleId(2L)); + // mock 获得用户的角色 + RoleDO roleDO = randomPojo(RoleDO.class, o -> o.setDataScope(DataScopeEnum.DEPT_CUSTOM.getScope()) + .setStatus(CommonStatusEnum.ENABLE.getStatus())); + when(roleService.getRolesFromCache(eq(singleton(2L)))).thenReturn(singletonList(roleDO)); + when(roleService.getRoleFromCache(eq(2L))).thenReturn(roleDO); + // mock 部门的返回 + when(userService.getUser(eq(1L))).thenReturn(new AdminUserDO().setDeptId(3L), null, null); // 最后返回 null 的目的,看看会不会重复调用 // 调用 - DeptDataPermissionRespDTO result = permissionService.getDeptDataPermission(loginUser); + DeptDataPermissionRespDTO result = permissionService.getDeptDataPermission(1L); // 断言 assertFalse(result.getAll()); assertFalse(result.getSelf()); assertEquals(roleDO.getDataScopeDeptIds().size() + 1, result.getDeptIds().size()); assertTrue(CollUtil.containsAll(result.getDeptIds(), roleDO.getDataScopeDeptIds())); - assertTrue(CollUtil.contains(result.getDeptIds(), loginUser.getDeptId())); - assertSame(result, loginUser.getContext(PermissionServiceImpl.CONTEXT_KEY, DeptDataPermissionRespDTO.class)); + assertTrue(CollUtil.contains(result.getDeptIds(), 3L)); } @Test public void testGetDeptDataPermission_DeptOnly() { // 准备参数 - LoginUser loginUser = randomPojo(LoginUser.class); - // mock 方法 - RoleDO roleDO = randomPojo(RoleDO.class, o -> o.setDataScope(DataScopeEnum.DEPT_ONLY.getScope())); - when(roleService.getRolesFromCache(same(loginUser.getRoleIds()))).thenReturn(singletonList(roleDO)); + Long userId = 1L; + // mock 用户的角色编号 + userRoleMapper.insert(new UserRoleDO().setUserId(userId).setRoleId(2L)); + // mock 获得用户的角色 + RoleDO roleDO = randomPojo(RoleDO.class, o -> o.setDataScope(DataScopeEnum.DEPT_ONLY.getScope()) + .setStatus(CommonStatusEnum.ENABLE.getStatus())); + when(roleService.getRolesFromCache(eq(singleton(2L)))).thenReturn(singletonList(roleDO)); + when(roleService.getRoleFromCache(eq(2L))).thenReturn(roleDO); + // mock 部门的返回 + when(userService.getUser(eq(1L))).thenReturn(new AdminUserDO().setDeptId(3L), null, null); // 最后返回 null 的目的,看看会不会重复调用 // 调用 - DeptDataPermissionRespDTO result = permissionService.getDeptDataPermission(loginUser); + DeptDataPermissionRespDTO result = permissionService.getDeptDataPermission(1L); // 断言 assertFalse(result.getAll()); assertFalse(result.getSelf()); assertEquals(1, result.getDeptIds().size()); - assertTrue(CollUtil.contains(result.getDeptIds(), loginUser.getDeptId())); - assertSame(result, loginUser.getContext(PermissionServiceImpl.CONTEXT_KEY, DeptDataPermissionRespDTO.class)); + assertTrue(CollUtil.contains(result.getDeptIds(), 3L)); } @Test public void testGetDeptDataPermission_DeptAndChild() { // 准备参数 - LoginUser loginUser = randomPojo(LoginUser.class); - // mock 方法(角色) - RoleDO roleDO = randomPojo(RoleDO.class, o -> o.setDataScope(DataScopeEnum.DEPT_AND_CHILD.getScope())); - when(roleService.getRolesFromCache(same(loginUser.getRoleIds()))).thenReturn(singletonList(roleDO)); + Long userId = 1L; + // mock 用户的角色编号 + userRoleMapper.insert(new UserRoleDO().setUserId(userId).setRoleId(2L)); + // mock 获得用户的角色 + RoleDO roleDO = randomPojo(RoleDO.class, o -> o.setDataScope(DataScopeEnum.DEPT_AND_CHILD.getScope()) + .setStatus(CommonStatusEnum.ENABLE.getStatus())); + when(roleService.getRolesFromCache(eq(singleton(2L)))).thenReturn(singletonList(roleDO)); + when(roleService.getRoleFromCache(eq(2L))).thenReturn(roleDO); + // mock 部门的返回 + when(userService.getUser(eq(1L))).thenReturn(new AdminUserDO().setDeptId(3L), null, null); // 最后返回 null 的目的,看看会不会重复调用 // mock 方法(部门) DeptDO deptDO = randomPojo(DeptDO.class); - when(deptService.getDeptsByParentIdFromCache(eq(loginUser.getDeptId()), eq(true))) + when(deptService.getDeptsByParentIdFromCache(eq(3L), eq(true))) .thenReturn(singletonList(deptDO)); // 调用 - DeptDataPermissionRespDTO result = permissionService.getDeptDataPermission(loginUser); + DeptDataPermissionRespDTO result = permissionService.getDeptDataPermission(userId); // 断言 assertFalse(result.getAll()); assertFalse(result.getSelf()); assertEquals(2, result.getDeptIds().size()); assertTrue(CollUtil.contains(result.getDeptIds(), deptDO.getId())); - assertTrue(CollUtil.contains(result.getDeptIds(), loginUser.getDeptId())); - assertSame(result, loginUser.getContext(PermissionServiceImpl.CONTEXT_KEY, DeptDataPermissionRespDTO.class)); + assertTrue(CollUtil.contains(result.getDeptIds(), 3L)); } @Test public void testGetDeptDataPermission_Self() { // 准备参数 - LoginUser loginUser = randomPojo(LoginUser.class); - // mock 方法 - RoleDO roleDO = randomPojo(RoleDO.class, o -> o.setDataScope(DataScopeEnum.SELF.getScope())); - when(roleService.getRolesFromCache(same(loginUser.getRoleIds()))).thenReturn(singletonList(roleDO)); + Long userId = 1L; + // mock 用户的角色编号 + userRoleMapper.insert(new UserRoleDO().setUserId(userId).setRoleId(2L)); + // mock 获得用户的角色 + RoleDO roleDO = randomPojo(RoleDO.class, o -> o.setDataScope(DataScopeEnum.SELF.getScope()) + .setStatus(CommonStatusEnum.ENABLE.getStatus())); + when(roleService.getRolesFromCache(eq(singleton(2L)))).thenReturn(singletonList(roleDO)); + when(roleService.getRoleFromCache(eq(2L))).thenReturn(roleDO); // 调用 - DeptDataPermissionRespDTO result = permissionService.getDeptDataPermission(loginUser); + DeptDataPermissionRespDTO result = permissionService.getDeptDataPermission(userId); // 断言 assertFalse(result.getAll()); assertTrue(result.getSelf()); assertTrue(CollUtil.isEmpty(result.getDeptIds())); - assertSame(result, loginUser.getContext(PermissionServiceImpl.CONTEXT_KEY, DeptDataPermissionRespDTO.class)); } } diff --git a/yudao-module-system/yudao-module-system-biz/src/test/resources/sql/clean.sql b/yudao-module-system/yudao-module-system-biz/src/test/resources/sql/clean.sql index 0f9ad467a..8eec9fefc 100644 --- a/yudao-module-system/yudao-module-system-biz/src/test/resources/sql/clean.sql +++ b/yudao-module-system/yudao-module-system-biz/src/test/resources/sql/clean.sql @@ -20,3 +20,4 @@ DELETE FROM "system_social_user_bind"; DELETE FROM "system_tenant"; DELETE FROM "system_tenant_package"; DELETE FROM "system_sensitive_word"; +DELETE FROM "system_oauth2_client"; diff --git a/yudao-module-system/yudao-module-system-biz/src/test/resources/sql/create_tables.sql b/yudao-module-system/yudao-module-system-biz/src/test/resources/sql/create_tables.sql index 5cf1da35f..c3a486ea0 100644 --- a/yudao-module-system/yudao-module-system-biz/src/test/resources/sql/create_tables.sql +++ b/yudao-module-system/yudao-module-system-biz/src/test/resources/sql/create_tables.sql @@ -470,3 +470,21 @@ CREATE TABLE IF NOT EXISTS "system_sensitive_word" ( "deleted" bit NOT NULL DEFAULT FALSE, PRIMARY KEY ("id") ) COMMENT '系统敏感词'; + +CREATE TABLE IF NOT EXISTS "system_oauth2_client" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "secret" varchar NOT NULL, + "name" varchar NOT NULL, + "logo" varchar NOT NULL, + "description" varchar, + "status" int NOT NULL, + "access_token_validity_seconds" int NOT NULL, + "refresh_token_validity_seconds" int NOT NULL, + "redirect_uris" varchar NOT NULL, + "creator" varchar DEFAULT '', + "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar DEFAULT '', + "update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + PRIMARY KEY ("id") +) COMMENT 'OAuth2 客户端表'; diff --git a/yudao-server/src/main/resources/application-dev.yaml b/yudao-server/src/main/resources/application-dev.yaml index 144c8d92f..f840f8a42 100644 --- a/yudao-server/src/main/resources/application-dev.yaml +++ b/yudao-server/src/main/resources/application-dev.yaml @@ -173,8 +173,6 @@ wx: # 参见 https://github.com/Wechat-Group/WxJava/blob/develop/spring-boot-sta yudao: security: token-header: Authorization - token-timeout: 1d - session-timeout: 30m mock-enable: true mock-secret: test xss: diff --git a/yudao-server/src/main/resources/application-local.yaml b/yudao-server/src/main/resources/application-local.yaml index 21476090f..d1d854da9 100644 --- a/yudao-server/src/main/resources/application-local.yaml +++ b/yudao-server/src/main/resources/application-local.yaml @@ -193,8 +193,6 @@ yudao: enable: false # 本地环境,暂时关闭图片验证码,方便登录等接口的测试 security: token-header: Authorization - token-timeout: 1d - session-timeout: 1d mock-enable: true mock-secret: test xss: diff --git a/yudao-server/src/main/resources/application.yaml b/yudao-server/src/main/resources/application.yaml index 808bff0da..179cad761 100644 --- a/yudao-server/src/main/resources/application.yaml +++ b/yudao-server/src/main/resources/application.yaml @@ -114,6 +114,7 @@ yudao: - system_sms_template - system_sms_log - system_sensitive_word + - system_oauth2_client - infra_codegen_column - infra_codegen_table - infra_test_demo diff --git a/yudao-ui-admin/src/api/login.js b/yudao-ui-admin/src/api/login.js index 33222bc1d..eb643e3a6 100644 --- a/yudao-ui-admin/src/api/login.js +++ b/yudao-ui-admin/src/api/login.js @@ -1,4 +1,6 @@ import request from '@/utils/request' +import {getRefreshToken} from "@/utils/auth"; +import service from "@/utils/request"; // 登录方法 export function login(username, password, code, uuid) { @@ -26,7 +28,7 @@ export function getInfo() { // 退出方法 export function logout() { return request({ - url: '/system/logout', + url: '/system/auth/logout', method: 'post' }) } @@ -99,3 +101,11 @@ export function smsLogin(mobile, code) { } }) } + +// 刷新访问令牌 +export function refreshToken() { + return service({ + url: '/system/auth/refresh-token?refreshToken=' + getRefreshToken(), + method: 'post' + }) +} diff --git a/yudao-ui-admin/src/api/system/oauth2/oauth2Client.js b/yudao-ui-admin/src/api/system/oauth2/oauth2Client.js new file mode 100755 index 000000000..b392b5735 --- /dev/null +++ b/yudao-ui-admin/src/api/system/oauth2/oauth2Client.js @@ -0,0 +1,44 @@ +import request from '@/utils/request' + +// 创建 OAuth2 客户端 +export function createOAuth2Client(data) { + return request({ + url: '/system/oauth2-client/create', + method: 'post', + data: data + }) +} + +// 更新 OAuth2 客户端 +export function updateOAuth2Client(data) { + return request({ + url: '/system/oauth2-client/update', + method: 'put', + data: data + }) +} + +// 删除 OAuth2 客户端 +export function deleteOAuth2Client(id) { + return request({ + url: '/system/oauth2-client/delete?id=' + id, + method: 'delete' + }) +} + +// 获得 OAuth2 客户端 +export function getOAuth2Client(id) { + return request({ + url: '/system/oauth2-client/get?id=' + id, + method: 'get' + }) +} + +// 获得 OAuth2 客户端分页 +export function getOAuth2ClientPage(query) { + return request({ + url: '/system/oauth2-client/page', + method: 'get', + params: query + }) +} diff --git a/yudao-ui-admin/src/api/system/oauth2/oauth2Token.js b/yudao-ui-admin/src/api/system/oauth2/oauth2Token.js new file mode 100644 index 000000000..37402e8f0 --- /dev/null +++ b/yudao-ui-admin/src/api/system/oauth2/oauth2Token.js @@ -0,0 +1,18 @@ +import request from '@/utils/request' + +// 获得访问令牌分页 +export function getAccessTokenPage(query) { + return request({ + url: '/system/oauth2-token/page', + method: 'get', + params: query + }) +} + +// 删除访问令牌 +export function deleteAccessToken(accessToken) { + return request({ + url: '/system/oauth2-token/delete?accessToken=' + accessToken, + method: 'delete' + }) +} diff --git a/yudao-ui-admin/src/api/system/session.js b/yudao-ui-admin/src/api/system/session.js deleted file mode 100644 index dee070995..000000000 --- a/yudao-ui-admin/src/api/system/session.js +++ /dev/null @@ -1,18 +0,0 @@ -import request from '@/utils/request' - -// 查询在线用户列表 -export function list(query) { - return request({ - url: '/system/user-session/page', - method: 'get', - params: query - }) -} - -// 强退用户 -export function forceLogout(tokenId) { - return request({ - url: '/system/user-session/delete?id=' + tokenId, - method: 'delete' - }) -} diff --git a/yudao-ui-admin/src/components/Editor/index.vue b/yudao-ui-admin/src/components/Editor/index.vue index 6bb5a18d3..61d62434e 100644 --- a/yudao-ui-admin/src/components/Editor/index.vue +++ b/yudao-ui-admin/src/components/Editor/index.vue @@ -1,7 +1,7 @@ - - \ No newline at end of file diff --git a/yudao-ui-admin/src/permission.js b/yudao-ui-admin/src/permission.js index 57cad56f3..525b8b566 100644 --- a/yudao-ui-admin/src/permission.js +++ b/yudao-ui-admin/src/permission.js @@ -3,7 +3,7 @@ import store from './store' import { Message } from 'element-ui' import NProgress from 'nprogress' import 'nprogress/nprogress.css' -import { getToken } from '@/utils/auth' +import { getAccessToken } from '@/utils/auth' import { isRelogin } from '@/utils/request' NProgress.configure({ showSpinner: false }) @@ -13,7 +13,7 @@ const whiteList = ['/login', '/social-login', '/auth-redirect', '/bind', '/regi router.beforeEach((to, from, next) => { NProgress.start() - if (getToken()) { + if (getAccessToken()) { to.meta.title && store.dispatch('settings/setTitle', to.meta.title) /* has token*/ if (to.path === '/login') { diff --git a/yudao-ui-admin/src/store/modules/user.js b/yudao-ui-admin/src/store/modules/user.js index dac07d8f7..fbe209b75 100644 --- a/yudao-ui-admin/src/store/modules/user.js +++ b/yudao-ui-admin/src/store/modules/user.js @@ -1,9 +1,8 @@ import {login, logout, getInfo, socialQuickLogin, socialBindLogin, smsLogin} from '@/api/login' -import { getToken, setToken, removeToken } from '@/utils/auth' +import {getAccessToken, setToken, removeToken, getRefreshToken} from '@/utils/auth' const user = { state: { - token: getToken(), id: 0, // 用户编号 name: '', avatar: '', @@ -15,9 +14,6 @@ const user = { SET_ID: (state, id) => { state.id = id }, - SET_TOKEN: (state, token) => { - state.token = token - }, SET_NAME: (state, name) => { state.name = name }, @@ -42,8 +38,8 @@ const user = { return new Promise((resolve, reject) => { login(username, password, code, uuid).then(res => { res = res.data; - setToken(res.token) - commit('SET_TOKEN', res.token) + // 设置 token + setToken(res) resolve() }).catch(error => { reject(error) @@ -59,8 +55,8 @@ const user = { return new Promise((resolve, reject) => { socialQuickLogin(type, code, state).then(res => { res = res.data; - setToken(res.token) - commit('SET_TOKEN', res.token) + // 设置 token + setToken(res) resolve() }).catch(error => { reject(error) @@ -78,8 +74,8 @@ const user = { return new Promise((resolve, reject) => { socialBindLogin(type, code, state, username, password).then(res => { res = res.data; - setToken(res.token) - commit('SET_TOKEN', res.token) + // 设置 token + setToken(res) resolve() }).catch(error => { reject(error) @@ -93,8 +89,8 @@ const user = { return new Promise((resolve, reject) => { smsLogin(mobile,mobileCode).then(res => { res = res.data; - setToken(res.token) - commit('SET_TOKEN', res.token) + // 设置 token + setToken(res) resolve() }).catch(error => { reject(error) @@ -142,7 +138,6 @@ const user = { LogOut({ commit, state }) { return new Promise((resolve, reject) => { logout(state.token).then(() => { - commit('SET_TOKEN', '') commit('SET_ROLES', []) commit('SET_PERMISSIONS', []) removeToken() @@ -151,15 +146,6 @@ const user = { reject(error) }) }) - }, - - // 前端 登出 - FedLogOut({ commit }) { - return new Promise(resolve => { - commit('SET_TOKEN', '') - removeToken() - resolve() - }) } } } diff --git a/yudao-ui-admin/src/utils/auth.js b/yudao-ui-admin/src/utils/auth.js index 08a43d6e2..0c00919ed 100644 --- a/yudao-ui-admin/src/utils/auth.js +++ b/yudao-ui-admin/src/utils/auth.js @@ -1,15 +1,22 @@ import Cookies from 'js-cookie' -const TokenKey = 'Admin-Token' +const AccessTokenKey = 'ACCESS_TOKEN' +const RefreshTokenKey = 'REFRESH_TOKEN' -export function getToken() { - return Cookies.get(TokenKey) +export function getAccessToken() { + return Cookies.get(AccessTokenKey) +} + +export function getRefreshToken() { + return Cookies.get(RefreshTokenKey) } export function setToken(token) { - return Cookies.set(TokenKey, token) + Cookies.set(AccessTokenKey, token.accessToken) + Cookies.set(RefreshTokenKey, token.refreshToken) } export function removeToken() { - return Cookies.remove(TokenKey) + Cookies.remove(AccessTokenKey) + Cookies.remove(RefreshTokenKey) } diff --git a/yudao-ui-admin/src/utils/request.js b/yudao-ui-admin/src/utils/request.js index ba675ec71..d15bf27ce 100644 --- a/yudao-ui-admin/src/utils/request.js +++ b/yudao-ui-admin/src/utils/request.js @@ -1,13 +1,19 @@ import axios from 'axios' import { Notification, MessageBox, Message } from 'element-ui' import store from '@/store' -import { getToken } from '@/utils/auth' +import {getAccessToken, getRefreshToken, setToken} from '@/utils/auth' import errorCode from '@/utils/errorCode' import Cookies from "js-cookie"; import {getPath, getTenantEnable} from "@/utils/ruoyi"; +import {refreshToken} from "@/api/login"; // 是否显示重新登录 export let isRelogin = { show: false }; +// Axios 无感知刷新令牌,参考 https://www.dashingdog.cn/article/11 与 https://segmentfault.com/a/1190000020210980 实现 +// 请求队列 +let requestList = [] +// 是否正在刷新中 +let isRefreshToken = false axios.defaults.headers['Content-Type'] = 'application/json;charset=utf-8' // 创建axios实例 @@ -21,8 +27,8 @@ const service = axios.create({ service.interceptors.request.use(config => { // 是否需要设置 token const isToken = (config.headers || {}).isToken === false - if (getToken() && !isToken) { - config.headers['Authorization'] = 'Bearer ' + getToken() // 让每个请求携带自定义token 请根据实际情况自行修改 + if (getAccessToken() && !isToken) { + config.headers['Authorization'] = 'Bearer ' + getAccessToken() // 让每个请求携带自定义token 请根据实际情况自行修改 } // 设置租户 if (getTenantEnable()) { @@ -60,29 +66,44 @@ service.interceptors.request.use(config => { }) // 响应拦截器 -service.interceptors.response.use(res => { +service.interceptors.response.use( async res => { // 未设置状态码则默认成功状态 const code = res.data.code || 200; // 获取错误信息 const msg = errorCode[code] || res.data.msg || errorCode['default'] if (code === 401) { - if (!isRelogin.show) { - isRelogin.show = true; - MessageBox.confirm('登录状态已过期,您可以继续留在该页面,或者重新登录', '系统提示', { - confirmButtonText: '重新登录', - cancelButtonText: '取消', - type: 'warning' - } - ).then(() => { - isRelogin.show = false; - store.dispatch('LogOut').then(() => { - location.href = getPath('/index'); + // 如果未认证,并且未进行刷新令牌,说明可能是访问令牌过期了 + if (!isRefreshToken) { + isRefreshToken = true; + // 1. 如果获取不到刷新令牌,则只能执行登出操作 + if (!getRefreshToken()) { + return handleAuthorized(); + } + // 2. 进行刷新访问令牌 + try { + const refreshTokenRes = await refreshToken() + // 2.1 刷新成功,则回放队列的请求 + 当前请求 + setToken(refreshTokenRes.data) + requestList.forEach(cb => cb()) + return service(res.config) + } catch (e) { + // 2.2 刷新失败,则只能执行登出操作 + // 为什么需要 catch 异常呢?刷新失败时,请求因为 Promise.reject 触发异常。 + return handleAuthorized(); + } finally { + requestList = [] + isRefreshToken = false + } + } else { + // 添加到队列,等待刷新获取到新的令牌 + return new Promise(resolve => { + requestList.push(() => { + config.headers['Authorization'] = 'Bearer ' + getAccessToken() // 让每个请求携带自定义token 请根据实际情况自行修改 + resolve(service(config)) }) - }).catch(() => { - isRelogin.show = false; - }); + }) } - return Promise.reject('无效的会话,或者会话已过期,请重新登录。') + return handleAuthorized(); } else if (code === 500) { Message({ message: msg, @@ -133,9 +154,29 @@ service.interceptors.response.use(res => { export function getBaseHeader() { return { - 'Authorization': "Bearer " + getToken(), + 'Authorization': "Bearer " + getAccessToken(), 'tenant-id': Cookies.get('tenantId'), } } +function handleAuthorized() { + if (!isRelogin.show) { + isRelogin.show = true; + MessageBox.confirm('登录状态已过期,您可以继续留在该页面,或者重新登录', '系统提示', { + confirmButtonText: '重新登录', + cancelButtonText: '取消', + type: 'warning' + } + ).then(() => { + isRelogin.show = false; + store.dispatch('LogOut').then(() => { + location.href = getPath('/index'); + }) + }).catch(() => { + isRelogin.show = false; + }); + } + return Promise.reject('无效的会话,或者会话已过期,请重新登录。') +} + export default service diff --git a/yudao-ui-admin/src/views/infra/file/index.vue b/yudao-ui-admin/src/views/infra/file/index.vue index 20f5b738b..0b02a3dae 100644 --- a/yudao-ui-admin/src/views/infra/file/index.vue +++ b/yudao-ui-admin/src/views/infra/file/index.vue @@ -77,7 +77,7 @@ diff --git a/yudao-ui-admin/src/views/system/session/index.vue b/yudao-ui-admin/src/views/system/oauth2/token/index.vue similarity index 55% rename from yudao-ui-admin/src/views/system/session/index.vue rename to yudao-ui-admin/src/views/system/oauth2/token/index.vue index 10135403c..a6cb9131c 100644 --- a/yudao-ui-admin/src/views/system/session/index.vue +++ b/yudao-ui-admin/src/views/system/oauth2/token/index.vue @@ -3,11 +3,14 @@ - - + + - - + + + + 搜索 @@ -16,20 +19,28 @@ - - - - - - + + + + + + + + + + @@ -40,10 +51,10 @@