refactor(platform): 移除旧版开放平台模块并迁移至独立仓库
- 删除api返回结果封装(ApiResult)、业务异常(BusinessException)、系统常量(Constants)等基础模块代码 - 移除开放平台上下文(OpenCaller)、租户上下文(OpenTenantContext)及相关拦截器等实现 - 删除开放平台安全配置、安全异常处理、JWT校验配置和相关测试用例 - 调整 MybatisPlusConfig 中对 OpenTenantContext 包路径的引用 - 将开放平台controller、参数、视图、配置、拦截器等迁移至新的独立仓库包名 com.gxwebsoft.platform.openapi - 开放接口controller添加 @OpenApi 注解用于异常处理匹配 - 更新文档,补充开放平台模块移除和迁移说明,指导后续开发维护 - 保持包名兼容,确保遗留代码引用不变,无需做import改动
This commit is contained in:
@@ -1,93 +0,0 @@
|
||||
package com.gxwebsoft.common.core;
|
||||
|
||||
/**
|
||||
* 系统常量
|
||||
* Created by WebSoft on 2019-10-29 15:55
|
||||
*/
|
||||
public class Constants {
|
||||
/**
|
||||
* 默认成功码
|
||||
*/
|
||||
public static final int RESULT_OK_CODE = 0;
|
||||
|
||||
/**
|
||||
* 默认失败码
|
||||
*/
|
||||
public static final int RESULT_ERROR_CODE = 1;
|
||||
|
||||
/**
|
||||
* 默认成功信息
|
||||
*/
|
||||
public static final String RESULT_OK_MSG = "操作成功";
|
||||
|
||||
/**
|
||||
* 默认失败信息
|
||||
*/
|
||||
public static final String RESULT_ERROR_MSG = "操作失败";
|
||||
|
||||
/**
|
||||
* 无权限错误码
|
||||
*/
|
||||
public static final int UNAUTHORIZED_CODE = 403;
|
||||
|
||||
/**
|
||||
* 无权限提示信息
|
||||
*/
|
||||
public static final String UNAUTHORIZED_MSG = "没有访问权限";
|
||||
|
||||
/**
|
||||
* 未认证错误码
|
||||
*/
|
||||
public static final int UNAUTHENTICATED_CODE = 401;
|
||||
|
||||
/**
|
||||
* 未认证提示信息
|
||||
*/
|
||||
public static final String UNAUTHENTICATED_MSG = "请先登录";
|
||||
|
||||
/**
|
||||
* 登录过期错误码
|
||||
*/
|
||||
public static final int TOKEN_EXPIRED_CODE = 401;
|
||||
|
||||
/**
|
||||
* 登录过期提示信息
|
||||
*/
|
||||
public static final String TOKEN_EXPIRED_MSG = "登录已过期";
|
||||
|
||||
/**
|
||||
* 非法token错误码
|
||||
*/
|
||||
public static final int BAD_CREDENTIALS_CODE = 401;
|
||||
|
||||
/**
|
||||
* 非法token提示信息
|
||||
*/
|
||||
public static final String BAD_CREDENTIALS_MSG = "请退出重新登录";
|
||||
|
||||
/**
|
||||
* 表示升序的值
|
||||
*/
|
||||
public static final String ORDER_ASC_VALUE = "asc";
|
||||
|
||||
/**
|
||||
* 表示降序的值
|
||||
*/
|
||||
public static final String ORDER_DESC_VALUE = "desc";
|
||||
|
||||
/**
|
||||
* token通过header传递的名称
|
||||
*/
|
||||
public static final String TOKEN_HEADER_NAME = "Authorization";
|
||||
|
||||
/**
|
||||
* token通过参数传递的名称
|
||||
*/
|
||||
public static final String TOKEN_PARAM_NAME = "access_token";
|
||||
|
||||
/**
|
||||
* token认证类型
|
||||
*/
|
||||
public static final String TOKEN_TYPE = "Bearer";
|
||||
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler;
|
||||
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
|
||||
import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor;
|
||||
import com.gxwebsoft.common.core.utils.RedisUtil;
|
||||
import com.gxwebsoft.openplatform.context.OpenTenantContext;
|
||||
import com.gxwebsoft.platform.openapi.context.OpenTenantContext;
|
||||
import com.gxwebsoft.common.system.entity.User;
|
||||
import net.sf.jsqlparser.expression.Expression;
|
||||
import net.sf.jsqlparser.expression.LongValue;
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
package com.gxwebsoft.common.core.exception;
|
||||
|
||||
import com.gxwebsoft.common.core.Constants;
|
||||
|
||||
/**
|
||||
* 自定义业务异常
|
||||
*
|
||||
* @author WebSoft
|
||||
* @since 2018-02-22 11:29:28
|
||||
*/
|
||||
public class BusinessException extends RuntimeException {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private Integer code;
|
||||
|
||||
public BusinessException() {
|
||||
this(Constants.RESULT_ERROR_MSG);
|
||||
}
|
||||
|
||||
public BusinessException(String message) {
|
||||
this(Constants.RESULT_ERROR_CODE, message);
|
||||
}
|
||||
|
||||
public BusinessException(Integer code, String message) {
|
||||
super(message);
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
public BusinessException(Integer code, String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
public BusinessException(Integer code, String message, Throwable cause,
|
||||
boolean enableSuppression, boolean writableStackTrace) {
|
||||
super(message, cause, enableSuppression, writableStackTrace);
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
public Integer getCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
public void setCode(Integer code) {
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
package com.gxwebsoft.common.core.web;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 返回结果
|
||||
*
|
||||
* @author WebSoft
|
||||
* @since 2017-06-10 10:10:50
|
||||
*/
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public class ApiResult<T> implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "状态码")
|
||||
private Integer code;
|
||||
|
||||
@Schema(description = "状态信息")
|
||||
private String message;
|
||||
|
||||
@Schema(description = "返回数据")
|
||||
private T data;
|
||||
|
||||
@Schema(description = "错误信息")
|
||||
private String error;
|
||||
|
||||
public ApiResult() {}
|
||||
|
||||
public ApiResult(Integer code) {
|
||||
this(code, null);
|
||||
}
|
||||
|
||||
public ApiResult(Integer code, String message) {
|
||||
this(code, message, null);
|
||||
}
|
||||
|
||||
public ApiResult(Integer code, String message, T data) {
|
||||
this(code, message, data, null);
|
||||
}
|
||||
|
||||
public ApiResult(Integer code, String message, T data, String error) {
|
||||
setCode(code);
|
||||
setMessage(message);
|
||||
setData(data);
|
||||
setError(error);
|
||||
}
|
||||
|
||||
public Integer getCode() {
|
||||
return this.code;
|
||||
}
|
||||
|
||||
public ApiResult<T> setCode(Integer code) {
|
||||
this.code = code;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getMessage() {
|
||||
return this.message;
|
||||
}
|
||||
|
||||
public ApiResult<T> setMessage(String message) {
|
||||
this.message = message;
|
||||
return this;
|
||||
}
|
||||
|
||||
public T getData() {
|
||||
return this.data;
|
||||
}
|
||||
|
||||
public ApiResult<T> setData(T data) {
|
||||
this.data = data;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getError() {
|
||||
return this.error;
|
||||
}
|
||||
|
||||
public ApiResult<T> setError(String error) {
|
||||
this.error = error;
|
||||
return this;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
package com.gxwebsoft.common.core.web;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 分页查询返回结果
|
||||
*
|
||||
* @author WebSoft
|
||||
* @since 2017-06-10 10:10:02
|
||||
*/
|
||||
public class PageResult<T> implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "当前页数据")
|
||||
private List<T> list;
|
||||
|
||||
@Schema(description = "总数量")
|
||||
private Long count;
|
||||
|
||||
public PageResult() {
|
||||
}
|
||||
|
||||
public PageResult(List<T> list) {
|
||||
this(list, null);
|
||||
}
|
||||
|
||||
public PageResult(List<T> list, Long count) {
|
||||
setList(list);
|
||||
setCount(count);
|
||||
}
|
||||
|
||||
public List<T> getList() {
|
||||
return this.list;
|
||||
}
|
||||
|
||||
public void setList(List<T> list) {
|
||||
this.list = list;
|
||||
}
|
||||
|
||||
public Long getCount() {
|
||||
return this.count;
|
||||
}
|
||||
|
||||
public void setCount(Long count) {
|
||||
this.count = count;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
package com.gxwebsoft.openplatform.config;
|
||||
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator;
|
||||
import org.springframework.security.oauth2.core.OAuth2Error;
|
||||
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
|
||||
import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
import org.springframework.security.oauth2.jwt.JwtDecoder;
|
||||
import org.springframework.security.oauth2.jwt.JwtValidators;
|
||||
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
|
||||
/**
|
||||
* 开放平台令牌校验配置。
|
||||
*
|
||||
* <p><b>不要使用 {@code spring.security.oauth2.resourceserver.jwt.issuer-uri}</b>:配置该项后
|
||||
* Spring Security 会在启动时请求 {@code {issuer}/.well-known/openid-configuration} 做 OIDC 发现,
|
||||
* 而 base-api 并未提供该文档(会返回「请先登录」),启动会直接失败。这里只配置 JWKS 地址,
|
||||
* issuer 与 audience 用显式校验器声明。</p>
|
||||
*
|
||||
* @author WebSoft
|
||||
*/
|
||||
@Configuration
|
||||
@ConditionalOnProperty(prefix = "open-platform", name = "enabled", havingValue = "true", matchIfMissing = true)
|
||||
public class OpenPlatformJwtConfig {
|
||||
|
||||
@Resource
|
||||
private OpenPlatformProperties properties;
|
||||
|
||||
@Bean
|
||||
public JwtDecoder openPlatformJwtDecoder() {
|
||||
// 只配置 JWKS:公钥懒加载,启动时不依赖 base-api 可达
|
||||
NimbusJwtDecoder decoder = NimbusJwtDecoder
|
||||
.withJwkSetUri(properties.getJwkSetUri())
|
||||
.build();
|
||||
decoder.setJwtValidator(buildTokenValidator());
|
||||
return decoder;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造令牌校验器:iss / exp / nbf 由 Spring Security 负责,aud 需要显式声明。
|
||||
*
|
||||
* <p>公开出来是为了让单元测试可以直接用本地生成的 RSA 密钥验证校验规则,
|
||||
* 不必依赖 base-api 在线。</p>
|
||||
*/
|
||||
public OAuth2TokenValidator<Jwt> buildTokenValidator() {
|
||||
return new DelegatingOAuth2TokenValidator<>(
|
||||
JwtValidators.createDefaultWithIssuer(properties.getIssuer()),
|
||||
audienceValidator());
|
||||
}
|
||||
|
||||
/**
|
||||
* aud 必须命中配置的受众,避免拿到其它系统的令牌来调业务接口。
|
||||
*/
|
||||
private OAuth2TokenValidator<Jwt> audienceValidator() {
|
||||
return jwt -> {
|
||||
if (jwt.getAudience() != null && jwt.getAudience().contains(properties.getAudience())) {
|
||||
return OAuth2TokenValidatorResult.success();
|
||||
}
|
||||
return OAuth2TokenValidatorResult.failure(
|
||||
new OAuth2Error("invalid_token", "令牌受众不正确", null));
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
package com.gxwebsoft.openplatform.config;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 开放平台(base-api)对接配置。
|
||||
*
|
||||
* <p>业务服务不共享密钥,只通过 JWKS 拉取公钥本地验签,因此这里只有地址与校验参数。</p>
|
||||
*
|
||||
* @author WebSoft
|
||||
*/
|
||||
@Data
|
||||
@Component
|
||||
@ConfigurationProperties(prefix = "open-platform")
|
||||
public class OpenPlatformProperties {
|
||||
|
||||
/**
|
||||
* 是否启用开放接口链路。关闭后 /api/open/** 不再放行(会退回 internal 鉴权链路)
|
||||
*/
|
||||
private boolean enabled = true;
|
||||
|
||||
/**
|
||||
* base-api 的 JWKS 地址,资源服务启动时拉取并缓存公钥
|
||||
*/
|
||||
private String jwkSetUri = "https://base-api.websoft.top/api/v1/oauth/jwks";
|
||||
|
||||
/**
|
||||
* 令牌签发方,必须与 base-api 的 iss 声明完全一致
|
||||
*/
|
||||
private String issuer = "https://base-api.websoft.top/api";
|
||||
|
||||
/**
|
||||
* 令牌受众,必须与 base-api 的 aud 声明一致
|
||||
*/
|
||||
private String audience = "websoft-open-platform";
|
||||
|
||||
/**
|
||||
* 对外接口路径前缀,默认与 base-api 的约定保持一致
|
||||
*/
|
||||
private String pathPrefix = "/api/open";
|
||||
|
||||
/**
|
||||
* 是否对手机号等敏感字段脱敏后再返回给第三方,默认开启
|
||||
*/
|
||||
private boolean maskSensitive = true;
|
||||
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
package com.gxwebsoft.openplatform.config;
|
||||
|
||||
import com.gxwebsoft.openplatform.web.OpenPlatformAccessDeniedHandler;
|
||||
import com.gxwebsoft.openplatform.web.OpenPlatformAuthenticationEntryPoint;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
|
||||
import org.springframework.security.config.http.SessionCreationPolicy;
|
||||
import org.springframework.security.oauth2.jwt.JwtDecoder;
|
||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
|
||||
/**
|
||||
* 开放平台接口的独立安全链。
|
||||
*
|
||||
* <p>必须与内部控制台的安全链分开,原因有两点:</p>
|
||||
* <ol>
|
||||
* <li>内部控制台用 HS256 的对称密钥,开放平台用 RS256 的非对称密钥,
|
||||
* 同一条链上会互相干扰(表现为
|
||||
* {@code UnsupportedJwtException: ... may not be used to verify RS256 signatures});</li>
|
||||
* <li>控制台的 {@code SecurityConfig} 为了兼容前台站点,把「所有 GET」放行了,
|
||||
* 对外开放接口绝不能继承这个默认放行。</li>
|
||||
* </ol>
|
||||
*
|
||||
* <p>本链排在前面(@Order(1)),只匹配 {@code /api/open/**},其余请求仍由原有链路处理。</p>
|
||||
*
|
||||
* @author WebSoft
|
||||
*/
|
||||
@Configuration
|
||||
@Order(1)
|
||||
@EnableWebSecurity
|
||||
@ConditionalOnProperty(prefix = "open-platform", name = "enabled", havingValue = "true", matchIfMissing = true)
|
||||
public class OpenPlatformSecurityConfig extends WebSecurityConfigurerAdapter {
|
||||
|
||||
@Resource
|
||||
private JwtDecoder openPlatformJwtDecoder;
|
||||
@Resource
|
||||
private OpenPlatformAccessDeniedHandler openPlatformAccessDeniedHandler;
|
||||
@Resource
|
||||
private OpenPlatformAuthenticationEntryPoint openPlatformAuthenticationEntryPoint;
|
||||
@Resource
|
||||
private OpenPlatformProperties properties;
|
||||
|
||||
@Override
|
||||
protected void configure(HttpSecurity http) throws Exception {
|
||||
http.requestMatcher(new AntPathRequestMatcher(properties.getPathPrefix() + "/**"))
|
||||
.authorizeRequests()
|
||||
// 开放接口一律需要令牌,没有任何免登录路径
|
||||
.anyRequest().authenticated()
|
||||
.and()
|
||||
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
|
||||
.and()
|
||||
.csrf().disable()
|
||||
.cors()
|
||||
.and()
|
||||
.logout().disable()
|
||||
.headers().frameOptions().disable()
|
||||
.and()
|
||||
.exceptionHandling()
|
||||
.accessDeniedHandler(openPlatformAccessDeniedHandler)
|
||||
.authenticationEntryPoint(openPlatformAuthenticationEntryPoint)
|
||||
.and()
|
||||
.oauth2ResourceServer()
|
||||
// 令牌解析失败(签名错误、已过期、格式不对)由 BearerTokenAuthenticationFilter
|
||||
// 直接短路,不会经过上面的 exceptionHandling,必须在资源服务器上再配一次,
|
||||
// 否则调用方收到的是「HTTP 401 + 空 body」,与平台约定的 200 + code 不一致。
|
||||
.authenticationEntryPoint(openPlatformAuthenticationEntryPoint)
|
||||
.accessDeniedHandler(openPlatformAccessDeniedHandler)
|
||||
.jwt().decoder(openPlatformJwtDecoder);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
package com.gxwebsoft.openplatform.config;
|
||||
|
||||
import com.gxwebsoft.openplatform.web.OpenTenantInterceptor;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
|
||||
/**
|
||||
* 开放接口的 WebMvc 配置:只给 /api/open/** 挂租户绑定拦截器。
|
||||
*
|
||||
* @author WebSoft
|
||||
*/
|
||||
@Configuration
|
||||
@ConditionalOnProperty(prefix = "open-platform", name = "enabled", havingValue = "true", matchIfMissing = true)
|
||||
public class OpenPlatformWebMvcConfig implements WebMvcConfigurer {
|
||||
|
||||
@Resource
|
||||
private OpenPlatformProperties properties;
|
||||
@Resource
|
||||
private OpenTenantInterceptor openTenantInterceptor;
|
||||
|
||||
@Override
|
||||
public void addInterceptors(InterceptorRegistry registry) {
|
||||
registry.addInterceptor(openTenantInterceptor)
|
||||
.addPathPatterns(properties.getPathPrefix() + "/**");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
package com.gxwebsoft.openplatform.context;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 开放接口的调用方身份,全部来自 base-api 签发的 access token。
|
||||
*
|
||||
* <p>租户、用户、权限一律以令牌为准,不接受任何请求参数或请求头覆盖。</p>
|
||||
*
|
||||
* @author WebSoft
|
||||
*/
|
||||
@Data
|
||||
public class OpenCaller implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/** 应用标识(client_id) */
|
||||
private String clientId;
|
||||
|
||||
/** 应用所属租户,数据隔离的唯一依据 */
|
||||
private Integer tenantId;
|
||||
|
||||
/** 终端用户 id,仅授权码模式携带 */
|
||||
private Long userId;
|
||||
|
||||
/** 终端用户账号,仅授权码模式携带 */
|
||||
private String username;
|
||||
|
||||
/** 授权模式:client_credentials / authorization_code */
|
||||
private String grantType;
|
||||
|
||||
/** 已授予的权限 */
|
||||
private List<String> scopes = Collections.emptyList();
|
||||
|
||||
/**
|
||||
* 从 Spring Security 的认证对象解析调用方;非开放平台令牌返回 null。
|
||||
*/
|
||||
public static OpenCaller from(Authentication authentication) {
|
||||
if (authentication == null || !(authentication.getPrincipal() instanceof Jwt)) {
|
||||
return null;
|
||||
}
|
||||
Jwt jwt = (Jwt) authentication.getPrincipal();
|
||||
OpenCaller caller = new OpenCaller();
|
||||
caller.setClientId(jwt.getClaimAsString("client_id"));
|
||||
caller.setTenantId(toInteger(jwt.getClaim("tenant_id")));
|
||||
caller.setUserId(toLong(jwt.getClaim("user_id")));
|
||||
caller.setUsername(jwt.getClaimAsString("username"));
|
||||
caller.setGrantType(jwt.getClaimAsString("grant_type"));
|
||||
caller.setScopes(parseScopes(jwt.getClaimAsString("scope")));
|
||||
return caller;
|
||||
}
|
||||
|
||||
/**
|
||||
* 令牌的 scope 是空格分隔的字符串
|
||||
*/
|
||||
private static List<String> parseScopes(String scope) {
|
||||
if (scope == null || scope.trim().isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
List<String> scopes = new ArrayList<>();
|
||||
for (String item : scope.trim().split("\\s+")) {
|
||||
if (!item.isEmpty()) {
|
||||
scopes.add(item);
|
||||
}
|
||||
}
|
||||
return scopes;
|
||||
}
|
||||
|
||||
private static Integer toInteger(Object value) {
|
||||
return value instanceof Number ? ((Number) value).intValue() : null;
|
||||
}
|
||||
|
||||
private static Long toLong(Object value) {
|
||||
return value instanceof Number ? ((Number) value).longValue() : null;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
package com.gxwebsoft.openplatform.context;
|
||||
|
||||
/**
|
||||
* 开放接口链路的租户上下文。
|
||||
*
|
||||
* <p>与既有的「请求头 tenantId → Domain → 登录用户」取值顺序不同:开放链路只认令牌里的租户,
|
||||
* 由 {@code OpenTenantInterceptor} 在请求进入 controller 之前写入、请求结束后清理。
|
||||
* {@code MybatisPlusConfig} 会优先读取这里,从而彻底屏蔽第三方通过请求头指定租户的可能。</p>
|
||||
*
|
||||
* @author WebSoft
|
||||
*/
|
||||
public final class OpenTenantContext {
|
||||
|
||||
private static final ThreadLocal<OpenCaller> HOLDER = new ThreadLocal<>();
|
||||
|
||||
private OpenTenantContext() {
|
||||
}
|
||||
|
||||
public static void set(OpenCaller caller) {
|
||||
HOLDER.set(caller);
|
||||
}
|
||||
|
||||
public static OpenCaller get() {
|
||||
return HOLDER.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* 当前开放调用的租户;不在开放链路中时为 null
|
||||
*/
|
||||
public static Integer getTenantId() {
|
||||
OpenCaller caller = HOLDER.get();
|
||||
return caller == null ? null : caller.getTenantId();
|
||||
}
|
||||
|
||||
public static void clear() {
|
||||
HOLDER.remove();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -7,12 +7,13 @@ import com.gxwebsoft.common.core.web.PageResult;
|
||||
import com.gxwebsoft.common.system.entity.Order;
|
||||
import com.gxwebsoft.common.system.param.OrderParam;
|
||||
import com.gxwebsoft.common.system.service.OrderService;
|
||||
import com.gxwebsoft.openplatform.config.OpenPlatformProperties;
|
||||
import com.gxwebsoft.platform.openapi.OpenApi;
|
||||
import com.gxwebsoft.platform.openapi.config.OpenPlatformProperties;
|
||||
import com.gxwebsoft.openplatform.constant.OpenScopes;
|
||||
import com.gxwebsoft.openplatform.context.OpenTenantContext;
|
||||
import com.gxwebsoft.platform.openapi.context.OpenTenantContext;
|
||||
import com.gxwebsoft.openplatform.param.OpenOrderPageParam;
|
||||
import com.gxwebsoft.openplatform.vo.OpenOrderVO;
|
||||
import com.gxwebsoft.openplatform.web.OpenPageResult;
|
||||
import com.gxwebsoft.platform.openapi.web.OpenPageResult;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
@@ -40,6 +41,7 @@ import java.util.List;
|
||||
* @author WebSoft
|
||||
*/
|
||||
@Tag(name = "开放接口-订单")
|
||||
@OpenApi
|
||||
@RestController
|
||||
@ConditionalOnProperty(prefix = "open-platform", name = "enabled", havingValue = "true", matchIfMissing = true)
|
||||
@RequestMapping("/api/open/v1/order")
|
||||
|
||||
@@ -7,12 +7,13 @@ import com.gxwebsoft.common.core.web.PageResult;
|
||||
import com.gxwebsoft.common.system.entity.User;
|
||||
import com.gxwebsoft.common.system.param.UserParam;
|
||||
import com.gxwebsoft.common.system.service.UserService;
|
||||
import com.gxwebsoft.openplatform.config.OpenPlatformProperties;
|
||||
import com.gxwebsoft.platform.openapi.OpenApi;
|
||||
import com.gxwebsoft.platform.openapi.config.OpenPlatformProperties;
|
||||
import com.gxwebsoft.openplatform.constant.OpenScopes;
|
||||
import com.gxwebsoft.openplatform.context.OpenTenantContext;
|
||||
import com.gxwebsoft.platform.openapi.context.OpenTenantContext;
|
||||
import com.gxwebsoft.openplatform.param.OpenUserPageParam;
|
||||
import com.gxwebsoft.openplatform.vo.OpenUserVO;
|
||||
import com.gxwebsoft.openplatform.web.OpenPageResult;
|
||||
import com.gxwebsoft.platform.openapi.web.OpenPageResult;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
@@ -34,6 +35,7 @@ import java.util.List;
|
||||
* @author WebSoft
|
||||
*/
|
||||
@Tag(name = "开放接口-用户")
|
||||
@OpenApi
|
||||
@RestController
|
||||
@ConditionalOnProperty(prefix = "open-platform", name = "enabled", havingValue = "true", matchIfMissing = true)
|
||||
@RequestMapping("/api/open/v1/user")
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
package com.gxwebsoft.openplatform.web;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 开放接口的分页返回结构。
|
||||
*
|
||||
* <p>刻意不复用内部的 {@code PageResult}(字段是 {@code count}):对外接口是长期契约,
|
||||
* 用独立结构可以避免以后内部字段调整波及第三方。</p>
|
||||
*
|
||||
* @author WebSoft
|
||||
*/
|
||||
@Data
|
||||
@Schema(name = "OpenPageResult", description = "开放接口分页结果")
|
||||
public class OpenPageResult<T> implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "当前页数据")
|
||||
private List<T> list = Collections.emptyList();
|
||||
|
||||
@Schema(description = "总记录数")
|
||||
private Long total;
|
||||
|
||||
@Schema(description = "当前页码,从 1 开始")
|
||||
private Long page;
|
||||
|
||||
@Schema(description = "每页数量")
|
||||
private Long limit;
|
||||
|
||||
public OpenPageResult() {
|
||||
}
|
||||
|
||||
public OpenPageResult(List<T> list, Long total, Long page, Long limit) {
|
||||
this.list = list == null ? Collections.emptyList() : list;
|
||||
this.total = total;
|
||||
this.page = page;
|
||||
this.limit = limit;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
package com.gxwebsoft.openplatform.web;
|
||||
|
||||
import com.gxwebsoft.common.core.Constants;
|
||||
import com.gxwebsoft.common.core.utils.CommonUtil;
|
||||
import org.springframework.security.access.AccessDeniedException;
|
||||
import org.springframework.security.web.access.AccessDeniedHandler;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* 开放接口权限不足响应:不返回 {@code error} 字段,避免泄露内部信息。
|
||||
*
|
||||
* @author WebSoft
|
||||
*/
|
||||
@Component
|
||||
public class OpenPlatformAccessDeniedHandler implements AccessDeniedHandler {
|
||||
|
||||
@Override
|
||||
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e)
|
||||
throws IOException {
|
||||
CommonUtil.responseError(response, Constants.UNAUTHORIZED_CODE, "权限不足,请确认应用已获得该接口权限", null);
|
||||
}
|
||||
|
||||
}
|
||||
-31
@@ -1,31 +0,0 @@
|
||||
package com.gxwebsoft.openplatform.web;
|
||||
|
||||
import com.gxwebsoft.common.core.Constants;
|
||||
import com.gxwebsoft.common.core.utils.CommonUtil;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.web.AuthenticationEntryPoint;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* 开放接口未认证响应。
|
||||
*
|
||||
* <p>与内部链路共用 {@code JwtAuthenticationEntryPoint} 的差别只有一个:不返回 {@code error} 字段。
|
||||
* 该字段承载的是异常 {@code toString()},会把内部类名与解析细节暴露给第三方。HTTP 状态码保持 200,
|
||||
* 与 base-api 一致,调用方以 body 里的 {@code code} 判断成败。</p>
|
||||
*
|
||||
* @author WebSoft
|
||||
*/
|
||||
@Component
|
||||
public class OpenPlatformAuthenticationEntryPoint implements AuthenticationEntryPoint {
|
||||
|
||||
@Override
|
||||
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e)
|
||||
throws IOException {
|
||||
CommonUtil.responseError(response, Constants.UNAUTHENTICATED_CODE, "令牌缺失或无效", null);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
package com.gxwebsoft.openplatform.web;
|
||||
|
||||
import com.gxwebsoft.common.core.Constants;
|
||||
import com.gxwebsoft.common.core.exception.BusinessException;
|
||||
import com.gxwebsoft.common.core.web.ApiResult;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.security.access.AccessDeniedException;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
|
||||
/**
|
||||
* 开放接口专用异常处理。
|
||||
*
|
||||
* <p>只作用于 {@code com.gxwebsoft.openplatform} 下的 controller,且优先级高于全局的
|
||||
* {@code GlobalExceptionHandler},目的是<b>不把 {@code error} 字段(异常 toString)返回给第三方</b>。</p>
|
||||
*
|
||||
* @author WebSoft
|
||||
*/
|
||||
@RestControllerAdvice(basePackages = "com.gxwebsoft.openplatform")
|
||||
@Order(Ordered.HIGHEST_PRECEDENCE)
|
||||
public class OpenPlatformExceptionAdvice {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(OpenPlatformExceptionAdvice.class);
|
||||
|
||||
@ExceptionHandler(BusinessException.class)
|
||||
public ApiResult<?> businessExceptionHandler(BusinessException e) {
|
||||
return new ApiResult<>(e.getCode(), e.getMessage());
|
||||
}
|
||||
|
||||
@ExceptionHandler(AccessDeniedException.class)
|
||||
public ApiResult<?> accessDeniedExceptionHandler(AccessDeniedException e) {
|
||||
return new ApiResult<>(Constants.UNAUTHORIZED_CODE, "权限不足,请确认应用已获得该接口权限");
|
||||
}
|
||||
|
||||
@ExceptionHandler(Throwable.class)
|
||||
public ApiResult<?> exceptionHandler(Throwable e) {
|
||||
logger.error("开放接口处理失败: {}", e.getMessage(), e);
|
||||
return new ApiResult<>(Constants.RESULT_ERROR_CODE, Constants.RESULT_ERROR_MSG);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
package com.gxwebsoft.openplatform.web;
|
||||
|
||||
import com.gxwebsoft.common.core.Constants;
|
||||
import com.gxwebsoft.common.core.exception.BusinessException;
|
||||
import com.gxwebsoft.openplatform.context.OpenCaller;
|
||||
import com.gxwebsoft.openplatform.context.OpenTenantContext;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.servlet.HandlerInterceptor;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
/**
|
||||
* 开放接口的租户绑定拦截器。
|
||||
*
|
||||
* <p>令牌验签由 Spring Security 的 resource server 完成,这里只负责把 {@code tenant_id}
|
||||
* 落到线程上下文,供 ORM 租户插件与业务代码使用。令牌没有租户时直接拒绝,
|
||||
* 不回退到请求头,避免第三方通过 {@code tenantId} 头越权访问其它租户数据。</p>
|
||||
*
|
||||
* @author WebSoft
|
||||
*/
|
||||
@Component
|
||||
public class OpenTenantInterceptor implements HandlerInterceptor {
|
||||
|
||||
@Override
|
||||
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
|
||||
OpenCaller caller = OpenCaller.from(SecurityContextHolder.getContext().getAuthentication());
|
||||
if (caller == null) {
|
||||
throw new BusinessException(Constants.UNAUTHENTICATED_CODE, Constants.UNAUTHENTICATED_MSG);
|
||||
}
|
||||
if (caller.getTenantId() == null) {
|
||||
throw new BusinessException(Constants.UNAUTHORIZED_CODE, "令牌缺少租户信息,无法确定数据范围");
|
||||
}
|
||||
OpenTenantContext.set(caller);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
|
||||
Exception ex) {
|
||||
// 线程复用,必须清理,否则租户会串到下一个请求
|
||||
OpenTenantContext.clear();
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user