清理多租户

This commit is contained in:
2026-03-12 19:45:27 +08:00
parent f0649cb888
commit 8cef3227f3
40 changed files with 123 additions and 753 deletions

View File

@@ -8,26 +8,23 @@ import java.time.LocalDateTime;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@Schema(description = "RPC 服务 - OAuth2 访问令牌的校验 Response DTO") @Schema(description = "RPC service - OAuth2 access token check response")
@Data @Data
public class OAuth2AccessTokenCheckRespDTO implements Serializable { public class OAuth2AccessTokenCheckRespDTO implements Serializable {
@Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") @Schema(description = "User id", requiredMode = Schema.RequiredMode.REQUIRED, example = "10")
private Long userId; private Long userId;
@Schema(description = "用户类型,参见 UserTypeEnum 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") @Schema(description = "User type", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Integer userType; private Integer userType;
@Schema(description = "用户信息", example = "{\"nickname\": \"灿能\"}") @Schema(description = "User info", example = "{\"nickname\": \"rdms\"}")
private Map<String, String> userInfo; private Map<String, String> userInfo;
@Schema(description = "租户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") @Schema(description = "Scopes", example = "user_info")
private Long tenantId;
@Schema(description = "授权范围的数组", example = "user_info")
private List<String> scopes; private List<String> scopes;
@Schema(description = "过期时间", requiredMode = Schema.RequiredMode.REQUIRED) @Schema(description = "Expire time", requiredMode = Schema.RequiredMode.REQUIRED)
private LocalDateTime expiresTime; private LocalDateTime expiresTime;
} }

View File

@@ -3,17 +3,11 @@ package com.njcn.rdms.framework.common.enums;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Getter; import lombok.Getter;
/**
* 文档地址
*
* @author hongawen
*/
@Getter @Getter
@AllArgsConstructor @AllArgsConstructor
public enum DocumentEnum { public enum DocumentEnum {
REDIS_INSTALL("https://gitee.com/zhijiantianya/ruoyi-vue-pro/issues/I4VCSJ", "Redis 安装文档"), REDIS_INSTALL("https://gitee.com/zhijiantianya/ruoyi-vue-pro/issues/I4VCSJ", "Redis 安装文档");
TENANT("https://doc.iocoder.cn", "SaaS 多租户文档");
private final String url; private final String url;
private final String memo; private final String memo;

View File

@@ -71,7 +71,6 @@
- Token 透传 - Token 透传
- 用户信息透传 - 用户信息透传
- TraceId / 请求链路标识透传 - TraceId / 请求链路标识透传
- 租户编号透传
常见做法是基于 `RequestInterceptor`,在所有 Feign 请求发出前统一补充请求头。 常见做法是基于 `RequestInterceptor`,在所有 Feign 请求发出前统一补充请求头。

View File

@@ -106,10 +106,8 @@
- `id` - `id`
- `userType` - `userType`
- `info` - `info`
- `tenantId`
- `scopes` - `scopes`
- `expiresTime` - `expiresTime`
- `visitTenantId`
认证成功后,`LoginUser` 会进入 Spring Security 上下文,后续可以通过 `SecurityFrameworkUtils` 获取,例如: 认证成功后,`LoginUser` 会进入 Spring Security 上下文,后续可以通过 `SecurityFrameworkUtils` 获取,例如:
@@ -646,4 +644,3 @@ public Long createUser(UserSaveReqVO reqVO) {
- 原始 token 透传 - 原始 token 透传
- 更细的权限表达式 - 更细的权限表达式
- 更完整的审计日志字段 - 更完整的审计日志字段
- 更严格的跨租户访问控制

View File

@@ -2,65 +2,27 @@ package com.njcn.rdms.framework.security.core;
import cn.hutool.core.map.MapUtil; import cn.hutool.core.map.MapUtil;
import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnore;
import com.njcn.rdms.framework.common.enums.UserTypeEnum;
import lombok.Data; import lombok.Data;
import lombok.experimental.Accessors;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
/**
* 登录用户信息
*
* @author hongawen
*/
@Data @Data
public class LoginUser { public class LoginUser {
public static final String INFO_KEY_NICKNAME = "nickname"; public static final String INFO_KEY_NICKNAME = "nickname";
public static final String INFO_KEY_DEPT_ID = "deptId"; public static final String INFO_KEY_DEPT_ID = "deptId";
/**
* 用户编号
*/
private Long id; private Long id;
/**
* 用户类型
*
* 关联 {@link UserTypeEnum}
*/
private Integer userType; private Integer userType;
/**
* 额外的用户信息
*/
private Map<String, String> info; private Map<String, String> info;
/**
* 租户编号
*/
private Long tenantId;
/**
* 授权范围
*/
private List<String> scopes; private List<String> scopes;
/**
* 过期时间
*/
private LocalDateTime expiresTime; private LocalDateTime expiresTime;
// ========== 上下文 ==========
/**
* 上下文字段,不进行持久化
*
* 1. 用于基于 LoginUser 维度的临时缓存
*/
@JsonIgnore @JsonIgnore
private Map<String, Object> context; private Map<String, Object> context;
/**
* 访问的租户编号
*/
private Long visitTenantId;
public void setContext(String key, Object value) { public void setContext(String key, Object value) {
if (context == null) { if (context == null) {

View File

@@ -26,42 +26,29 @@ import java.io.IOException;
import java.net.URLDecoder; import java.net.URLDecoder;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
/**
* Token 过滤器,验证 token 的有效性
* 验证通过后,获得 {@link LoginUser} 信息,并加入到 Spring Security 上下文
*
* @author hongawen
*/
@RequiredArgsConstructor @RequiredArgsConstructor
@Slf4j @Slf4j
public class TokenAuthenticationFilter extends OncePerRequestFilter { public class TokenAuthenticationFilter extends OncePerRequestFilter {
private final SecurityProperties securityProperties; private final SecurityProperties securityProperties;
private final GlobalExceptionHandler globalExceptionHandler; private final GlobalExceptionHandler globalExceptionHandler;
private final OAuth2TokenCommonApi oauth2TokenApi; private final OAuth2TokenCommonApi oauth2TokenApi;
@Override @Override
@SuppressWarnings("NullableProblems") @SuppressWarnings("NullableProblems")
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException { throws ServletException, IOException {
// 情况一,基于 header[login-user] 获得用户,例如说来自 Gateway 或者其它服务透传
LoginUser loginUser = buildLoginUserByHeader(request); LoginUser loginUser = buildLoginUserByHeader(request);
// 情况二,基于 Token 获得用户
// 注意,这里主要满足直接使用 Nginx 直接转发到 Spring Cloud 服务的场景。
if (loginUser == null) { if (loginUser == null) {
String token = SecurityFrameworkUtils.obtainAuthorization(request, String token = SecurityFrameworkUtils.obtainAuthorization(request,
securityProperties.getTokenHeader(), securityProperties.getTokenParameter()); securityProperties.getTokenHeader(), securityProperties.getTokenParameter());
if (StrUtil.isNotEmpty(token)) { if (StrUtil.isNotEmpty(token)) {
Integer userType = WebFrameworkUtils.getLoginUserType(request); Integer userType = WebFrameworkUtils.getLoginUserType(request);
try { try {
// 1.1 基于 token 构建登录用户
loginUser = buildLoginUserByToken(token, userType); loginUser = buildLoginUserByToken(token, userType);
// 1.2 模拟 Login 功能,方便日常开发调试
if (loginUser == null) { if (loginUser == null) {
loginUser = mockLoginUser(request, token, userType); loginUser = mockLoginUser(token, userType);
} }
} catch (Throwable ex) { } catch (Throwable ex) {
CommonResult<?> result = globalExceptionHandler.allExceptionHandler(request, ex); CommonResult<?> result = globalExceptionHandler.allExceptionHandler(request, ex);
@@ -71,61 +58,40 @@ public class TokenAuthenticationFilter extends OncePerRequestFilter {
} }
} }
// 设置当前用户
if (loginUser != null) { if (loginUser != null) {
SecurityFrameworkUtils.setLoginUser(loginUser, request); SecurityFrameworkUtils.setLoginUser(loginUser, request);
} }
// 继续过滤链
chain.doFilter(request, response); chain.doFilter(request, response);
} }
private LoginUser buildLoginUserByToken(String token, Integer userType) { private LoginUser buildLoginUserByToken(String token, Integer userType) {
try { try {
// 校验访问令牌
OAuth2AccessTokenCheckRespDTO accessToken = oauth2TokenApi.checkAccessToken(token).getCheckedData(); OAuth2AccessTokenCheckRespDTO accessToken = oauth2TokenApi.checkAccessToken(token).getCheckedData();
if (accessToken == null) { if (accessToken == null) {
return null; return null;
} }
// 用户类型不匹配,无权限 if (userType != null && ObjectUtil.notEqual(accessToken.getUserType(), userType)) {
// 注意:只有 /admin-api/* 和 /app-api/* 有 userType才需要比对用户类型
// 类似 WebSocket 的 /ws/* 连接地址,是不需要比对用户类型的
if (userType != null
&& ObjectUtil.notEqual(accessToken.getUserType(), userType)) {
throw new AccessDeniedException("错误的用户类型"); throw new AccessDeniedException("错误的用户类型");
} }
// 构建登录用户 return new LoginUser().setId(accessToken.getUserId())
return new LoginUser().setId(accessToken.getUserId()).setUserType(accessToken.getUserType()) .setUserType(accessToken.getUserType())
.setInfo(accessToken.getUserInfo()) // 额外的用户信息 .setInfo(accessToken.getUserInfo())
.setTenantId(accessToken.getTenantId()).setScopes(accessToken.getScopes()) .setScopes(accessToken.getScopes())
.setExpiresTime(accessToken.getExpiresTime()); .setExpiresTime(accessToken.getExpiresTime());
} catch (ServiceException serviceException) { } catch (ServiceException serviceException) {
// 校验 Token 不通过时,考虑到一些接口是无需登录的,所以直接返回 null 即可
return null; return null;
} }
} }
/** private LoginUser mockLoginUser(String token, Integer userType) {
* 模拟登录用户,方便日常开发调试
*
* 注意,在线上环境下,一定要关闭该功能!!!
*
* @param request 请求
* @param token 模拟的 token格式为 {@link SecurityProperties#getMockSecret()} + 用户编号
* @param userType 用户类型
* @return 模拟的 LoginUser
*/
private LoginUser mockLoginUser(HttpServletRequest request, String token, Integer userType) {
if (!securityProperties.getMockEnable()) { if (!securityProperties.getMockEnable()) {
return null; return null;
} }
// 必须以 mockSecret 开头
if (!token.startsWith(securityProperties.getMockSecret())) { if (!token.startsWith(securityProperties.getMockSecret())) {
return null; return null;
} }
// 构建模拟用户
Long userId = Long.valueOf(token.substring(securityProperties.getMockSecret().length())); Long userId = Long.valueOf(token.substring(securityProperties.getMockSecret().length()));
return new LoginUser().setId(userId).setUserType(userType) return new LoginUser().setId(userId).setUserType(userType);
.setTenantId(WebFrameworkUtils.getTenantId(request));
} }
private LoginUser buildLoginUserByHeader(HttpServletRequest request) { private LoginUser buildLoginUserByHeader(HttpServletRequest request) {
@@ -134,20 +100,15 @@ public class TokenAuthenticationFilter extends OncePerRequestFilter {
return null; return null;
} }
try { try {
loginUserStr = URLDecoder.decode(loginUserStr, StandardCharsets.UTF_8); // 解码,解决中文乱码问题 loginUserStr = URLDecoder.decode(loginUserStr, StandardCharsets.UTF_8);
LoginUser loginUser = JsonUtils.parseObject(loginUserStr, LoginUser.class); LoginUser loginUser = JsonUtils.parseObject(loginUserStr, LoginUser.class);
// 用户类型不匹配,无权限
// 注意:只有 /admin-api/* 和 /app-api/* 有 userType才需要比对用户类型
// 类似 WebSocket 的 /ws/* 连接地址,是不需要比对用户类型的
Integer userType = WebFrameworkUtils.getLoginUserType(request); Integer userType = WebFrameworkUtils.getLoginUserType(request);
if (userType != null if (userType != null && loginUser != null && ObjectUtil.notEqual(loginUser.getUserType(), userType)) {
&& loginUser != null
&& ObjectUtil.notEqual(loginUser.getUserType(), userType)) {
throw new AccessDeniedException("错误的用户类型"); throw new AccessDeniedException("错误的用户类型");
} }
return loginUser; return loginUser;
} catch (Exception ex) { } catch (Exception ex) {
log.error("[buildLoginUserByHeader][解析 LoginUser({}) 发生异常]", loginUserStr, ex); ; log.error("[buildLoginUserByHeader][parse LoginUser({}) error]", loginUserStr, ex);
throw ex; throw ex;
} }
} }

View File

@@ -16,44 +16,28 @@ import java.util.List;
import static com.njcn.rdms.framework.common.util.cache.CacheUtils.buildCache; import static com.njcn.rdms.framework.common.util.cache.CacheUtils.buildCache;
import static com.njcn.rdms.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; import static com.njcn.rdms.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
import static com.njcn.rdms.framework.security.core.util.SecurityFrameworkUtils.skipPermissionCheck;
/**
* 默认的 {@link SecurityFrameworkService} 实现类
*
* @author hongawen
*/
@AllArgsConstructor @AllArgsConstructor
public class SecurityFrameworkServiceImpl implements SecurityFrameworkService { public class SecurityFrameworkServiceImpl implements SecurityFrameworkService {
private final PermissionCommonApi permissionApi; private final PermissionCommonApi permissionApi;
/**
* 针对 {@link #hasAnyRoles(String...)} 的缓存
*/
private final LoadingCache<KeyValue<Long, List<String>>, Boolean> hasAnyRolesCache = buildCache( private final LoadingCache<KeyValue<Long, List<String>>, Boolean> hasAnyRolesCache = buildCache(
Duration.ofMinutes(1L), // 过期时间 1 分钟 Duration.ofMinutes(1L),
new CacheLoader<KeyValue<Long, List<String>>, Boolean>() { new CacheLoader<KeyValue<Long, List<String>>, Boolean>() {
@Override @Override
public Boolean load(KeyValue<Long, List<String>> key) { public Boolean load(KeyValue<Long, List<String>> key) {
return permissionApi.hasAnyRoles(key.getKey(), key.getValue().toArray(new String[0])).getCheckedData(); return permissionApi.hasAnyRoles(key.getKey(), key.getValue().toArray(new String[0])).getCheckedData();
} }
}); });
/**
* 针对 {@link #hasAnyPermissions(String...)} 的缓存
*/
private final LoadingCache<KeyValue<Long, List<String>>, Boolean> hasAnyPermissionsCache = buildCache( private final LoadingCache<KeyValue<Long, List<String>>, Boolean> hasAnyPermissionsCache = buildCache(
Duration.ofMinutes(1L), // 过期时间 1 分钟 Duration.ofMinutes(1L),
new CacheLoader<KeyValue<Long, List<String>>, Boolean>() { new CacheLoader<KeyValue<Long, List<String>>, Boolean>() {
@Override @Override
public Boolean load(KeyValue<Long, List<String>> key) { public Boolean load(KeyValue<Long, List<String>> key) {
return permissionApi.hasAnyPermissions(key.getKey(), key.getValue().toArray(new String[0])).getCheckedData(); return permissionApi.hasAnyPermissions(key.getKey(), key.getValue().toArray(new String[0])).getCheckedData();
} }
}); });
@Override @Override
@@ -64,12 +48,6 @@ public class SecurityFrameworkServiceImpl implements SecurityFrameworkService {
@Override @Override
@SneakyThrows @SneakyThrows
public boolean hasAnyPermissions(String... permissions) { public boolean hasAnyPermissions(String... permissions) {
// 特殊:跨租户访问
if (skipPermissionCheck()) {
return true;
}
// 权限校验
Long userId = getLoginUserId(); Long userId = getLoginUserId();
if (userId == null) { if (userId == null) {
return false; return false;
@@ -85,12 +63,6 @@ public class SecurityFrameworkServiceImpl implements SecurityFrameworkService {
@Override @Override
@SneakyThrows @SneakyThrows
public boolean hasAnyRoles(String... roles) { public boolean hasAnyRoles(String... roles) {
// 特殊:跨租户访问
if (skipPermissionCheck()) {
return true;
}
// 权限校验
Long userId = getLoginUserId(); Long userId = getLoginUserId();
if (userId == null) { if (userId == null) {
return false; return false;
@@ -105,12 +77,6 @@ public class SecurityFrameworkServiceImpl implements SecurityFrameworkService {
@Override @Override
public boolean hasAnyScopes(String... scope) { public boolean hasAnyScopes(String... scope) {
// 特殊:跨租户访问
if (skipPermissionCheck()) {
return true;
}
// 权限校验
LoginUser user = SecurityFrameworkUtils.getLoginUser(); LoginUser user = SecurityFrameworkUtils.getLoginUser();
if (user == null) { if (user == null) {
return false; return false;

View File

@@ -1,7 +1,6 @@
package com.njcn.rdms.framework.security.core.util; package com.njcn.rdms.framework.security.core.util;
import cn.hutool.core.map.MapUtil; import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import com.njcn.rdms.framework.security.core.LoginUser; import com.njcn.rdms.framework.security.core.LoginUser;
import com.njcn.rdms.framework.web.core.util.WebFrameworkUtils; import com.njcn.rdms.framework.web.core.util.WebFrameworkUtils;
@@ -16,33 +15,14 @@ import org.springframework.util.StringUtils;
import java.util.Collections; import java.util.Collections;
/**
* 安全服务工具类
*
* @author hongawen
*/
public class SecurityFrameworkUtils { public class SecurityFrameworkUtils {
/**
* HEADER 认证头 value 的前缀
*/
public static final String AUTHORIZATION_BEARER = "Bearer"; public static final String AUTHORIZATION_BEARER = "Bearer";
public static final String LOGIN_USER_HEADER = "login-user"; public static final String LOGIN_USER_HEADER = "login-user";
private SecurityFrameworkUtils() {} private SecurityFrameworkUtils() {}
/** public static String obtainAuthorization(HttpServletRequest request, String headerName, String parameterName) {
* 从请求中,获得认证 Token
*
* @param request 请求
* @param headerName 认证 Token 对应的 Header 名字
* @param parameterName 认证 Token 对应的 Parameter 名字
* @return 认证 Token
*/
public static String obtainAuthorization(HttpServletRequest request,
String headerName, String parameterName) {
// 1. 获得 Token。优先级Header > Parameter
String token = request.getHeader(headerName); String token = request.getHeader(headerName);
if (StrUtil.isEmpty(token)) { if (StrUtil.isEmpty(token)) {
token = request.getParameter(parameterName); token = request.getParameter(parameterName);
@@ -50,16 +30,10 @@ public class SecurityFrameworkUtils {
if (!StringUtils.hasText(token)) { if (!StringUtils.hasText(token)) {
return null; return null;
} }
// 2. 去除 Token 中带的 Bearer
int index = token.indexOf(AUTHORIZATION_BEARER + " "); int index = token.indexOf(AUTHORIZATION_BEARER + " ");
return index >= 0 ? token.substring(index + 7).trim() : token; return index >= 0 ? token.substring(index + 7).trim() : token;
} }
/**
* 获得当前认证信息
*
* @return 认证信息
*/
public static Authentication getAuthentication() { public static Authentication getAuthentication() {
SecurityContext context = SecurityContextHolder.getContext(); SecurityContext context = SecurityContextHolder.getContext();
if (context == null) { if (context == null) {
@@ -68,11 +42,6 @@ public class SecurityFrameworkUtils {
return context.getAuthentication(); return context.getAuthentication();
} }
/**
* 获取当前用户
*
* @return 当前用户
*/
@Nullable @Nullable
public static LoginUser getLoginUser() { public static LoginUser getLoginUser() {
Authentication authentication = getAuthentication(); Authentication authentication = getAuthentication();
@@ -82,52 +51,28 @@ public class SecurityFrameworkUtils {
return authentication.getPrincipal() instanceof LoginUser ? (LoginUser) authentication.getPrincipal() : null; return authentication.getPrincipal() instanceof LoginUser ? (LoginUser) authentication.getPrincipal() : null;
} }
/**
* 获得当前用户的编号,从上下文中
*
* @return 用户编号
*/
@Nullable @Nullable
public static Long getLoginUserId() { public static Long getLoginUserId() {
LoginUser loginUser = getLoginUser(); LoginUser loginUser = getLoginUser();
return loginUser != null ? loginUser.getId() : null; return loginUser != null ? loginUser.getId() : null;
} }
/**
* 获得当前用户的昵称,从上下文中
*
* @return 昵称
*/
@Nullable @Nullable
public static String getLoginUserNickname() { public static String getLoginUserNickname() {
LoginUser loginUser = getLoginUser(); LoginUser loginUser = getLoginUser();
return loginUser != null ? MapUtil.getStr(loginUser.getInfo(), LoginUser.INFO_KEY_NICKNAME) : null; return loginUser != null ? MapUtil.getStr(loginUser.getInfo(), LoginUser.INFO_KEY_NICKNAME) : null;
} }
/**
* 获得当前用户的部门编号,从上下文中
*
* @return 部门编号
*/
@Nullable @Nullable
public static Long getLoginUserDeptId() { public static Long getLoginUserDeptId() {
LoginUser loginUser = getLoginUser(); LoginUser loginUser = getLoginUser();
return loginUser != null ? MapUtil.getLong(loginUser.getInfo(), LoginUser.INFO_KEY_DEPT_ID) : null; return loginUser != null ? MapUtil.getLong(loginUser.getInfo(), LoginUser.INFO_KEY_DEPT_ID) : null;
} }
/**
* 设置当前用户
*
* @param loginUser 登录用户
* @param request 请求
*/
public static void setLoginUser(LoginUser loginUser, HttpServletRequest request) { public static void setLoginUser(LoginUser loginUser, HttpServletRequest request) {
// 创建 Authentication并设置到上下文
Authentication authentication = buildAuthentication(loginUser, request); Authentication authentication = buildAuthentication(loginUser, request);
SecurityContextHolder.getContext().setAuthentication(authentication); SecurityContextHolder.getContext().setAuthentication(authentication);
// 额外设置到 request 中,用于 ApiAccessLogFilter 可以获取到用户编号;
// 原因是Spring Security 的 Filter 在 ApiAccessLogFilter 后面,在它记录访问日志时,线上上下文已经没有用户编号等信息
if (request != null) { if (request != null) {
WebFrameworkUtils.setLoginUserId(request, loginUser.getId()); WebFrameworkUtils.setLoginUserId(request, loginUser.getId());
WebFrameworkUtils.setLoginUserType(request, loginUser.getUserType()); WebFrameworkUtils.setLoginUserType(request, loginUser.getUserType());
@@ -135,28 +80,10 @@ public class SecurityFrameworkUtils {
} }
private static Authentication buildAuthentication(LoginUser loginUser, HttpServletRequest request) { private static Authentication buildAuthentication(LoginUser loginUser, HttpServletRequest request) {
// 创建 UsernamePasswordAuthenticationToken 对象 UsernamePasswordAuthenticationToken authenticationToken =
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken( new UsernamePasswordAuthenticationToken(loginUser, null, Collections.emptyList());
loginUser, null, Collections.emptyList());
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
return authenticationToken; return authenticationToken;
} }
/**
* 是否条件跳过权限校验,包括数据权限、功能权限
*
* @return 是否跳过
*/
public static boolean skipPermissionCheck() {
LoginUser loginUser = getLoginUser();
if (loginUser == null) {
return false;
}
if (loginUser.getVisitTenantId() == null) {
return false;
}
// 重点:跨租户访问时,无法进行权限校验
return ObjUtil.notEqual(loginUser.getVisitTenantId(), loginUser.getTenantId());
}
} }

View File

@@ -224,7 +224,7 @@ private RestTemplate loadBalancedRestTemplate;
- `/admin-api/**` - `/admin-api/**`
- `/app-api/**` - `/app-api/**`
同时会在文档中预置常见请求头,例如认证头、租户头,便于在 Swagger 页面直接调试接口。 同时会在文档中预置常见请求头,例如认证头,便于在 Swagger 页面直接调试接口。
Swagger 配置示例: Swagger 配置示例:

View File

@@ -6,7 +6,6 @@ import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Contact; import io.swagger.v3.oas.models.info.Contact;
import io.swagger.v3.oas.models.info.Info; import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.info.License; import io.swagger.v3.oas.models.info.License;
import io.swagger.v3.oas.models.media.IntegerSchema;
import io.swagger.v3.oas.models.media.StringSchema; import io.swagger.v3.oas.models.media.StringSchema;
import io.swagger.v3.oas.models.parameters.Parameter; import io.swagger.v3.oas.models.parameters.Parameter;
import io.swagger.v3.oas.models.security.SecurityRequirement; import io.swagger.v3.oas.models.security.SecurityRequirement;
@@ -34,42 +33,24 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import static com.njcn.rdms.framework.web.core.util.WebFrameworkUtils.HEADER_TENANT_ID; @AutoConfiguration(before = Knife4jAutoConfiguration.class)
/**
* Swagger 自动配置类,基于 OpenAPI + Springdoc 实现。
*
* 友情提示:
* 1. Springdoc 文档地址:<a href="https://github.com/springdoc/springdoc-openapi">仓库</a>
* 2. Swagger 规范,于 2015 更名为 OpenAPI 规范,本质是一个东西
*
* @author hongawen
*/
@AutoConfiguration(before = Knife4jAutoConfiguration.class) // before 原因,保证覆写的 Knife4jOpenApiCustomizer 先生效!相关 https://github.com/YunaiV/ruoyi-vue-pro/issues/954 讨论
@ConditionalOnClass({OpenAPI.class}) @ConditionalOnClass({OpenAPI.class})
@EnableConfigurationProperties(SwaggerProperties.class) @EnableConfigurationProperties(SwaggerProperties.class)
@ConditionalOnProperty(prefix = "springdoc.api-docs", name = "enabled", havingValue = "true", matchIfMissing = true) // 设置为 false 时,禁用 @ConditionalOnProperty(prefix = "springdoc.api-docs", name = "enabled", havingValue = "true", matchIfMissing = true)
@Import(Knife4jOpenApiCustomizer.class) @Import(Knife4jOpenApiCustomizer.class)
public class RdmsSwaggerAutoConfiguration { public class RdmsSwaggerAutoConfiguration {
// ========== 全局 OpenAPI 配置 ==========
@Bean @Bean
public OpenAPI createApi(SwaggerProperties properties) { public OpenAPI createApi(SwaggerProperties properties) {
Map<String, SecurityScheme> securitySchemas = buildSecuritySchemes(); Map<String, SecurityScheme> securitySchemas = buildSecuritySchemes();
OpenAPI openAPI = new OpenAPI() OpenAPI openAPI = new OpenAPI()
// 接口信息
.info(buildInfo(properties)) .info(buildInfo(properties))
// 接口安全配置
.components(new Components().securitySchemes(securitySchemas)) .components(new Components().securitySchemes(securitySchemas))
.addSecurityItem(new SecurityRequirement().addList(HttpHeaders.AUTHORIZATION)); .addSecurityItem(new SecurityRequirement().addList(HttpHeaders.AUTHORIZATION));
securitySchemas.keySet().forEach(key -> openAPI.addSecurityItem(new SecurityRequirement().addList(key))); securitySchemas.keySet().forEach(key -> openAPI.addSecurityItem(new SecurityRequirement().addList(key)));
return openAPI; return openAPI;
} }
/**
* API 摘要信息
*/
private Info buildInfo(SwaggerProperties properties) { private Info buildInfo(SwaggerProperties properties) {
return new Info() return new Info()
.title(properties.getTitle()) .title(properties.getTitle())
@@ -79,24 +60,18 @@ public class RdmsSwaggerAutoConfiguration {
.license(new License().name(properties.getLicense()).url(properties.getLicenseUrl())); .license(new License().name(properties.getLicense()).url(properties.getLicenseUrl()));
} }
/**
* 安全模式,这里配置通过请求头 Authorization 传递 token 参数
*/
private Map<String, SecurityScheme> buildSecuritySchemes() { private Map<String, SecurityScheme> buildSecuritySchemes() {
Map<String, SecurityScheme> securitySchemes = new HashMap<>(); Map<String, SecurityScheme> securitySchemes = new HashMap<>();
SecurityScheme securityScheme = new SecurityScheme() SecurityScheme securityScheme = new SecurityScheme()
.type(SecurityScheme.Type.APIKEY) // 类型 .type(SecurityScheme.Type.APIKEY)
.name(HttpHeaders.AUTHORIZATION) // 请求头的 name .name(HttpHeaders.AUTHORIZATION)
.in(SecurityScheme.In.HEADER); // token 所在位置 .in(SecurityScheme.In.HEADER);
securitySchemes.put(HttpHeaders.AUTHORIZATION, securityScheme); securitySchemes.put(HttpHeaders.AUTHORIZATION, securityScheme);
return securitySchemes; return securitySchemes;
} }
/**
* 自定义 OpenAPI 处理器
*/
@Bean @Bean
@Primary // 目的:以我们创建的 OpenAPIService Bean 为主,避免一键改包后,启动报错! @Primary
public OpenAPIService openApiBuilder(Optional<OpenAPI> openAPI, public OpenAPIService openApiBuilder(Optional<OpenAPI> openAPI,
SecurityService securityParser, SecurityService securityParser,
SpringDocConfigProperties springDocConfigProperties, SpringDocConfigProperties springDocConfigProperties,
@@ -108,11 +83,6 @@ public class RdmsSwaggerAutoConfiguration {
propertyResolverUtils, openApiBuilderCustomizers, serverBaseUrlCustomizers, javadocProvider); propertyResolverUtils, openApiBuilderCustomizers, serverBaseUrlCustomizers, javadocProvider);
} }
// ========== 分组 OpenAPI 配置 ==========
/**
* 所有模块的 API 分组
*/
@Bean @Bean
public GroupedOpenApi allGroupedOpenApi() { public GroupedOpenApi allGroupedOpenApi() {
return buildGroupedOpenApi("all", ""); return buildGroupedOpenApi("all", "");
@@ -127,60 +97,29 @@ public class RdmsSwaggerAutoConfiguration {
.group(group) .group(group)
.pathsToMatch("/admin-api/" + path + "/**", "/app-api/" + path + "/**") .pathsToMatch("/admin-api/" + path + "/**", "/app-api/" + path + "/**")
.addOperationCustomizer((operation, handlerMethod) -> operation .addOperationCustomizer((operation, handlerMethod) -> operation
.addParametersItem(buildTenantHeaderParameter())
.addParametersItem(buildSecurityHeaderParameter())) .addParametersItem(buildSecurityHeaderParameter()))
.addOperationCustomizer(buildOperationIdCustomizer()) .addOperationCustomizer(buildOperationIdCustomizer())
.build(); .build();
} }
/**
* 构建 Tenant 租户编号请求头参数
*
* @return 多租户参数
*/
private static Parameter buildTenantHeaderParameter() {
return new Parameter()
.name(HEADER_TENANT_ID) // header 名
.description("租户编号") // 描述
.in(String.valueOf(SecurityScheme.In.HEADER)) // 请求 header
.schema(new IntegerSchema()._default(1L).name(HEADER_TENANT_ID).description("租户编号")); // 默认:使用租户编号为 1
}
/**
* 构建 Authorization 认证请求头参数
*
* 解决 Knife4j <a href="https://gitee.com/xiaoym/knife4j/issues/I69QBU">Authorize 未生效请求header里未包含参数</a>
*
* @return 认证参数
*/
private static Parameter buildSecurityHeaderParameter() { private static Parameter buildSecurityHeaderParameter() {
return new Parameter() return new Parameter()
.name(HttpHeaders.AUTHORIZATION) // header 名 .name(HttpHeaders.AUTHORIZATION)
.description("认证 Token") // 描述 .description("认证 Token")
.in(String.valueOf(SecurityScheme.In.HEADER)) // 请求 header .in(String.valueOf(SecurityScheme.In.HEADER))
.schema(new StringSchema()._default("Bearer test1").name(HEADER_TENANT_ID).description("认证 Token")); // 默认:使用用户编号为 1 .schema(new StringSchema()._default("Bearer test1")
.name(HttpHeaders.AUTHORIZATION)
.description("认证 Token"));
} }
/**
* 核心自定义OperationId生成规则组合「类名前缀 + 方法名」
*
* @see <a href="https://github.com/YunaiV/ruoyi-vue-pro/issues/957">app-api 前缀不生效,都是使用 admin-api</a>
*/
private static OperationCustomizer buildOperationIdCustomizer() { private static OperationCustomizer buildOperationIdCustomizer() {
return (operation, handlerMethod) -> { return (operation, handlerMethod) -> {
// 1. 获取控制器类名(如 UserController
String className = handlerMethod.getBeanType().getSimpleName(); String className = handlerMethod.getBeanType().getSimpleName();
// 2. 提取类名前缀(去除 Controller 后缀,如 UserController -> User
String classPrefix = className.replaceAll("Controller$", ""); String classPrefix = className.replaceAll("Controller$", "");
// 3. 获取方法名(如 list
String methodName = handlerMethod.getMethod().getName(); String methodName = handlerMethod.getMethod().getName();
// 4. 组合生成 operationId如 User_list operation.setOperationId(classPrefix + "_" + methodName);
String operationId = classPrefix + "_" + methodName;
// 5. 设置自定义 operationId
operation.setOperationId(operationId);
return operation; return operation;
}; };
} }
} }

View File

@@ -12,26 +12,12 @@ import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes; import org.springframework.web.context.request.ServletRequestAttributes;
/**
* 专属于 web 包的工具类
*
* @author hongawen
*/
public class WebFrameworkUtils { public class WebFrameworkUtils {
private static final String REQUEST_ATTRIBUTE_LOGIN_USER_ID = "login_user_id"; private static final String REQUEST_ATTRIBUTE_LOGIN_USER_ID = "login_user_id";
private static final String REQUEST_ATTRIBUTE_LOGIN_USER_TYPE = "login_user_type"; private static final String REQUEST_ATTRIBUTE_LOGIN_USER_TYPE = "login_user_type";
private static final String REQUEST_ATTRIBUTE_COMMON_RESULT = "common_result"; private static final String REQUEST_ATTRIBUTE_COMMON_RESULT = "common_result";
public static final String HEADER_TENANT_ID = "tenant-id";
public static final String HEADER_VISIT_TENANT_ID = "visit-tenant-id";
/**
* 终端的 Header
*
* @see TerminalEnum
*/
public static final String HEADER_TERMINAL = "terminal"; public static final String HEADER_TERMINAL = "terminal";
private static WebProperties properties; private static WebProperties properties;
@@ -40,51 +26,14 @@ public class WebFrameworkUtils {
WebFrameworkUtils.properties = webProperties; WebFrameworkUtils.properties = webProperties;
} }
/**
* 获得租户编号,从 header 中
* 考虑到其它 framework 组件也会使用到租户编号,所以不得不放在 WebFrameworkUtils 统一提供
*
* @param request 请求
* @return 租户编号
*/
public static Long getTenantId(HttpServletRequest request) {
String tenantId = request.getHeader(HEADER_TENANT_ID);
return NumberUtil.isNumber(tenantId) ? Long.valueOf(tenantId) : null;
}
/**
* 获得访问的租户编号,从 header 中
* 考虑到其它 framework 组件也会使用到租户编号,所以不得不放在 WebFrameworkUtils 统一提供
*
* @param request 请求
* @return 租户编号
*/
public static Long getVisitTenantId(HttpServletRequest request) {
String tenantId = request.getHeader(HEADER_VISIT_TENANT_ID);
return NumberUtil.isNumber(tenantId)? Long.valueOf(tenantId) : null;
}
public static void setLoginUserId(ServletRequest request, Long userId) { public static void setLoginUserId(ServletRequest request, Long userId) {
request.setAttribute(REQUEST_ATTRIBUTE_LOGIN_USER_ID, userId); request.setAttribute(REQUEST_ATTRIBUTE_LOGIN_USER_ID, userId);
} }
/**
* 设置用户类型
*
* @param request 请求
* @param userType 用户类型
*/
public static void setLoginUserType(ServletRequest request, Integer userType) { public static void setLoginUserType(ServletRequest request, Integer userType) {
request.setAttribute(REQUEST_ATTRIBUTE_LOGIN_USER_TYPE, userType); request.setAttribute(REQUEST_ATTRIBUTE_LOGIN_USER_TYPE, userType);
} }
/**
* 获得当前用户的编号,从请求中
* 注意:该方法仅限于 framework 框架使用!!!
*
* @param request 请求
* @return 用户编号
*/
public static Long getLoginUserId(HttpServletRequest request) { public static Long getLoginUserId(HttpServletRequest request) {
if (request == null) { if (request == null) {
return null; return null;
@@ -92,23 +41,14 @@ public class WebFrameworkUtils {
return (Long) request.getAttribute(REQUEST_ATTRIBUTE_LOGIN_USER_ID); return (Long) request.getAttribute(REQUEST_ATTRIBUTE_LOGIN_USER_ID);
} }
/**
* 获得当前用户的类型
* 注意:该方法仅限于 web 相关的 framework 组件使用!!!
*
* @param request 请求
* @return 用户编号
*/
public static Integer getLoginUserType(HttpServletRequest request) { public static Integer getLoginUserType(HttpServletRequest request) {
if (request == null) { if (request == null) {
return null; return null;
} }
// 1. 优先,从 Attribute 中获取
Integer userType = (Integer) request.getAttribute(REQUEST_ATTRIBUTE_LOGIN_USER_TYPE); Integer userType = (Integer) request.getAttribute(REQUEST_ATTRIBUTE_LOGIN_USER_TYPE);
if (userType != null) { if (userType != null) {
return userType; return userType;
} }
// 2. 其次,基于 URL 前缀的约定
if (request.getServletPath().startsWith(properties.getAdminApi().getPrefix())) { if (request.getServletPath().startsWith(properties.getAdminApi().getPrefix())) {
return UserTypeEnum.ADMIN.getValue(); return UserTypeEnum.ADMIN.getValue();
} }
@@ -119,13 +59,11 @@ public class WebFrameworkUtils {
} }
public static Integer getLoginUserType() { public static Integer getLoginUserType() {
HttpServletRequest request = getRequest(); return getLoginUserType(getRequest());
return getLoginUserType(request);
} }
public static Long getLoginUserId() { public static Long getLoginUserId() {
HttpServletRequest request = getRequest(); return getLoginUserId(getRequest());
return getLoginUserId(request);
} }
public static Integer getTerminal() { public static Integer getTerminal() {
@@ -155,24 +93,10 @@ public class WebFrameworkUtils {
return servletRequestAttributes.getRequest(); return servletRequestAttributes.getRequest();
} }
/**
* 判断是否为 RPC 请求
*
* @param request 请求
* @return 是否为 RPC 请求
*/
public static boolean isRpcRequest(HttpServletRequest request) { public static boolean isRpcRequest(HttpServletRequest request) {
return request.getRequestURI().startsWith(RpcConstants.RPC_API_PREFIX); return request.getRequestURI().startsWith(RpcConstants.RPC_API_PREFIX);
} }
/**
* 判断是否为 RPC 请求
*
* 约定大于配置,只要以 Api 结尾,都认为是 RPC 接口
*
* @param className 类名
* @return 是否为 RPC 请求
*/
public static boolean isRpcRequest(String className) { public static boolean isRpcRequest(String className) {
return className.endsWith("Api"); return className.endsWith("Api");
} }

View File

@@ -75,7 +75,7 @@
- 需要前端通过 `?token={token}` 形式携带令牌 - 需要前端通过 `?token={token}` 形式携带令牌
- `LoginUserHandshakeInterceptor` 会将 `LoginUser` 写入 Session - `LoginUserHandshakeInterceptor` 会将 `LoginUser` 写入 Session
- `WebSocketFrameworkUtils` 可获取 `userId/userType/tenantId` - `WebSocketFrameworkUtils` 可获取 `userId/userType`
## 4. 开发人员上手 ## 4. 开发人员上手

View File

@@ -5,63 +5,26 @@ import org.springframework.web.socket.WebSocketSession;
import java.util.Map; import java.util.Map;
/**
* 专属于 web 包的工具类
*
* @author hongawen
*/
public class WebSocketFrameworkUtils { public class WebSocketFrameworkUtils {
public static final String ATTRIBUTE_LOGIN_USER = "LOGIN_USER"; public static final String ATTRIBUTE_LOGIN_USER = "LOGIN_USER";
/**
* 设置当前用户
*
* @param loginUser 登录用户
* @param attributes Session
*/
public static void setLoginUser(LoginUser loginUser, Map<String, Object> attributes) { public static void setLoginUser(LoginUser loginUser, Map<String, Object> attributes) {
attributes.put(ATTRIBUTE_LOGIN_USER, loginUser); attributes.put(ATTRIBUTE_LOGIN_USER, loginUser);
} }
/**
* 获取当前用户
*
* @return 当前用户
*/
public static LoginUser getLoginUser(WebSocketSession session) { public static LoginUser getLoginUser(WebSocketSession session) {
return (LoginUser) session.getAttributes().get(ATTRIBUTE_LOGIN_USER); return (LoginUser) session.getAttributes().get(ATTRIBUTE_LOGIN_USER);
} }
/**
* 获得当前用户的编号
*
* @return 用户编号
*/
public static Long getLoginUserId(WebSocketSession session) { public static Long getLoginUserId(WebSocketSession session) {
LoginUser loginUser = getLoginUser(session); LoginUser loginUser = getLoginUser(session);
return loginUser != null ? loginUser.getId() : null; return loginUser != null ? loginUser.getId() : null;
} }
/**
* 获得当前用户的类型
*
* @return 用户编号
*/
public static Integer getLoginUserType(WebSocketSession session) { public static Integer getLoginUserType(WebSocketSession session) {
LoginUser loginUser = getLoginUser(session); LoginUser loginUser = getLoginUser(session);
return loginUser != null ? loginUser.getUserType() : null; return loginUser != null ? loginUser.getUserType() : null;
} }
/**
* 获得当前用户的租户编号
*
* @param session Session
* @return 租户编号
*/
public static Long getTenantId(WebSocketSession session) {
LoginUser loginUser = getLoginUser(session);
return loginUser != null ? loginUser.getTenantId() : null;
}
} }

View File

@@ -96,7 +96,6 @@ spring:
``` ```
Authorization: Bearer <token> Authorization: Bearer <token>
tenant-id: <tenantId>
``` ```
网关校验成功后,会透传 `login-user` 给后端服务。 网关校验成功后,会透传 `login-user` 给后端服务。

View File

@@ -79,7 +79,7 @@ public class GrayLoadBalancer implements ReactorServiceInstanceLoadBalancer {
// 基于 tag 过滤实例列表 // 基于 tag 过滤实例列表
chooseInstances = filterTagServiceInstances(chooseInstances, headers); chooseInstances = filterTagServiceInstances(chooseInstances, headers);
// 随机 + 权重获取实例列表 TODO 芋艿:目前直接使用 Nacos 提供的方法,如果替换注册中心,需要重新失败该方法 // 随机 + 权重获取实例列表 TODO 目前直接使用 Nacos 提供的方法,如果替换注册中心,需要重新失败该方法
return new DefaultResponse(NacosBalancer.getHostByRandomWeight3(chooseInstances)); return new DefaultResponse(NacosBalancer.getHostByRandomWeight3(chooseInstances));
} }

View File

@@ -50,7 +50,7 @@ import static cn.hutool.core.date.DatePattern.NORM_DATETIME_MS_FORMATTER;
* *
* 从功能上,它类似 rdms-spring-boot-starter-web 的 ApiAccessLogFilter 过滤器 * 从功能上,它类似 rdms-spring-boot-starter-web 的 ApiAccessLogFilter 过滤器
* *
* TODO 芋艿:如果网关执行异常,不会记录访问日志,后续研究下 https://github.com/Silvmike/webflux-demo/blob/master/tests/src/test/java/ru/hardcoders/demo/webflux/web_handler/filters/logging * TODO 如果网关执行异常,不会记录访问日志,后续研究下 https://github.com/Silvmike/webflux-demo/blob/master/tests/src/test/java/ru/hardcoders/demo/webflux/web_handler/filters/logging
* *
* @author hongawen * @author hongawen
*/ */
@@ -71,34 +71,34 @@ public class AccessLogFilter implements GlobalFilter, Ordered {
// log.info("[writeAccessLog][日志内容:{}]", JsonUtils.toJsonString(gatewayLog)); // log.info("[writeAccessLog][日志内容:{}]", JsonUtils.toJsonString(gatewayLog));
// 方式二:调用远程服务,记录到数据库中 // 方式二:调用远程服务,记录到数据库中
// TODO 芋艿:暂未实现 // TODO 暂未实现
// 方式三:打印到控制台,方便排查错误 // 方式三:打印到控制台,方便排查错误
try { // try {
Map<String, Object> values = MapUtil.newHashMap(15, true); // 手工拼接保证排序15 保证不用扩容 // Map<String, Object> values = MapUtil.newHashMap(15, true); // 手工拼接保证排序15 保证不用扩容
values.put("userId", gatewayLog.getUserId()); // values.put("userId", gatewayLog.getUserId());
values.put("userType", gatewayLog.getUserType()); // values.put("userType", gatewayLog.getUserType());
values.put("routeId", gatewayLog.getRoute() != null ? gatewayLog.getRoute().getId() : null); // values.put("routeId", gatewayLog.getRoute() != null ? gatewayLog.getRoute().getId() : null);
values.put("schema", gatewayLog.getSchema()); // values.put("schema", gatewayLog.getSchema());
values.put("requestUrl", gatewayLog.getRequestUrl()); // values.put("requestUrl", gatewayLog.getRequestUrl());
values.put("queryParams", gatewayLog.getQueryParams().toSingleValueMap()); // values.put("queryParams", gatewayLog.getQueryParams().toSingleValueMap());
values.put("requestBody", JsonUtils.isJson(gatewayLog.getRequestBody()) ? // 保证 body 的展示好看 // values.put("requestBody", JsonUtils.isJson(gatewayLog.getRequestBody()) ? // 保证 body 的展示好看
JSONUtil.parse(gatewayLog.getRequestBody()) : gatewayLog.getRequestBody()); // JSONUtil.parse(gatewayLog.getRequestBody()) : gatewayLog.getRequestBody());
values.put("requestHeaders", JsonUtils.toJsonString(gatewayLog.getRequestHeaders().toSingleValueMap())); // values.put("requestHeaders", JsonUtils.toJsonString(gatewayLog.getRequestHeaders().toSingleValueMap()));
values.put("userIp", gatewayLog.getUserIp()); // values.put("userIp", gatewayLog.getUserIp());
values.put("responseBody", JsonUtils.isJson(gatewayLog.getResponseBody()) ? // 保证 body 的展示好看 // values.put("responseBody", JsonUtils.isJson(gatewayLog.getResponseBody()) ? // 保证 body 的展示好看
JSONUtil.parse(gatewayLog.getResponseBody()) : gatewayLog.getResponseBody()); // JSONUtil.parse(gatewayLog.getResponseBody()) : gatewayLog.getResponseBody());
values.put("responseHeaders", gatewayLog.getResponseHeaders() != null ? // values.put("responseHeaders", gatewayLog.getResponseHeaders() != null ?
JsonUtils.toJsonString(gatewayLog.getResponseHeaders().toSingleValueMap()) : null); // JsonUtils.toJsonString(gatewayLog.getResponseHeaders().toSingleValueMap()) : null);
values.put("httpStatus", gatewayLog.getHttpStatus()); // values.put("httpStatus", gatewayLog.getHttpStatus());
values.put("startTime", LocalDateTimeUtil.format(gatewayLog.getStartTime(), NORM_DATETIME_MS_FORMATTER)); // values.put("startTime", LocalDateTimeUtil.format(gatewayLog.getStartTime(), NORM_DATETIME_MS_FORMATTER));
values.put("endTime", LocalDateTimeUtil.format(gatewayLog.getEndTime(), NORM_DATETIME_MS_FORMATTER)); // values.put("endTime", LocalDateTimeUtil.format(gatewayLog.getEndTime(), NORM_DATETIME_MS_FORMATTER));
values.put("duration", gatewayLog.getDuration() != null ? gatewayLog.getDuration() + " ms" : null); // values.put("duration", gatewayLog.getDuration() != null ? gatewayLog.getDuration() + " ms" : null);
log.info("[writeAccessLog][网关日志:{}]", JsonUtils.toJsonPrettyString(values)); // log.info("[writeAccessLog][网关日志:{}]", JsonUtils.toJsonPrettyString(values));
} catch (Exception e) { // } catch (Exception e) {
// 兜底处理,参见 https://gitee.com/zhijiantianya/rdms-cloud/issues/IC9A70 // // 兜底处理,参见 https://gitee.com/zhijiantianya/rdms-cloud/issues/IC9A70
log.error("[writeAccessLog][打印网关日志时,发生异常]", e); // log.error("[writeAccessLog][打印网关日志时,发生异常]", e);
} // }
} }
@Override @Override

View File

@@ -6,39 +6,13 @@ import java.time.LocalDateTime;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
/**
* 登录用户信息
*
* copy from rdms-spring-boot-starter-security 的 LoginUser 类
*
* @author hongawen
*/
@Data @Data
public class LoginUser { public class LoginUser {
/**
* 用户编号
*/
private Long id; private Long id;
/**
* 用户类型
*/
private Integer userType; private Integer userType;
/**
* 额外的用户信息
*/
private Map<String, String> info; private Map<String, String> info;
/**
* 租户编号
*/
private Long tenantId;
/**
* 授权范围
*/
private List<String> scopes; private List<String> scopes;
/**
* 过期时间
*/
private LocalDateTime expiresTime; private LocalDateTime expiresTime;
} }

View File

@@ -6,12 +6,10 @@ import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache; import com.google.common.cache.LoadingCache;
import com.njcn.rdms.framework.common.biz.system.oauth2.OAuth2TokenCommonApi; import com.njcn.rdms.framework.common.biz.system.oauth2.OAuth2TokenCommonApi;
import com.njcn.rdms.framework.common.biz.system.oauth2.dto.OAuth2AccessTokenCheckRespDTO; import com.njcn.rdms.framework.common.biz.system.oauth2.dto.OAuth2AccessTokenCheckRespDTO;
import com.njcn.rdms.framework.common.core.KeyValue;
import com.njcn.rdms.framework.common.pojo.CommonResult; import com.njcn.rdms.framework.common.pojo.CommonResult;
import com.njcn.rdms.framework.common.util.date.LocalDateTimeUtils; import com.njcn.rdms.framework.common.util.date.LocalDateTimeUtils;
import com.njcn.rdms.framework.common.util.json.JsonUtils; import com.njcn.rdms.framework.common.util.json.JsonUtils;
import com.njcn.rdms.gateway.util.SecurityFrameworkUtils; import com.njcn.rdms.gateway.util.SecurityFrameworkUtils;
import com.njcn.rdms.gateway.util.WebFrameworkUtils;
import org.springframework.cloud.client.loadbalancer.reactive.ReactorLoadBalancerExchangeFilterFunction; import org.springframework.cloud.client.loadbalancer.reactive.ReactorLoadBalancerExchangeFilterFunction;
import org.springframework.cloud.gateway.filter.GatewayFilterChain; import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter; import org.springframework.cloud.gateway.filter.GlobalFilter;
@@ -28,141 +26,102 @@ import java.util.function.Function;
import static com.njcn.rdms.framework.common.util.cache.CacheUtils.buildAsyncReloadingCache; import static com.njcn.rdms.framework.common.util.cache.CacheUtils.buildAsyncReloadingCache;
/**
* Token 过滤器,验证 token 的有效性
* 1. 验证通过时,将 userId、userType、tenantId 通过 Header 转发给服务
* 2. 验证不通过,还是会转发给服务。因为,接口是否需要登录的校验,还是交给服务自身处理
*
* @author hongawen
*/
@Component @Component
public class TokenAuthenticationFilter implements GlobalFilter, Ordered { public class TokenAuthenticationFilter implements GlobalFilter, Ordered {
/** private static final TypeReference<CommonResult<OAuth2AccessTokenCheckRespDTO>> CHECK_RESULT_TYPE_REFERENCE =
* CommonResult<OAuth2AccessTokenCheckRespDTO> 对应的 TypeReference 结果,用于解析 checkToken 的结果 new TypeReference<CommonResult<OAuth2AccessTokenCheckRespDTO>>() {};
*/
private static final TypeReference<CommonResult<OAuth2AccessTokenCheckRespDTO>> CHECK_RESULT_TYPE_REFERENCE
= new TypeReference<CommonResult<OAuth2AccessTokenCheckRespDTO>>() {};
/**
* 空的 LoginUser 的结果
*
* 用于解决如下问题:
* 1. {@link #getLoginUser(ServerWebExchange, String)} 返回 Mono.empty() 时,会导致后续的 flatMap 无法进行处理的问题。
* 2. {@link #buildUser(String)} 时,如果 Token 已经过期,返回 LOGIN_USER_EMPTY 对象,避免缓存无法刷新
*/
private static final LoginUser LOGIN_USER_EMPTY = new LoginUser(); private static final LoginUser LOGIN_USER_EMPTY = new LoginUser();
private final WebClient webClient; private final WebClient webClient;
/** private final LoadingCache<String, LoginUser> loginUserCache = buildAsyncReloadingCache(Duration.ofMinutes(1),
* 登录用户的本地缓存 new CacheLoader<String, LoginUser>() {
*
* key1多租户的编号
* key2访问令牌
*/
private final LoadingCache<KeyValue<Long, String>, LoginUser> loginUserCache = buildAsyncReloadingCache(Duration.ofMinutes(1),
new CacheLoader<KeyValue<Long, String>, LoginUser>() {
@Override @Override
public LoginUser load(KeyValue<Long, String> token) { public LoginUser load(String token) {
String body = checkAccessToken(token.getKey(), token.getValue()).block(); String body = checkAccessToken(token).block();
return buildUser(body); return buildUser(body);
} }
}); });
public TokenAuthenticationFilter(ReactorLoadBalancerExchangeFilterFunction lbFunction) { public TokenAuthenticationFilter(ReactorLoadBalancerExchangeFilterFunction lbFunction) {
// Q为什么不使用 OAuth2TokenApi 进行调用?
// A1Spring Cloud OpenFeign 官方未内置 Reactive 的支持 https://docs.spring.io/spring-cloud-openfeign/docs/current/reference/html/#reactive-support
// A2校验 Token 的 API 需要使用到 header[tenant-id] 传递租户编号,暂时不想编写 RequestInterceptor 实现
// 因此,这里采用 WebClient通过 lbFunction 实现负载均衡
this.webClient = WebClient.builder().filter(lbFunction).build(); this.webClient = WebClient.builder().filter(lbFunction).build();
} }
@Override @Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 移除 login-user 的请求头,避免伪造模拟
exchange = SecurityFrameworkUtils.removeLoginUser(exchange); exchange = SecurityFrameworkUtils.removeLoginUser(exchange);
// 情况一,如果没有 Token 令牌,则直接继续 filter
String token = SecurityFrameworkUtils.obtainAuthorization(exchange); String token = SecurityFrameworkUtils.obtainAuthorization(exchange);
if (StrUtil.isEmpty(token)) { if (StrUtil.isEmpty(token)) {
return chain.filter(exchange); return chain.filter(exchange);
} }
// 情况二,如果有 Token 令牌,则解析对应 userId、userType、tenantId 等字段,并通过 通过 Header 转发给服务
// 重要说明defaultIfEmpty 作用,保证 Mono.empty() 情况,可以继续执行 `flatMap 的 chain.filter(exchange)` 逻辑,避免返回给前端空的 Response
ServerWebExchange finalExchange = exchange; ServerWebExchange finalExchange = exchange;
return getLoginUser(exchange, token).defaultIfEmpty(LOGIN_USER_EMPTY).flatMap(user -> { return getLoginUser(token).defaultIfEmpty(LOGIN_USER_EMPTY).flatMap(user -> {
// 1. 无用户,直接 filter 继续请求 if (user == LOGIN_USER_EMPTY
if (user == LOGIN_USER_EMPTY || // 下面 expiresTime 的判断,为了解决 token 实际已经过期的情况 || user.getExpiresTime() == null
user.getExpiresTime() == null || LocalDateTimeUtils.beforeNow(user.getExpiresTime())) { || LocalDateTimeUtils.beforeNow(user.getExpiresTime())) {
return chain.filter(finalExchange); return chain.filter(finalExchange);
} }
// 2.1 有用户,则设置登录用户
SecurityFrameworkUtils.setLoginUser(finalExchange, user); SecurityFrameworkUtils.setLoginUser(finalExchange, user);
// 2.2 将 user 并设置到 login-user 的请求头,使用 json 存储值
ServerWebExchange newExchange = finalExchange.mutate() ServerWebExchange newExchange = finalExchange.mutate()
.request(builder -> SecurityFrameworkUtils.setLoginUserHeader(builder, user)).build(); .request(builder -> SecurityFrameworkUtils.setLoginUserHeader(builder, user))
.build();
return chain.filter(newExchange); return chain.filter(newExchange);
}); });
} }
private Mono<LoginUser> getLoginUser(ServerWebExchange exchange, String token) { private Mono<LoginUser> getLoginUser(String token) {
// 从缓存中,获取 LoginUser LoginUser localUser = loginUserCache.getIfPresent(token);
Long tenantId = WebFrameworkUtils.getTenantId(exchange);
KeyValue<Long, String> cacheKey = new KeyValue<Long, String>().setKey(tenantId).setValue(token);
LoginUser localUser = loginUserCache.getIfPresent(cacheKey);
if (localUser != null) { if (localUser != null) {
return Mono.just(localUser); return Mono.just(localUser);
} }
// 缓存不存在,则请求远程服务 return checkAccessToken(token).flatMap((Function<String, Mono<LoginUser>>) body -> {
return checkAccessToken(tenantId, token).flatMap((Function<String, Mono<LoginUser>>) body -> {
LoginUser remoteUser = buildUser(body); LoginUser remoteUser = buildUser(body);
if (remoteUser != null) { if (remoteUser != null) {
// 非空,则进行缓存 loginUserCache.put(token, remoteUser);
loginUserCache.put(cacheKey, remoteUser);
return Mono.just(remoteUser); return Mono.just(remoteUser);
} }
return Mono.empty(); return Mono.empty();
}); });
} }
private Mono<String> checkAccessToken(Long tenantId, String token) { private Mono<String> checkAccessToken(String token) {
return webClient.get() return webClient.get()
.uri(OAuth2TokenCommonApi.URL_CHECK, uriBuilder -> uriBuilder.queryParam("accessToken", token).build()) .uri(OAuth2TokenCommonApi.URL_CHECK, uriBuilder -> uriBuilder.queryParam("accessToken", token).build())
.headers(httpHeaders -> WebFrameworkUtils.setTenantIdHeader(tenantId, httpHeaders)) // 设置租户的 Header .retrieve()
.retrieve().bodyToMono(String.class); .bodyToMono(String.class);
} }
private LoginUser buildUser(String body) { private LoginUser buildUser(String body) {
// 处理结果,结果不正确
CommonResult<OAuth2AccessTokenCheckRespDTO> result = JsonUtils.parseObject(body, CHECK_RESULT_TYPE_REFERENCE); CommonResult<OAuth2AccessTokenCheckRespDTO> result = JsonUtils.parseObject(body, CHECK_RESULT_TYPE_REFERENCE);
if (result == null) { if (result == null) {
return null; return null;
} }
if (result.isError()) { if (result.isError()) {
// 特殊情况令牌已经过期code = 401需要返回 LOGIN_USER_EMPTY避免 Token 一直因为缓存,被误判为有效
if (Objects.equals(result.getCode(), HttpStatus.UNAUTHORIZED.value())) { if (Objects.equals(result.getCode(), HttpStatus.UNAUTHORIZED.value())) {
return LOGIN_USER_EMPTY; return LOGIN_USER_EMPTY;
} }
return null; return null;
} }
// 创建登录用户
OAuth2AccessTokenCheckRespDTO tokenInfo = result.getData(); OAuth2AccessTokenCheckRespDTO tokenInfo = result.getData();
return new LoginUser().setId(tokenInfo.getUserId()).setUserType(tokenInfo.getUserType()) return new LoginUser().setId(tokenInfo.getUserId())
.setInfo(tokenInfo.getUserInfo()) // 额外的用户信息 .setUserType(tokenInfo.getUserType())
.setTenantId(tokenInfo.getTenantId()).setScopes(tokenInfo.getScopes()) .setInfo(tokenInfo.getUserInfo())
.setScopes(tokenInfo.getScopes())
.setExpiresTime(tokenInfo.getExpiresTime()); .setExpiresTime(tokenInfo.getExpiresTime());
} }
@Override @Override
public int getOrder() { public int getOrder() {
return -100; // 和 Spring Security Filter 的顺序对齐 return -100;
} }
} }

View File

@@ -52,7 +52,7 @@ public class GlobalExceptionHandler implements ErrorWebExceptionHandler {
*/ */
private CommonResult<?> responseStatusExceptionHandler(ServerWebExchange exchange, private CommonResult<?> responseStatusExceptionHandler(ServerWebExchange exchange,
ResponseStatusException ex) { ResponseStatusException ex) {
// TODO 芋艿:这里要精细化翻译,默认返回用户是看不懂的 // TODO 这里要精细化翻译,默认返回用户是看不懂的
ServerHttpRequest request = exchange.getRequest(); ServerHttpRequest request = exchange.getRequest();
log.error("[responseStatusExceptionHandler][uri({}/{}) 发生异常]", request.getURI(), request.getMethod(), ex); log.error("[responseStatusExceptionHandler][uri({}/{}) 发生异常]", request.getURI(), request.getMethod(), ex);
return CommonResult.error(ex.getStatusCode().value(), ex.getReason()); return CommonResult.error(ex.getStatusCode().value(), ex.getReason());
@@ -66,7 +66,7 @@ public class GlobalExceptionHandler implements ErrorWebExceptionHandler {
Throwable ex) { Throwable ex) {
ServerHttpRequest request = exchange.getRequest(); ServerHttpRequest request = exchange.getRequest();
log.error("[defaultExceptionHandler][uri({}/{}) 发生异常]", request.getURI(), request.getMethod(), ex); log.error("[defaultExceptionHandler][uri({}/{}) 发生异常]", request.getURI(), request.getMethod(), ex);
// TODO 芋艿:是否要插入异常日志呢? // TODO 是否要插入异常日志呢?
// 返回 ERROR CommonResult // 返回 ERROR CommonResult
return CommonResult.error(INTERNAL_SERVER_ERROR.getCode(), INTERNAL_SERVER_ERROR.getMsg()); return CommonResult.error(INTERNAL_SERVER_ERROR.getCode(), INTERNAL_SERVER_ERROR.getMsg());
} }

View File

@@ -2,91 +2,45 @@ package com.njcn.rdms.gateway.util;
import cn.hutool.core.net.NetUtil; import cn.hutool.core.net.NetUtil;
import cn.hutool.core.util.ArrayUtil; import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.NumberUtil;
import cn.hutool.extra.servlet.ServletUtil; import cn.hutool.extra.servlet.ServletUtil;
import com.njcn.rdms.framework.common.util.json.JsonUtils; import com.njcn.rdms.framework.common.util.json.JsonUtils;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.route.Route; import org.springframework.cloud.gateway.route.Route;
import org.springframework.cloud.gateway.support.ServerWebExchangeUtils; import org.springframework.cloud.gateway.support.ServerWebExchangeUtils;
import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
/**
* Web 工具类
*
* copy from rdms-spring-boot-starter-web 的 WebFrameworkUtils 类
*
* @author hongawen
*/
@Slf4j @Slf4j
public class WebFrameworkUtils { public class WebFrameworkUtils {
private static final String HEADER_TENANT_ID = "tenant-id";
private WebFrameworkUtils() {} private WebFrameworkUtils() {}
/** @SuppressWarnings("deprecation")
* 将 Gateway 请求中的 header设置到 HttpHeaders 中
*
* @param tenantId 租户编号
* @param httpHeaders WebClient 的请求
*/
public static void setTenantIdHeader(Long tenantId, HttpHeaders httpHeaders) {
if (tenantId == null) {
return;
}
httpHeaders.set(HEADER_TENANT_ID, String.valueOf(tenantId));
}
public static Long getTenantId(ServerWebExchange exchange) {
String tenantId = exchange.getRequest().getHeaders().getFirst(HEADER_TENANT_ID);
return NumberUtil.isNumber(tenantId) ? Long.valueOf(tenantId) : null;
}
/**
* 返回 JSON 字符串
*
* @param exchange 响应
* @param object 对象,会序列化成 JSON 字符串
*/
@SuppressWarnings("deprecation") // 必须使用 APPLICATION_JSON_UTF8_VALUE否则会乱码
public static Mono<Void> writeJSON(ServerWebExchange exchange, Object object) { public static Mono<Void> writeJSON(ServerWebExchange exchange, Object object) {
// 设置 header
ServerHttpResponse response = exchange.getResponse(); ServerHttpResponse response = exchange.getResponse();
response.getHeaders().setContentType(MediaType.APPLICATION_JSON_UTF8); response.getHeaders().setContentType(MediaType.APPLICATION_JSON_UTF8);
// 设置 body
return response.writeWith(Mono.fromSupplier(() -> { return response.writeWith(Mono.fromSupplier(() -> {
DataBufferFactory bufferFactory = response.bufferFactory(); DataBufferFactory bufferFactory = response.bufferFactory();
try { try {
return bufferFactory.wrap(JsonUtils.toJsonByte(object)); return bufferFactory.wrap(JsonUtils.toJsonByte(object));
} catch (Exception ex) { } catch (Exception ex) {
ServerHttpRequest request = exchange.getRequest(); ServerHttpRequest request = exchange.getRequest();
log.error("[writeJSON][uri({}/{}) 发生异常]", request.getURI(), request.getMethod(), ex); log.error("[writeJSON][uri({}/{}) error]", request.getURI(), request.getMethod(), ex);
return bufferFactory.wrap(new byte[0]); return bufferFactory.wrap(new byte[0]);
} }
})); }));
} }
/**
* 获得客户端 IP
*
* 参考 {@link ServletUtil} 的 getClientIP 方法
*
* @param exchange 请求
* @param otherHeaderNames 其它 header 名字的数组
* @return 客户端 IP
*/
public static String getClientIP(ServerWebExchange exchange, String... otherHeaderNames) { public static String getClientIP(ServerWebExchange exchange, String... otherHeaderNames) {
String[] headers = { "X-Forwarded-For", "X-Real-IP", "Proxy-Client-IP", "WL-Proxy-Client-IP", "HTTP_CLIENT_IP", "HTTP_X_FORWARDED_FOR" }; String[] headers = {"X-Forwarded-For", "X-Real-IP", "Proxy-Client-IP", "WL-Proxy-Client-IP",
"HTTP_CLIENT_IP", "HTTP_X_FORWARDED_FOR"};
if (ArrayUtil.isNotEmpty(otherHeaderNames)) { if (ArrayUtil.isNotEmpty(otherHeaderNames)) {
headers = ArrayUtil.addAll(headers, otherHeaderNames); headers = ArrayUtil.addAll(headers, otherHeaderNames);
} }
// 方式一,通过 header 获取
String ip; String ip;
for (String header : headers) { for (String header : headers) {
ip = exchange.getRequest().getHeaders().getFirst(header); ip = exchange.getRequest().getHeaders().getFirst(header);
@@ -94,8 +48,6 @@ public class WebFrameworkUtils {
return NetUtil.getMultistageReverseProxyIp(ip); return NetUtil.getMultistageReverseProxyIp(ip);
} }
} }
// 方式二,通过 remoteAddress 获取
if (exchange.getRequest().getRemoteAddress() == null) { if (exchange.getRequest().getRemoteAddress() == null) {
return null; return null;
} }
@@ -103,12 +55,6 @@ public class WebFrameworkUtils {
return NetUtil.getMultistageReverseProxyIp(ip); return NetUtil.getMultistageReverseProxyIp(ip);
} }
/**
* 获得请求匹配的 Route 路由
*
* @param exchange 请求
* @return 路由
*/
public static Route getGatewayRoute(ServerWebExchange exchange) { public static Route getGatewayRoute(ServerWebExchange exchange) {
return exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR); return exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR);
} }

View File

@@ -100,20 +100,6 @@ public interface ErrorCodeConstants {
ErrorCode SMS_CODE_EXCEED_SEND_MAXIMUM_QUANTITY_PER_DAY = new ErrorCode(1_002_014_004, "超过每日短信发送数量"); ErrorCode SMS_CODE_EXCEED_SEND_MAXIMUM_QUANTITY_PER_DAY = new ErrorCode(1_002_014_004, "超过每日短信发送数量");
ErrorCode SMS_CODE_SEND_TOO_FAST = new ErrorCode(1_002_014_005, "短信发送过于频繁"); ErrorCode SMS_CODE_SEND_TOO_FAST = new ErrorCode(1_002_014_005, "短信发送过于频繁");
// ========== 租户信息 1-002-015-000 ==========
ErrorCode TENANT_NOT_EXISTS = new ErrorCode(1_002_015_000, "租户不存在");
ErrorCode TENANT_DISABLE = new ErrorCode(1_002_015_001, "名字为【{}】的租户已被禁用");
ErrorCode TENANT_EXPIRE = new ErrorCode(1_002_015_002, "名字为【{}】的租户已过期");
ErrorCode TENANT_CAN_NOT_UPDATE_SYSTEM = new ErrorCode(1_002_015_003, "系统租户不能进行修改、删除等操作!");
ErrorCode TENANT_NAME_DUPLICATE = new ErrorCode(1_002_015_004, "名字为【{}】的租户已存在");
ErrorCode TENANT_WEBSITE_DUPLICATE = new ErrorCode(1_002_015_005, "域名为【{}】的租户已存在");
// ========== 租户套餐 1-002-016-000 ==========
ErrorCode TENANT_PACKAGE_NOT_EXISTS = new ErrorCode(1_002_016_000, "租户套餐不存在");
ErrorCode TENANT_PACKAGE_USED = new ErrorCode(1_002_016_001, "租户正在使用该套餐,请给租户重新设置套餐后再尝试删除");
ErrorCode TENANT_PACKAGE_DISABLE = new ErrorCode(1_002_016_002, "名字为【{}】的租户套餐已被禁用");
ErrorCode TENANT_PACKAGE_NAME_DUPLICATE = new ErrorCode(1_002_016_003, "已经存在该名字的租户套餐");
// ========== OAuth2 客户端 1-002-020-000 ========= // ========== OAuth2 客户端 1-002-020-000 =========
ErrorCode OAUTH2_CLIENT_NOT_EXISTS = new ErrorCode(1_002_020_000, "OAuth2 客户端不存在"); ErrorCode OAUTH2_CLIENT_NOT_EXISTS = new ErrorCode(1_002_020_000, "OAuth2 客户端不存在");
ErrorCode OAUTH2_CLIENT_EXISTS = new ErrorCode(1_002_020_001, "OAuth2 客户端编号已存在"); ErrorCode OAUTH2_CLIENT_EXISTS = new ErrorCode(1_002_020_001, "OAuth2 客户端编号已存在");

View File

@@ -4,25 +4,14 @@ import com.njcn.rdms.framework.common.util.object.ObjectUtils;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Getter; import lombok.Getter;
/**
* 角色标识枚举
*/
@Getter @Getter
@AllArgsConstructor @AllArgsConstructor
public enum RoleCodeEnum { public enum RoleCodeEnum {
SUPER_ADMIN("super_admin", "超级管理员"), SUPER_ADMIN("super_admin", "超级管理员"),
TENANT_ADMIN("tenant_admin", "租户管理员"), CRM_ADMIN("crm_admin", "CRM 管理员");
CRM_ADMIN("crm_admin", "CRM 管理员"); // CRM 系统专用
;
/**
* 角色编码
*/
private final String code; private final String code;
/**
* 名字
*/
private final String name; private final String name;
public static boolean isSuperAdmin(String code) { public static boolean isSuperAdmin(String code) {

View File

@@ -1,12 +1,11 @@
package com.njcn.rdms.module.system.api.oauth2; package com.njcn.rdms.module.system.api.oauth2;
import com.njcn.rdms.framework.common.biz.system.oauth2.OAuth2TokenCommonApi; import com.njcn.rdms.framework.common.biz.system.oauth2.OAuth2TokenCommonApi;
import com.njcn.rdms.framework.common.pojo.CommonResult;
import com.njcn.rdms.framework.common.util.object.BeanUtils;
import com.njcn.rdms.framework.common.biz.system.oauth2.dto.OAuth2AccessTokenCheckRespDTO; import com.njcn.rdms.framework.common.biz.system.oauth2.dto.OAuth2AccessTokenCheckRespDTO;
import com.njcn.rdms.framework.common.biz.system.oauth2.dto.OAuth2AccessTokenCreateReqDTO; import com.njcn.rdms.framework.common.biz.system.oauth2.dto.OAuth2AccessTokenCreateReqDTO;
import com.njcn.rdms.framework.common.biz.system.oauth2.dto.OAuth2AccessTokenRespDTO; import com.njcn.rdms.framework.common.biz.system.oauth2.dto.OAuth2AccessTokenRespDTO;
import com.njcn.rdms.framework.common.pojo.CommonResult;
import com.njcn.rdms.framework.common.util.object.BeanUtils;
import com.njcn.rdms.module.system.dal.dataobject.oauth2.OAuth2AccessTokenDO; import com.njcn.rdms.module.system.dal.dataobject.oauth2.OAuth2AccessTokenDO;
import com.njcn.rdms.module.system.service.oauth2.OAuth2TokenService; import com.njcn.rdms.module.system.service.oauth2.OAuth2TokenService;
import jakarta.annotation.Resource; import jakarta.annotation.Resource;
@@ -15,7 +14,7 @@ import org.springframework.web.bind.annotation.RestController;
import static com.njcn.rdms.framework.common.pojo.CommonResult.success; import static com.njcn.rdms.framework.common.pojo.CommonResult.success;
@RestController // 提供 RESTful API 接口,给 Feign 调用 @RestController
@Validated @Validated
public class OAuth2TokenApiImpl implements OAuth2TokenCommonApi { public class OAuth2TokenApiImpl implements OAuth2TokenCommonApi {
@@ -30,7 +29,6 @@ public class OAuth2TokenApiImpl implements OAuth2TokenCommonApi {
} }
@Override @Override
// 访问令牌校验时,无需传递租户编号;主要解决上传文件的场景,前端不会传递 tenant-id
public CommonResult<OAuth2AccessTokenCheckRespDTO> checkAccessToken(String accessToken) { public CommonResult<OAuth2AccessTokenCheckRespDTO> checkAccessToken(String accessToken) {
OAuth2AccessTokenDO accessTokenDO = oauth2TokenService.checkAccessToken(accessToken); OAuth2AccessTokenDO accessTokenDO = oauth2TokenService.checkAccessToken(accessToken);
return success(BeanUtils.toBean(accessTokenDO, OAuth2AccessTokenCheckRespDTO.class)); return success(BeanUtils.toBean(accessTokenDO, OAuth2AccessTokenCheckRespDTO.class));

View File

@@ -1,7 +1,6 @@
### 请求 /login 接口 => 成功 ### 请求 /login 接口 => 成功
POST {{baseUrl}}/system/auth/login POST {{baseUrl}}/system/auth/login
Content-Type: application/json Content-Type: application/json
tenant-id: {{adminTenantId}}
tag: Yunai.local tag: Yunai.local
{ {
@@ -14,7 +13,6 @@ tag: Yunai.local
### 请求 /login 接口【加密 AES】 => 成功 ### 请求 /login 接口【加密 AES】 => 成功
POST {{baseUrl}}/system/auth/login POST {{baseUrl}}/system/auth/login
Content-Type: application/json Content-Type: application/json
tenant-id: {{adminTenantId}}
tag: Yunai.local tag: Yunai.local
X-API-ENCRYPT: true X-API-ENCRYPT: true
@@ -23,7 +21,6 @@ WvSX9MOrenyGfBhEM0g1/hHgq8ocktMZ9OwAJ6MOG5FUrzYF/rG5JF1eMptQM1wT73VgDS05l/37WeRt
### 请求 /login 接口【加密 RSA】 => 成功 ### 请求 /login 接口【加密 RSA】 => 成功
POST {{baseUrl}}/system/auth/login POST {{baseUrl}}/system/auth/login
Content-Type: application/json Content-Type: application/json
tenant-id: {{adminTenantId}}
tag: Yunai.local tag: Yunai.local
X-API-ENCRYPT: true X-API-ENCRYPT: true
@@ -32,7 +29,6 @@ e7QZTork9ZV5CmgZvSd+cHZk3xdUxKtowLM02kOha+gxHK2H/daU8nVBYS3+bwuDRy5abf+Pz1QJJGVA
### 请求 /login 接口 => 成功(无验证码) ### 请求 /login 接口 => 成功(无验证码)
POST {{baseUrl}}/system/auth/login POST {{baseUrl}}/system/auth/login
Content-Type: application/json Content-Type: application/json
tenant-id: {{adminTenantId}}
{ {
"username": "admin", "username": "admin",
@@ -42,10 +38,8 @@ tenant-id: {{adminTenantId}}
### 请求 /get-permission-info 接口 => 成功 ### 请求 /get-permission-info 接口 => 成功
GET {{baseUrl}}/system/auth/get-permission-info GET {{baseUrl}}/system/auth/get-permission-info
Authorization: Bearer {{token}} Authorization: Bearer {{token}}
tenant-id: {{adminTenantId}}
### 请求 /list-menus 接口 => 成功 ### 请求 /list-menus 接口 => 成功
GET {{baseUrl}}/system/list-menus GET {{baseUrl}}/system/list-menus
Authorization: Bearer {{token}} Authorization: Bearer {{token}}
#Authorization: Bearer a6aa7714a2e44c95aaa8a2c5adc2a67a #Authorization: Bearer a6aa7714a2e44c95aaa8a2c5adc2a67a
tenant-id: {{adminTenantId}}

View File

@@ -1,4 +1,3 @@
### 请求 /menu/list 接口 => 成功 ### 请求 /menu/list 接口 => 成功
GET {{baseUrl}}/system/dict-data/list-all-simple GET {{baseUrl}}/system/dict-data/list-all-simple
Authorization: Bearer {{token}} Authorization: Bearer {{token}}
tenant-id: {{adminTenantId}}

View File

@@ -1,7 +1,6 @@
### 请求 /system/file-config/create 接口 => 成功 ### 请求 /system/file-config/create 接口 => 成功
POST {{baseUrl}}/system/file-config/create POST {{baseUrl}}/system/file-config/create
Content-Type: application/json Content-Type: application/json
tenant-id: {{adminTenantId}}
Authorization: Bearer {{token}} Authorization: Bearer {{token}}
{ {
@@ -21,7 +20,6 @@ Authorization: Bearer {{token}}
### 请求 /system/file-config/update 接口 => 成功 ### 请求 /system/file-config/update 接口 => 成功
PUT {{baseUrl}}/system/file-config/update PUT {{baseUrl}}/system/file-config/update
Content-Type: application/json Content-Type: application/json
tenant-id: {{adminTenantId}}
Authorization: Bearer {{token}} Authorization: Bearer {{token}}
{ {
@@ -41,6 +39,4 @@ Authorization: Bearer {{token}}
### 请求 /system/file-config/test 接口 => 成功 ### 请求 /system/file-config/test 接口 => 成功
GET {{baseUrl}}/system/file-config/test?id=2 GET {{baseUrl}}/system/file-config/test?id=2
Content-Type: application/json Content-Type: application/json
tenant-id: {{adminTenantId}}
Authorization: Bearer {{token}} Authorization: Bearer {{token}}

View File

@@ -1,5 +1,3 @@
### 获得地区树 ### 获得地区树
GET {{baseUrl}}/system/area/tree GET {{baseUrl}}/system/area/tree
Authorization: Bearer {{token}} Authorization: Bearer {{token}}
tenant-id: {{adminTenantId}}

View File

@@ -1,4 +1,3 @@
### 请求 /system/operate-log/page 接口 => 成功 ### 请求 /system/operate-log/page 接口 => 成功
GET {{baseUrl}}/system/operate-log/page GET {{baseUrl}}/system/operate-log/page
Authorization: Bearer {{token}} Authorization: Bearer {{token}}
tenant-id: {{adminTenantId}}

View File

@@ -2,7 +2,6 @@
POST {{baseUrl}}/system/oauth2-client/create POST {{baseUrl}}/system/oauth2-client/create
Content-Type: application/json Content-Type: application/json
Authorization: Bearer {{token}} Authorization: Bearer {{token}}
tenant-id: {{adminTenantId}}
{ {
"id": "1", "id": "1",

View File

@@ -1,13 +1,11 @@
### 请求 /system/oauth2/authorize 接口 => 成功 ### 请求 /system/oauth2/authorize 接口 => 成功
GET {{baseUrl}}/system/oauth2/authorize?clientId=default GET {{baseUrl}}/system/oauth2/authorize?clientId=default
Authorization: Bearer {{token}} Authorization: Bearer {{token}}
tenant-id: {{adminTenantId}}
### 请求 /system/oauth2/authorize + token 接口 => 成功 ### 请求 /system/oauth2/authorize + token 接口 => 成功
POST {{baseUrl}}/system/oauth2/authorize POST {{baseUrl}}/system/oauth2/authorize
Content-Type: application/x-www-form-urlencoded Content-Type: application/x-www-form-urlencoded
Authorization: Bearer {{token}} Authorization: Bearer {{token}}
tenant-id: {{adminTenantId}}
response_type=token&client_id=default&scope={"user.read": true}&redirect_uri=https://www.iocoder.cn&auto_approve=true response_type=token&client_id=default&scope={"user.read": true}&redirect_uri=https://www.iocoder.cn&auto_approve=true
@@ -15,7 +13,6 @@ response_type=token&client_id=default&scope={"user.read": true}&redirect_uri=htt
POST {{baseUrl}}/system/oauth2/authorize POST {{baseUrl}}/system/oauth2/authorize
Content-Type: application/x-www-form-urlencoded Content-Type: application/x-www-form-urlencoded
Authorization: Bearer {{token}} Authorization: Bearer {{token}}
tenant-id: {{adminTenantId}}
response_type=code&client_id=default&scope={"user.read": true}&redirect_uri=https://www.iocoder.cn&auto_approve=false response_type=code&client_id=default&scope={"user.read": true}&redirect_uri=https://www.iocoder.cn&auto_approve=false
@@ -23,7 +20,6 @@ response_type=code&client_id=default&scope={"user.read": true}&redirect_uri=http
POST {{baseUrl}}/system/oauth2/token POST {{baseUrl}}/system/oauth2/token
Content-Type: application/x-www-form-urlencoded Content-Type: application/x-www-form-urlencoded
Authorization: Basic ZGVmYXVsdDphZG1pbjEyMw== Authorization: Basic ZGVmYXVsdDphZG1pbjEyMw==
tenant-id: {{adminTenantId}}
grant_type=authorization_code&redirect_uri=https://www.iocoder.cn&code=189956c07a174588a97157eabef2f93a grant_type=authorization_code&redirect_uri=https://www.iocoder.cn&code=189956c07a174588a97157eabef2f93a
@@ -31,7 +27,6 @@ grant_type=authorization_code&redirect_uri=https://www.iocoder.cn&code=189956c07
POST {{baseUrl}}/system/oauth2/token POST {{baseUrl}}/system/oauth2/token
Content-Type: application/x-www-form-urlencoded Content-Type: application/x-www-form-urlencoded
Authorization: Basic ZGVmYXVsdDphZG1pbjEyMw== Authorization: Basic ZGVmYXVsdDphZG1pbjEyMw==
tenant-id: {{adminTenantId}}
grant_type=password&username=admin&password=admin123&scope=user.read grant_type=password&username=admin&password=admin123&scope=user.read
@@ -39,7 +34,6 @@ grant_type=password&username=admin&password=admin123&scope=user.read
POST {{baseUrl}}/system/oauth2/token POST {{baseUrl}}/system/oauth2/token
Content-Type: application/x-www-form-urlencoded Content-Type: application/x-www-form-urlencoded
Authorization: Basic ZGVmYXVsdDphZG1pbjEyMw== Authorization: Basic ZGVmYXVsdDphZG1pbjEyMw==
tenant-id: {{adminTenantId}}
grant_type=client_credentials&scope=user.read grant_type=client_credentials&scope=user.read
@@ -47,16 +41,13 @@ grant_type=client_credentials&scope=user.read
POST {{baseUrl}}/system/oauth2/token POST {{baseUrl}}/system/oauth2/token
Content-Type: application/x-www-form-urlencoded Content-Type: application/x-www-form-urlencoded
Authorization: Basic ZGVmYXVsdDphZG1pbjEyMw== Authorization: Basic ZGVmYXVsdDphZG1pbjEyMw==
tenant-id: {{adminTenantId}}
grant_type=refresh_token&refresh_token=00895465d6994f72a9d926ceeed0f588 grant_type=refresh_token&refresh_token=00895465d6994f72a9d926ceeed0f588
### 请求 /system/oauth2/token + DELETE 接口 => 成功 ### 请求 /system/oauth2/token + DELETE 接口 => 成功
DELETE {{baseUrl}}/system/oauth2/token?token=ca8a188f464441d6949c51493a2b7596 DELETE {{baseUrl}}/system/oauth2/token?token=ca8a188f464441d6949c51493a2b7596
Authorization: Basic ZGVmYXVsdDphZG1pbjEyMw== Authorization: Basic ZGVmYXVsdDphZG1pbjEyMw==
tenant-id: {{adminTenantId}}
### 请求 /system/oauth2/check-token 接口 => 成功 ### 请求 /system/oauth2/check-token 接口 => 成功
POST {{baseUrl}}/system/oauth2/check-token?token=620d307c5b4148df8a98dd6c6c547106 POST {{baseUrl}}/system/oauth2/check-token?token=620d307c5b4148df8a98dd6c6c547106
Authorization: Basic ZGVmYXVsdDphZG1pbjEyMw== Authorization: Basic ZGVmYXVsdDphZG1pbjEyMw==
tenant-id: {{adminTenantId}}

View File

@@ -1,13 +1,11 @@
### 请求 /system/oauth2/user/get 接口 => 成功 ### 请求 /system/oauth2/user/get 接口 => 成功
GET {{baseUrl}}/system/oauth2/user/get GET {{baseUrl}}/system/oauth2/user/get
Authorization: Bearer 47f9c74ec11041f193b777ebb95c3b0d Authorization: Bearer 47f9c74ec11041f193b777ebb95c3b0d
tenant-id: {{adminTenantId}}
### 请求 /system/oauth2/user/update 接口 => 成功 ### 请求 /system/oauth2/user/update 接口 => 成功
PUT {{baseUrl}}/system/oauth2/user/update PUT {{baseUrl}}/system/oauth2/user/update
Content-Type: application/json Content-Type: application/json
Authorization: Bearer 47f9c74ec11041f193b777ebb95c3b0d Authorization: Bearer 47f9c74ec11041f193b777ebb95c3b0d
tenant-id: {{adminTenantId}}
{ {
"nickname": "灿能源码" "nickname": "灿能源码"

View File

@@ -8,33 +8,32 @@ import lombok.NoArgsConstructor;
import java.util.List; import java.util.List;
@Schema(description = "管理后台 - 【开放接口】校验令牌 Response VO") @Schema(description = "Admin API - open check token response")
@Data @Data
@NoArgsConstructor @NoArgsConstructor
@AllArgsConstructor @AllArgsConstructor
public class OAuth2OpenCheckTokenRespVO { public class OAuth2OpenCheckTokenRespVO {
@Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "666") @Schema(description = "User id", requiredMode = Schema.RequiredMode.REQUIRED, example = "666")
@JsonProperty("user_id") @JsonProperty("user_id")
private Long userId; private Long userId;
@Schema(description = "用户类型,参见 UserTypeEnum 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
@Schema(description = "User type", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
@JsonProperty("user_type") @JsonProperty("user_type")
private Integer userType; private Integer userType;
@Schema(description = "租户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
@JsonProperty("tenant_id")
private Long tenantId;
@Schema(description = "客户端编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "car") @Schema(description = "Client id", requiredMode = Schema.RequiredMode.REQUIRED, example = "car")
@JsonProperty("client_id") @JsonProperty("client_id")
private String clientId; private String clientId;
@Schema(description = "授权范围", requiredMode = Schema.RequiredMode.REQUIRED, example = "user_info")
@Schema(description = "Scopes", requiredMode = Schema.RequiredMode.REQUIRED, example = "user_info")
private List<String> scopes; private List<String> scopes;
@Schema(description = "访问令牌", requiredMode = Schema.RequiredMode.REQUIRED, example = "tudou") @Schema(description = "Access token", requiredMode = Schema.RequiredMode.REQUIRED, example = "tudou")
@JsonProperty("access_token") @JsonProperty("access_token")
private String accessToken; private String accessToken;
@Schema(description = "过期时间,时间戳 / 1000即单位", requiredMode = Schema.RequiredMode.REQUIRED, example = "1593092157") @Schema(description = "Expire timestamp in seconds", requiredMode = Schema.RequiredMode.REQUIRED, example = "1593092157")
private Long exp; private Long exp;
} }

View File

@@ -1,4 +1,3 @@
### 请求 /menu/list 接口 => 成功 ### 请求 /menu/list 接口 => 成功
GET {{baseUrl}}/system/menu/list GET {{baseUrl}}/system/menu/list
Authorization: Bearer {{token}} Authorization: Bearer {{token}}
tenant-id: {{adminTenantId}}

View File

@@ -16,7 +16,14 @@ import jakarta.annotation.Resource;
import jakarta.validation.Valid; import jakarta.validation.Valid;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated; import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.Comparator; import java.util.Comparator;
import java.util.List; import java.util.List;
@@ -50,7 +57,7 @@ public class MenuController {
@DeleteMapping("/delete") @DeleteMapping("/delete")
@Operation(summary = "删除菜单") @Operation(summary = "删除菜单")
@Parameter(name = "id", description = "菜单编号", required= true, example = "1024") @Parameter(name = "id", description = "菜单编号", required = true, example = "1024")
@PreAuthorize("@ss.hasPermission('system:menu:delete')") @PreAuthorize("@ss.hasPermission('system:menu:delete')")
public CommonResult<Boolean> deleteMenu(@RequestParam("id") Long id) { public CommonResult<Boolean> deleteMenu(@RequestParam("id") Long id) {
menuService.deleteMenu(id); menuService.deleteMenu(id);
@@ -76,11 +83,9 @@ public class MenuController {
} }
@GetMapping({"/list-all-simple", "simple-list"}) @GetMapping({"/list-all-simple", "simple-list"})
@Operation(summary = "获取菜单精简信息列表", @Operation(summary = "获取菜单精简信息列表", description = "只包含已启用的菜单,用于【角色分配菜单】功能的选项")
description = "只包含被开启的菜单,用于【角色分配菜单】功能的选项。在多租户的场景下,会只返回租户所在套餐有的菜单")
public CommonResult<List<MenuSimpleRespVO>> getSimpleMenuList() { public CommonResult<List<MenuSimpleRespVO>> getSimpleMenuList() {
List<MenuDO> list = menuService.getMenuListByTenant( List<MenuDO> list = menuService.getMenuList(new MenuListReqVO().setStatus(CommonStatusEnum.ENABLE.getStatus()));
new MenuListReqVO().setStatus(CommonStatusEnum.ENABLE.getStatus()));
list = menuService.filterDisableMenus(list); list = menuService.filterDisableMenus(list);
list.sort(Comparator.comparing(MenuDO::getSort)); list.sort(Comparator.comparing(MenuDO::getSort));
return success(BeanUtils.toBean(list, MenuSimpleRespVO.class)); return success(BeanUtils.toBean(list, MenuSimpleRespVO.class));

View File

@@ -2,7 +2,6 @@
POST {{baseUrl}}/system/role/create POST {{baseUrl}}/system/role/create
Authorization: Bearer {{token}} Authorization: Bearer {{token}}
Content-Type: application/json Content-Type: application/json
tenant-id: {{adminTenantId}}
{ {
"name": "测试角色", "name": "测试角色",
@@ -14,7 +13,6 @@ tenant-id: {{adminTenantId}}
POST {{baseUrl}}/system/role/update POST {{baseUrl}}/system/role/update
Authorization: Bearer {{token}} Authorization: Bearer {{token}}
Content-Type: application/json Content-Type: application/json
tenant-id: {{adminTenantId}}
{ {
"id": 100, "id": 100,
@@ -26,7 +24,6 @@ tenant-id: {{adminTenantId}}
POST {{baseUrl}}/system/role/delete POST {{baseUrl}}/system/role/delete
Content-Type: application/x-www-form-urlencoded Content-Type: application/x-www-form-urlencoded
Authorization: Bearer {{token}} Authorization: Bearer {{token}}
tenant-id: {{adminTenantId}}
roleId=14 roleId=14
@@ -34,9 +31,7 @@ roleId=14
GET {{baseUrl}}/system/role/get?id=100 GET {{baseUrl}}/system/role/get?id=100
Content-Type: application/x-www-form-urlencoded Content-Type: application/x-www-form-urlencoded
Authorization: Bearer {{token}} Authorization: Bearer {{token}}
tenant-id: {{adminTenantId}}
### /role/page 成功 ### /role/page 成功
GET {{baseUrl}}/system/role/page?pageNo=1&pageSize=10 GET {{baseUrl}}/system/role/page?pageNo=1&pageSize=10
Authorization: Bearer {{token}} Authorization: Bearer {{token}}
tenant-id: {{adminTenantId}}

View File

@@ -1,5 +1,4 @@
### 请求 /system/redis/get-monitor-info 接口 => 成功 ### 请求 /system/redis/get-monitor-info 接口 => 成功
GET {{baseUrl}}/system/redis/get-monitor-info GET {{baseUrl}}/system/redis/get-monitor-info
Authorization: Bearer {{token}} Authorization: Bearer {{token}}
tenant-id: {{adminTenantId}}

View File

@@ -2,10 +2,7 @@
GET {{baseUrl}}/system/user/page?pageNo=1&pageSize=10 GET {{baseUrl}}/system/user/page?pageNo=1&pageSize=10
Authorization: Bearer {{token}} Authorization: Bearer {{token}}
#Authorization: Bearer test100 #Authorization: Bearer test100
tenant-id: {{adminTenantId}}
### 请求 /system/user/page 接口(测试访问别的租户) ### 请求 /system/user/page 接口(测试访问别的租户)
GET {{baseUrl}}/system/user/page?pageNo=1&pageSize=10 GET {{baseUrl}}/system/user/page?pageNo=1&pageSize=10
Authorization: Bearer {{token}} Authorization: Bearer {{token}}
tenant-id: {{adminTenantId}}
visit-tenant-id: 122

View File

@@ -1,4 +1,3 @@
### 请求 /system/user/profile/get 接口 => 没有权限 ### 请求 /system/user/profile/get 接口 => 没有权限
GET {{baseUrl}}/system/user/profile/get GET {{baseUrl}}/system/user/profile/get
Authorization: Bearer {{token}} Authorization: Bearer {{token}}
tenant-id: {{adminTenantId}}

View File

@@ -7,96 +7,26 @@ import com.njcn.rdms.module.system.dal.dataobject.permission.MenuDO;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
/**
* 菜单 Service 接口
*
* @author hongawen
*/
public interface MenuService { public interface MenuService {
/**
* 创建菜单
*
* @param createReqVO 菜单信息
* @return 创建出来的菜单编号
*/
Long createMenu(MenuSaveVO createReqVO); Long createMenu(MenuSaveVO createReqVO);
/**
* 更新菜单
*
* @param updateReqVO 菜单信息
*/
void updateMenu(MenuSaveVO updateReqVO); void updateMenu(MenuSaveVO updateReqVO);
/**
* 删除菜单
*
* @param id 菜单编号
*/
void deleteMenu(Long id); void deleteMenu(Long id);
/**
* 批量删除菜单
*
* @param ids 菜单编号数组
*/
void deleteMenuList(List<Long> ids); void deleteMenuList(List<Long> ids);
/**
* 获得所有菜单列表
*
* @return 菜单列表
*/
List<MenuDO> getMenuList(); List<MenuDO> getMenuList();
/**
* 基于租户,筛选菜单列表
* 注意,如果是系统租户,返回的还是全菜单
*
* @param reqVO 筛选条件请求 VO
* @return 菜单列表
*/
List<MenuDO> getMenuListByTenant(MenuListReqVO reqVO);
/**
* 过滤掉关闭的菜单及其子菜单
*
* @param list 菜单列表
* @return 过滤后的菜单列表
*/
List<MenuDO> filterDisableMenus(List<MenuDO> list); List<MenuDO> filterDisableMenus(List<MenuDO> list);
/**
* 筛选菜单列表
*
* @param reqVO 筛选条件请求 VO
* @return 菜单列表
*/
List<MenuDO> getMenuList(MenuListReqVO reqVO); List<MenuDO> getMenuList(MenuListReqVO reqVO);
/**
* 获得权限对应的菜单编号数组
*
* @param permission 权限标识
* @return 数组
*/
List<Long> getMenuIdListByPermissionFromCache(String permission); List<Long> getMenuIdListByPermissionFromCache(String permission);
/**
* 获得菜单
*
* @param id 菜单编号
* @return 菜单
*/
MenuDO getMenu(Long id); MenuDO getMenu(Long id);
/**
* 获得菜单数组
*
* @param ids 菜单编号数组
* @return 菜单数组
*/
List<MenuDO> getMenuList(Collection<Long> ids); List<MenuDO> getMenuList(Collection<Long> ids);
} }

View File

@@ -125,13 +125,6 @@ public class MenuServiceImpl implements MenuService {
return menuMapper.selectList(); return menuMapper.selectList();
} }
@Override
public List<MenuDO> getMenuListByTenant(MenuListReqVO reqVO) {
// 查询所有菜单,并过滤掉关闭的节点
List<MenuDO> menus = getMenuList(reqVO);
return menus;
}
@Override @Override
public List<MenuDO> filterDisableMenus(List<MenuDO> menuList) { public List<MenuDO> filterDisableMenus(List<MenuDO> menuList) {
if (CollUtil.isEmpty(menuList)){ if (CollUtil.isEmpty(menuList)){