diff --git a/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/filter/TokenAuthenticationFilter.java b/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/filter/TokenAuthenticationFilter.java index 11b82a12b..8ccbb8fe6 100644 --- a/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/filter/TokenAuthenticationFilter.java +++ b/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/filter/TokenAuthenticationFilter.java @@ -4,6 +4,7 @@ import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.framework.common.exception.ServiceException; import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils; import cn.iocoder.yudao.framework.security.config.SecurityProperties; import cn.iocoder.yudao.framework.security.core.LoginUser; @@ -41,28 +42,34 @@ public class TokenAuthenticationFilter extends OncePerRequestFilter { @SuppressWarnings("NullableProblems") protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { - String token = SecurityFrameworkUtils.obtainAuthorization(request, securityProperties.getTokenHeader()); - if (StrUtil.isNotEmpty(token)) { - Integer userType = WebFrameworkUtils.getLoginUserType(request); - try { - // 1.1 基于 token 构建登录用户 - LoginUser loginUser = buildLoginUserByToken(token, userType); - // 1.2 模拟 Login 功能,方便日常开发调试 - if (loginUser == null) { - loginUser = mockLoginUser(request, token, userType); - } + // 情况一,基于 header[login-user] 获得用户,例如说来自 Gateway 或者其它服务透传 + LoginUser loginUser = buildLoginUserByHeader(request); - // 2. 设置当前用户 - if (loginUser != null) { - SecurityFrameworkUtils.setLoginUser(loginUser, request); + // 情况二,基于 Token 获得用户 + // 注意,这里主要满足直接使用 Nginx 直接转发到 Spring Cloud 服务的场景。 + if (loginUser == null) { + String token = SecurityFrameworkUtils.obtainAuthorization(request, securityProperties.getTokenHeader()); + if (StrUtil.isNotEmpty(token)) { + Integer userType = WebFrameworkUtils.getLoginUserType(request); + try { + // 1.1 基于 token 构建登录用户 + loginUser = buildLoginUserByToken(token, userType); + // 1.2 模拟 Login 功能,方便日常开发调试 + if (loginUser == null) { + loginUser = mockLoginUser(request, token, userType); + } + } catch (Throwable ex) { + CommonResult result = globalExceptionHandler.allExceptionHandler(request, ex); + ServletUtils.writeJSON(response, result); + return; } - } catch (Throwable ex) { - CommonResult result = globalExceptionHandler.allExceptionHandler(request, ex); - ServletUtils.writeJSON(response, result); - return; } } + // 设置当前用户 + if (loginUser != null) { + SecurityFrameworkUtils.setLoginUser(loginUser, request); + } // 继续过滤链 chain.doFilter(request, response); } @@ -113,4 +120,9 @@ public class TokenAuthenticationFilter extends OncePerRequestFilter { .setTenantId(WebFrameworkUtils.getTenantId(request)); } + private LoginUser buildLoginUserByHeader(HttpServletRequest request) { + String loginUserStr = request.getHeader(SecurityFrameworkUtils.LOGIN_USER_HEADER); + return StrUtil.isNotEmpty(loginUserStr) ? JsonUtils.parseObject(loginUserStr, LoginUser.class) : null; + } + } diff --git a/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/util/SecurityFrameworkUtils.java b/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/util/SecurityFrameworkUtils.java index 5dc17b626..3fa9c0004 100644 --- a/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/util/SecurityFrameworkUtils.java +++ b/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/util/SecurityFrameworkUtils.java @@ -22,6 +22,8 @@ public class SecurityFrameworkUtils { public static final String AUTHORIZATION_BEARER = "Bearer"; + public static final String LOGIN_USER_HEADER = "login-user"; + private SecurityFrameworkUtils() {} /** diff --git a/yudao-gateway/pom.xml b/yudao-gateway/pom.xml index 8d2386954..fccace9c8 100644 --- a/yudao-gateway/pom.xml +++ b/yudao-gateway/pom.xml @@ -35,10 +35,10 @@ spring-cloud-starter-loadbalancer - - org.springframework.cloud - spring-cloud-starter-openfeign - + + + + @@ -48,4 +48,27 @@ + + + ${project.artifactId} + + + + org.springframework.boot + spring-boot-maven-plugin + 2.6.7 + + true + + + + + repackage + + + + + + + diff --git a/yudao-gateway/src/main/java/cn/iocoder/yudao/gateway/GatewayServerApplication.java b/yudao-gateway/src/main/java/cn/iocoder/yudao/gateway/GatewayServerApplication.java index 5ad844753..d5904db49 100644 --- a/yudao-gateway/src/main/java/cn/iocoder/yudao/gateway/GatewayServerApplication.java +++ b/yudao-gateway/src/main/java/cn/iocoder/yudao/gateway/GatewayServerApplication.java @@ -1,14 +1,9 @@ package cn.iocoder.yudao.gateway; -import cn.iocoder.yudao.module.system.api.oauth2.OAuth2TokenApi; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.cloud.openfeign.EnableFeignClients; @SpringBootApplication -@EnableFeignClients(clients = { - OAuth2TokenApi.class -}) // TODO 芋艿:需要改下 public class GatewayServerApplication { public static void main(String[] args) { diff --git a/yudao-gateway/src/main/java/cn/iocoder/yudao/gateway/config/TmpConfiguration.java b/yudao-gateway/src/main/java/cn/iocoder/yudao/gateway/config/TmpConfiguration.java deleted file mode 100644 index 0d00d3288..000000000 --- a/yudao-gateway/src/main/java/cn/iocoder/yudao/gateway/config/TmpConfiguration.java +++ /dev/null @@ -1,16 +0,0 @@ -package cn.iocoder.yudao.gateway.config; - -import org.springframework.cloud.client.loadbalancer.reactive.ReactorLoadBalancerExchangeFilterFunction; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.web.reactive.function.client.WebClient; - -@Configuration -public class TmpConfiguration { - - @Bean - public WebClient webClient(ReactorLoadBalancerExchangeFilterFunction lbFunction) { - return WebClient.builder().filter(lbFunction).build(); - } - -} diff --git a/yudao-gateway/src/main/java/cn/iocoder/yudao/gateway/filter/TokenAuthenticationFilter.java b/yudao-gateway/src/main/java/cn/iocoder/yudao/gateway/filter/TokenAuthenticationFilter.java index b7ad27f2e..5a456f0dd 100644 --- a/yudao-gateway/src/main/java/cn/iocoder/yudao/gateway/filter/TokenAuthenticationFilter.java +++ b/yudao-gateway/src/main/java/cn/iocoder/yudao/gateway/filter/TokenAuthenticationFilter.java @@ -4,11 +4,11 @@ import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.framework.common.pojo.CommonResult; import cn.iocoder.yudao.framework.common.util.json.JsonUtils; import cn.iocoder.yudao.gateway.util.SecurityFrameworkUtils; +import cn.iocoder.yudao.gateway.util.WebFrameworkUtils; import cn.iocoder.yudao.module.system.api.oauth2.OAuth2TokenApi; import cn.iocoder.yudao.module.system.api.oauth2.dto.OAuth2AccessTokenCheckRespDTO; -import cn.iocoder.yudao.module.system.api.oauth2.dto.OAuth2AccessTokenRespDTO; import com.fasterxml.jackson.core.type.TypeReference; -import com.google.common.net.HttpHeaders; +import org.springframework.cloud.client.loadbalancer.reactive.ReactorLoadBalancerExchangeFilterFunction; import org.springframework.cloud.gateway.filter.GatewayFilterChain; import org.springframework.cloud.gateway.filter.GlobalFilter; import org.springframework.core.Ordered; @@ -29,14 +29,21 @@ import java.util.function.Function; * * @author 芋道源码 */ -@Component // TODO 芋艿:要改成 configuration +@Component public class TokenAuthenticationFilter implements GlobalFilter, Ordered { -// @Resource -// private OAuth2TokenApi oauth2TokenApi; + private static final TypeReference> CHECK_RESULT_TYPE_REFERENCE + = new TypeReference>() {}; - @Resource - private WebClient webClient; + private final WebClient webClient; + + public TokenAuthenticationFilter(ReactorLoadBalancerExchangeFilterFunction lbFunction) { + // Q:为什么不使用 OAuth2TokenApi 进行调用? + // A1:Spring 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(); + } @Override public Mono filter(final ServerWebExchange exchange, GatewayFilterChain chain) { @@ -46,46 +53,23 @@ public class TokenAuthenticationFilter implements GlobalFilter, Ordered { return chain.filter(exchange); } -// exchange = exchange.mutate().request(r -> r.headers(new Consumer() { -// @Override -// public void accept(HttpHeaders headers) { -// headers.set("user-id", "1"); -// } -// })).build(); - - -// return Mono.fromCallable(new Callable>() { -// @Override -// public CommonResult call() throws Exception { -//// return oauth2TokenApi.checkAccessToken("1234"); -// return CommonResult.success(new OAuth2AccessTokenCheckRespDTO().setUserId(1L)); -// } -// }).subscribeOn(Schedulers.boundedElastic()).flatMap(new Function, Mono>() { -// @Override -// public Mono apply(CommonResult oAuth2AccessTokenCheckRespDTOCommonResult) { -// return chain.filter(exchange); -// } -// }); - // 情况二,如果有 Token 令牌,则解析对应 userId、userType、tenantId 等字段,并通过 通过 Header 转发给服务 - // TODO 芋艿:tenant-id - String tenantId = exchange.getRequest().getHeaders().getFirst("tenant-id"); return webClient.get() .uri(OAuth2TokenApi.URL_CHECK, uriBuilder -> uriBuilder.queryParam("accessToken", token).build()) - .header("tenant-id", tenantId) + .headers(httpHeaders -> WebFrameworkUtils.setTenantIdHeader(exchange, httpHeaders)) // 设置租户的 Header .retrieve().bodyToMono(String.class) // 发起请求,设置 body 为 String 结果 - // 处理请求的结果 - .flatMap((Function>) body -> chain.filter(buildNewServerWebExchange(exchange, body))); + .flatMap((Function>) body -> chain.filter(buildNewServerWebExchange(exchange, body))); // 处理请求的结果 } private ServerWebExchange buildNewServerWebExchange(ServerWebExchange exchange, String body) { // 校验 Token 令牌失败,则直接返回 - CommonResult result = JsonUtils.parseObject(body, CommonResult.class); + CommonResult result = JsonUtils.parseObject(body, CHECK_RESULT_TYPE_REFERENCE); if (result == null || result.isError()) { return exchange; } - // 创建新的 exchange 对象 - return exchange.mutate().request(builder -> builder.header("login-user", result.getData().toString())).build(); + + // 将访问令牌封装成 LoginUser,并设置到 login-user 的请求头,使用 json 存储值 + return exchange.mutate().request(builder -> SecurityFrameworkUtils.setLoginUserHeader(builder, result.getData())).build(); } @Override diff --git a/yudao-gateway/src/main/java/cn/iocoder/yudao/gateway/util/SecurityFrameworkUtils.java b/yudao-gateway/src/main/java/cn/iocoder/yudao/gateway/util/SecurityFrameworkUtils.java index 1ce8af64d..ea107fd32 100644 --- a/yudao-gateway/src/main/java/cn/iocoder/yudao/gateway/util/SecurityFrameworkUtils.java +++ b/yudao-gateway/src/main/java/cn/iocoder/yudao/gateway/util/SecurityFrameworkUtils.java @@ -1,8 +1,14 @@ package cn.iocoder.yudao.gateway.util; +import cn.hutool.core.map.MapUtil; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import cn.iocoder.yudao.module.system.api.oauth2.dto.OAuth2AccessTokenCheckRespDTO; +import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.util.StringUtils; import org.springframework.web.server.ServerWebExchange; +import java.util.Map; + /** * 安全服务工具类 * @@ -12,9 +18,11 @@ import org.springframework.web.server.ServerWebExchange; */ public class SecurityFrameworkUtils { - public static final String AUTHORIZATION_HEADER = "Authorization"; + private static final String AUTHORIZATION_HEADER = "Authorization"; - public static final String AUTHORIZATION_BEARER = "Bearer"; + private static final String AUTHORIZATION_BEARER = "Bearer"; + + private static final String LOGIN_USER_HEADER = "login-user"; private SecurityFrameworkUtils() {} @@ -36,4 +44,21 @@ public class SecurityFrameworkUtils { return authorization.substring(index + 7).trim(); } + /** + * 将访问令牌封装成 LoginUser,并设置到 login-user 的请求头,使用 json 存储值 + * + * @param builder 请求 + * @param token 访问令牌 + */ + public static void setLoginUserHeader(ServerHttpRequest.Builder builder, OAuth2AccessTokenCheckRespDTO token) { + // 构建 LoginUser 对象。由于 Gateway 没有 loginUser 类,所以使用 Map + Map loginUser = MapUtil.newHashMap(4); + loginUser.put("id", token.getUserId()); + loginUser.put("userType", token.getUserType()); + loginUser.put("tenantId", token.getTenantId()); + loginUser.put("scopes", token.getScopes()); + // 设置到 Header 中 + builder.header(LOGIN_USER_HEADER, JsonUtils.toJsonString(loginUser)); + } + } diff --git a/yudao-gateway/src/main/java/cn/iocoder/yudao/gateway/util/WebFrameworkUtils.java b/yudao-gateway/src/main/java/cn/iocoder/yudao/gateway/util/WebFrameworkUtils.java new file mode 100644 index 000000000..e696ae710 --- /dev/null +++ b/yudao-gateway/src/main/java/cn/iocoder/yudao/gateway/util/WebFrameworkUtils.java @@ -0,0 +1,41 @@ +package cn.iocoder.yudao.gateway.util; + +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.module.system.api.oauth2.dto.OAuth2AccessTokenCheckRespDTO; +import org.springframework.http.HttpHeaders; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.web.server.ServerWebExchange; + +import java.util.HashMap; +import java.util.Map; + +/** + * Web 工具类 + * + * copy from yudao-spring-boot-starter-web 的 WebFrameworkUtils 类 + * + * @author 芋道源码 + */ +public class WebFrameworkUtils { + + @SuppressWarnings("UastIncorrectHttpHeaderInspection") + private static final String HEADER_TENANT_ID = "tenant-id"; + + private WebFrameworkUtils() {} + + /** + * 将 Gateway 请求中的 header,设置到 HttpHeaders 中 + * + * @param exchange Gateway 请求 + * @param httpHeaders WebClient 的请求 + */ + public static void setTenantIdHeader(ServerWebExchange exchange, HttpHeaders httpHeaders) { + String tenantId = exchange.getRequest().getHeaders().getFirst(HEADER_TENANT_ID); + if (StrUtil.isNotEmpty(tenantId)) { + return; + } + httpHeaders.set(HEADER_TENANT_ID, tenantId); + } + +}